PocketBase supports S3-compatible storage backends as an alternative to local filesystem storage. This includes AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2, and other S3-compatible services.
Why use S3 storage
S3-compatible storage provides several advantages:
Scalability - Handle unlimited file storage without disk space concerns
Reliability - Built-in redundancy and durability (99.999999999% for AWS S3)
Performance - Geographic distribution and CDN integration
Cost efficiency - Pay only for what you use, with automatic scaling
Backups - Easy replication across regions
Storage backend selection
PocketBase automatically selects the storage backend based on your settings:
// From core/base.go:714
func ( app * BaseApp ) NewFilesystem () ( * filesystem . System , error ) {
if app . settings != nil && app . settings . S3 . Enabled {
return filesystem . NewS3 (
app . settings . S3 . Bucket ,
app . settings . S3 . Region ,
app . settings . S3 . Endpoint ,
app . settings . S3 . AccessKey ,
app . settings . S3 . Secret ,
app . settings . S3 . ForcePathStyle ,
)
}
// Fallback to local filesystem
return filesystem . NewLocal ( filepath . Join ( app . DataDir (), LocalStorageDirName ))
}
Configuration via Admin UI
The easiest way to configure S3 storage is through the PocketBase Admin UI:
Navigate to Settings
Go to Settings > Files storage in the Admin UI
Enable S3 storage
Toggle “Use S3 storage” to enable it
Enter S3 credentials
Fill in your S3 bucket configuration:
Bucket name
Region
Endpoint
Access key
Secret key
Force path style (if required)
Test connection
Use the “Test” button to verify your configuration
Save settings
Click “Save” to apply the configuration
Changing from local to S3 storage (or vice versa) does not automatically migrate existing files. You’ll need to manually copy files between storage backends.
Configuration via code
You can also configure S3 storage programmatically:
package main
import (
" github.com/pocketbase/pocketbase "
" github.com/pocketbase/pocketbase/core "
)
func main () {
app := pocketbase . New ()
app . OnServe (). BindFunc ( func ( e * core . ServeEvent ) error {
// Update S3 settings
settings , err := e . App . Settings (). Clone ()
if err != nil {
return err
}
settings . S3 . Enabled = true
settings . S3 . Bucket = "my-bucket"
settings . S3 . Region = "us-east-1"
settings . S3 . Endpoint = "https://s3.amazonaws.com"
settings . S3 . AccessKey = "YOUR_ACCESS_KEY"
settings . S3 . Secret = "YOUR_SECRET_KEY"
settings . S3 . ForcePathStyle = false
if err := e . App . Settings (). Merge ( settings ); err != nil {
return err
}
return e . Next ()
})
if err := app . Start (); err != nil {
panic ( err )
}
}
Environment variables
For production deployments, use environment variables to avoid hardcoding credentials:
export S3_ENABLED = true
export S3_BUCKET = my-bucket
export S3_REGION = us-east-1
export S3_ENDPOINT = https :// s3 . amazonaws . com
export S3_ACCESS_KEY = YOUR_ACCESS_KEY
export S3_SECRET_KEY = YOUR_SECRET_KEY
export S3_FORCE_PATH_STYLE = false
Then read them in your code:
import " os "
settings . S3 . Enabled = os . Getenv ( "S3_ENABLED" ) == "true"
settings . S3 . Bucket = os . Getenv ( "S3_BUCKET" )
settings . S3 . Region = os . Getenv ( "S3_REGION" )
settings . S3 . Endpoint = os . Getenv ( "S3_ENDPOINT" )
settings . S3 . AccessKey = os . Getenv ( "S3_ACCESS_KEY" )
settings . S3 . Secret = os . Getenv ( "S3_SECRET_KEY" )
settings . S3 . ForcePathStyle = os . Getenv ( "S3_FORCE_PATH_STYLE" ) == "true"
Provider-specific configurations
AWS S3
settings . S3 . Enabled = true
settings . S3 . Bucket = "my-app-files"
settings . S3 . Region = "us-east-1"
settings . S3 . Endpoint = "https://s3.amazonaws.com"
settings . S3 . AccessKey = "AKIA..."
settings . S3 . Secret = "your-secret-key"
settings . S3 . ForcePathStyle = false
For AWS S3, you can omit the endpoint - it will be constructed automatically from the region: https://s3.{region}.amazonaws.com
MinIO
settings . S3 . Enabled = true
settings . S3 . Bucket = "pocketbase"
settings . S3 . Region = "us-east-1" // MinIO region (can be any value)
settings . S3 . Endpoint = "https://minio.example.com"
settings . S3 . AccessKey = "minioadmin"
settings . S3 . Secret = "minioadmin"
settings . S3 . ForcePathStyle = true // MinIO requires path-style URLs
DigitalOcean Spaces
settings . S3 . Enabled = true
settings . S3 . Bucket = "my-space"
settings . S3 . Region = "nyc3"
settings . S3 . Endpoint = "https://nyc3.digitaloceanspaces.com"
settings . S3 . AccessKey = "DO00..."
settings . S3 . Secret = "your-secret-key"
settings . S3 . ForcePathStyle = false
Cloudflare R2
settings . S3 . Enabled = true
settings . S3 . Bucket = "my-bucket"
settings . S3 . Region = "auto"
settings . S3 . Endpoint = "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com"
settings . S3 . AccessKey = "your-access-key"
settings . S3 . Secret = "your-secret-key"
settings . S3 . ForcePathStyle = false
Backblaze B2
settings . S3 . Enabled = true
settings . S3 . Bucket = "my-bucket"
settings . S3 . Region = "us-west-002"
settings . S3 . Endpoint = "https://s3.us-west-002.backblazeb2.com"
settings . S3 . AccessKey = "your-key-id"
settings . S3 . Secret = "your-application-key"
settings . S3 . ForcePathStyle = false
Path-style vs. virtual-hosted-style URLs
S3 supports two URL styles:
Virtual-hosted-style (default)
https://bucket-name.s3.amazonaws.com/path/to/file.txt
Used by AWS S3, DigitalOcean Spaces, and most modern providers.
Set ForcePathStyle: false (or omit, as it’s the default).
Path-style
https://s3.amazonaws.com/bucket-name/path/to/file.txt
Required by MinIO and some self-hosted S3 implementations.
Set ForcePathStyle: true.
AWS S3 deprecated path-style URLs for buckets created after September 30, 2020. Use virtual-hosted-style for AWS S3.
Direct S3 filesystem creation
For advanced use cases, you can create S3 filesystem instances directly:
import (
" github.com/pocketbase/pocketbase/tools/filesystem "
)
func createS3Filesystem () ( * filesystem . System , error ) {
fsys , err := filesystem . NewS3 (
"my-bucket" , // bucket
"us-east-1" , // region
"https://s3.amazonaws.com" , // endpoint
"AKIA..." , // access key
"your-secret-key" , // secret key
false , // force path style
)
if err != nil {
return nil , err
}
return fsys , nil
}
Using the S3 filesystem
fsys , err := createS3Filesystem ()
if err != nil {
return err
}
defer fsys . Close ()
// Upload file
file , _ := filesystem . NewFileFromPath ( "/path/to/file.txt" )
err = fsys . UploadFile ( file , "uploads/file.txt" )
// Download file
reader , err := fsys . GetReader ( "uploads/file.txt" )
if err != nil {
return err
}
defer reader . Close ()
// List files
files , err := fsys . List ( "uploads/" )
for _ , file := range files {
fmt . Println ( file . Key , file . Size , file . ModTime )
}
// Delete file
err = fsys . Delete ( "uploads/file.txt" )
// Copy file
err = fsys . Copy ( "uploads/file.txt" , "backups/file.txt" )
S3 implementation details
PocketBase uses a custom S3 implementation (not AWS SDK) for minimal dependencies:
// From tools/filesystem/filesystem.go:44
func NewS3 (
bucketName string ,
region string ,
endpoint string ,
accessKey string ,
secretKey string ,
s3ForcePathStyle bool ,
) ( * System , error ) {
ctx := context . Background ()
client := & s3 . S3 {
Bucket : bucketName ,
Region : region ,
Endpoint : endpoint ,
AccessKey : accessKey ,
SecretKey : secretKey ,
UsePathStyle : s3ForcePathStyle ,
}
drv , err := s3blob . New ( client )
if err != nil {
return nil , err
}
return & System { ctx : ctx , bucket : blob . NewBucket ( drv )}, nil
}
The internal S3 client supports:
✅ GetObject - Download files
✅ HeadObject - Get file metadata
✅ PutObject - Upload files
✅ DeleteObject - Delete files
✅ CopyObject - Copy files
✅ ListObjects - List files with pagination
✅ Multipart uploads - For large files
✅ AWS Signature V4 - Authentication
Bucket permissions
Your S3 bucket needs the following permissions:
{
"Version" : "2012-10-17" ,
"Statement" : [
{
"Effect" : "Allow" ,
"Action" : [
"s3:GetObject" ,
"s3:PutObject" ,
"s3:DeleteObject" ,
"s3:ListBucket" ,
"s3:GetObjectAttributes"
],
"Resource" : [
"arn:aws:s3:::my-bucket" ,
"arn:aws:s3:::my-bucket/*"
]
}
]
}
Never make your bucket publicly readable! PocketBase handles all file access through its API, respecting collection rules and file field protection settings.
CORS configuration
If you’re accessing files directly from the browser, configure CORS on your bucket:
[
{
"AllowedOrigins" : [ "https://your-app.com" ],
"AllowedMethods" : [ "GET" , "HEAD" ],
"AllowedHeaders" : [ "*" ],
"ExposeHeaders" : [ "ETag" ],
"MaxAgeSeconds" : 3600
}
]
CORS is only needed if you’re serving files directly from S3. When using PocketBase’s API endpoint, CORS is handled by PocketBase.
Migration between storage backends
To migrate files from local to S3 (or vice versa):
func migrateStorage ( app core . App ) error {
// Create source filesystem (local)
src , err := filesystem . NewLocal ( "./pb_data/storage" )
if err != nil {
return err
}
defer src . Close ()
// Create destination filesystem (S3)
dst , err := filesystem . NewS3 (
"my-bucket" ,
"us-east-1" ,
"https://s3.amazonaws.com" ,
"access-key" ,
"secret-key" ,
false ,
)
if err != nil {
return err
}
defer dst . Close ()
// List all files
files , err := src . List ( "" )
if err != nil {
return err
}
// Copy each file
for _ , file := range files {
if file . IsDir {
continue
}
// Read from source
reader , err := src . GetReader ( file . Key )
if err != nil {
return err
}
// Read all data
data , err := io . ReadAll ( reader )
reader . Close ()
if err != nil {
return err
}
// Upload to destination
err = dst . Upload ( data , file . Key )
if err != nil {
return err
}
fmt . Printf ( "Migrated: %s \n " , file . Key )
}
return nil
}
Multipart uploads
For files larger than 5MB, PocketBase automatically uses multipart uploads:
// Configure multipart upload settings
writerOpts := & blob . WriterOptions {
BufferSize : 10 << 20 , // 10MB part size
MaxConcurrency : 5 , // 5 concurrent uploads
}
Concurrent operations
The S3 driver supports concurrent operations:
// Concurrent file uploads
var wg sync . WaitGroup
for _ , file := range files {
wg . Add ( 1 )
go func ( f * filesystem . File ) {
defer wg . Done ()
fsys . UploadFile ( f , f . Name )
}( file )
}
wg . Wait ()
Caching
PocketBase sets aggressive cache headers for S3 files:
Cache-Control : max-age=2592000, stale-while-revalidate=86400
This caches files for 30 days while allowing background revalidation.
Troubleshooting
Connection errors
failed to initialize S3: endpoint must be set
Ensure the endpoint URL is set correctly and includes the protocol (https://).
Authentication errors
status code 403: Access Denied
Verify your access key and secret key. Check that your IAM user/role has the required permissions.
Path-style errors
status code 404: NoSuchBucket
Try toggling ForcePathStyle. MinIO requires true, AWS S3 requires false.
Region errors
AuthorizationHeaderMalformed: The authorization header is malformed
Ensure the region matches your bucket’s region exactly.
Next steps
File upload Learn how to upload files to S3 storage
File download Understand how files are served from S3