S3 Storage
S3 Storage
Connect ModulaCMS to S3-compatible object storage for media assets and backups.
Supported Providers
Any S3-compatible storage provider works with ModulaCMS:
- AWS S3
- MinIO (self-hosted)
- DigitalOcean Spaces
- Backblaze B2
- Cloudflare R2
- Linode Object Storage
Configuration
Set these fields in modula.config.json:
| Field | Type | Default | Description |
|---|---|---|---|
bucket_region |
string | "us-east-1" |
S3 region |
bucket_media |
string | "" |
Bucket name for media assets |
bucket_backup |
string | "" |
Bucket name for backups |
bucket_endpoint |
string | "" |
S3 API endpoint hostname (without scheme) |
bucket_access_key |
string | "" |
S3 access key ID |
bucket_secret_key |
string | "" |
S3 secret access key |
bucket_public_url |
string | (falls back to endpoint) | Public-facing base URL for media links |
bucket_default_acl |
string | "" |
ACL applied to uploaded objects |
bucket_force_path_style |
bool | true |
Use path-style URLs instead of virtual-hosted |
max_upload_size |
integer | 10485760 |
Maximum upload size in bytes (10 MB) |
All S3 storage fields are hot-reloadable. Changes take effect without restarting the server.
Good to know: Do not include the URL scheme (
http://orhttps://) inbucket_endpoint. The scheme is determined by theenvironmentconfig field. Local environments (local,local-docker) use HTTP; all others use HTTPS.
Set Up AWS S3
- Create an S3 bucket in the AWS Console.
- Create an IAM user with
s3:PutObject,s3:GetObject,s3:DeleteObject, ands3:HeadBucketpermissions on the bucket. - Generate access keys for the IAM user.
{
"bucket_region": "us-east-1",
"bucket_media": "my-cms-media",
"bucket_backup": "my-cms-backups",
"bucket_endpoint": "s3.us-east-1.amazonaws.com",
"bucket_access_key": "AKIAIOSFODNN7EXAMPLE",
"bucket_secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"bucket_public_url": "https://my-cms-media.s3.us-east-1.amazonaws.com",
"bucket_force_path_style": false
}
Good to know: AWS S3 uses virtual-hosted style URLs by default. Set
bucket_force_path_styletofalsefor AWS. Most other providers require it set totrue.
Set Up MinIO
MinIO is a self-hosted S3-compatible server, commonly used for local development and Docker-based deployments.
{
"bucket_region": "us-east-1",
"bucket_media": "media",
"bucket_backup": "backups",
"bucket_endpoint": "localhost:9000",
"bucket_access_key": "minioadmin",
"bucket_secret_key": "minioadmin",
"bucket_public_url": "http://localhost:9000",
"bucket_force_path_style": true
}
MinIO in Docker
When running both ModulaCMS and MinIO in Docker containers, the two services communicate over the Docker network using container hostnames. Browsers cannot resolve container hostnames, so you need different values for bucket_endpoint (internal) and bucket_public_url (external).
{
"bucket_region": "us-east-1",
"bucket_media": "media",
"bucket_backup": "backups",
"bucket_endpoint": "minio:9000",
"bucket_access_key": "minioadmin",
"bucket_secret_key": "minioadmin",
"bucket_public_url": "http://localhost:9000",
"bucket_force_path_style": true
}
bucket_endpointpoints to the MinIO container hostname (minio:9000) for API calls.bucket_public_urlpoints to the externally reachable address (http://localhost:9000) so media URLs in API responses work in the browser.
Set Up DigitalOcean Spaces
- Create a Space in the DigitalOcean Console.
- Generate a Spaces access key under API > Spaces Keys.
{
"bucket_region": "nyc3",
"bucket_media": "my-cms-media",
"bucket_endpoint": "nyc3.digitaloceanspaces.com",
"bucket_access_key": "DO00EXAMPLE...",
"bucket_secret_key": "your-spaces-secret-key",
"bucket_public_url": "https://my-cms-media.nyc3.digitaloceanspaces.com",
"bucket_force_path_style": false
}
Set Up Backblaze B2
- Create a B2 bucket in the Backblaze Console.
- Create an application key with read/write access to the bucket.
{
"bucket_region": "us-west-004",
"bucket_media": "my-cms-media",
"bucket_endpoint": "s3.us-west-004.backblazeb2.com",
"bucket_access_key": "your-b2-key-id",
"bucket_secret_key": "your-b2-application-key",
"bucket_public_url": "https://my-cms-media.s3.us-west-004.backblazeb2.com",
"bucket_force_path_style": false
}
Set Up Cloudflare R2
- Create an R2 bucket in the Cloudflare Dashboard.
- Create an API token under R2 > Manage R2 API Tokens with Object Read & Write permissions.
{
"bucket_region": "auto",
"bucket_media": "my-cms-media",
"bucket_endpoint": "your-account-id.r2.cloudflarestorage.com",
"bucket_access_key": "your-r2-access-key",
"bucket_secret_key": "your-r2-secret-key",
"bucket_public_url": "https://media.example.com",
"bucket_force_path_style": true
}
Good to know: Cloudflare R2 does not charge egress fees. Set
bucket_public_urlto your R2 custom domain or the public bucket URL for serving media.
Understand bucket_public_url vs bucket_endpoint
These two fields serve different purposes:
bucket_endpointis the S3 API hostname that ModulaCMS uses to upload, download, and delete objects. This is where API calls go.bucket_public_urlis the base URL that appears in media URLs returned by the API. This is what browsers and frontends use to load images and files.
In most production setups, these are the same. They diverge in Docker environments where the S3 service has an internal hostname that browsers cannot reach.
Verify the Connection
After configuring S3, check the health endpoint to verify connectivity:
curl http://localhost:8080/api/v1/health
The storage check performs a HeadBucket call against your configured bucket. A true value confirms ModulaCMS can reach your storage provider.
{
"status": "ok",
"checks": {
"database": true,
"storage": true,
"plugins": true
}
}
Media Uploads
With S3 configured, upload files through the media API:
curl -X POST http://localhost:8080/api/v1/media \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-F "file=@/path/to/photo.jpg"
ModulaCMS uploads the original file and generates optimized image variants (WebP at configured dimension presets) automatically.
Next Steps
- Media management guide -- uploading, optimization, dimension presets
- Observability -- storage health checks and monitoring
- Configuration reference -- all config fields