Store Django Media Files on DigitalOcean Spaces
Store Django uploads on DigitalOcean Spaces with django-storages’ S3 backend: set the regional endpoint, create scoped access keys, choose private files with presigned URLs or public files behind the Spaces CDN, and configure CORS.
Problem statement
Django apps on DigitalOcean App Platform have ephemeral filesystems, and apps on Droplets lose uploads when the Droplet is rebuilt or scaled out to several machines. DigitalOcean Spaces is S3-compatible object storage with a built-in CDN, priced simply. Because it speaks the S3 API, django-storages' S3 backend works with a few settings: the regional endpoint URL, access keys, and choices about public versus private files.
- Create a Space in the region closest to your app, and an access key (preferably scoped to that bucket).
pip install "django-storages[s3]".- Configure
S3Storagewithendpoint_url="https://<region>.digitaloceanspaces.com",bucket_name,region_name, and the keys from environment variables. - Keep
default_acl="private"andquerystring_auth=Truefor private uploads; use a separate public Space with the CDN for public assets. - Set CORS on the Space if browsers upload directly.
- Use unique
upload_topaths so files never overwrite each other.
Step-by-step solution
1. Create the Space and keys
In the DigitalOcean control panel, create a Space (for example myapp-media in fra1), leave file listing restricted, and enable the CDN only for the public Space. Create an access key under API → Spaces Keys; newer accounts can scope keys to specific buckets with read/write permissions, which limits damage if the key leaks.
2. Configure django-storages
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-media",
"endpoint_url": "https://fra1.digitaloceanspaces.com",
"region_name": "fra1",
"access_key": env("SPACES_KEY"),
"secret_key": env("SPACES_SECRET"),
"default_acl": "private",
"querystring_auth": True,
"querystring_expire": 600,
"file_overwrite": False,
"object_parameters": {"CacheControl": "max-age=86400"},
},
},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
The endpoint is the region URL, not the bucket URL; the backend adds the bucket. With querystring_auth, file.url returns a presigned URL valid for 10 minutes.
3. Public assets through the CDN
For public files, add a second storage:
STORAGES["public"] = {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-public",
"endpoint_url": "https://fra1.digitaloceanspaces.com",
"access_key": env("SPACES_KEY"), "secret_key": env("SPACES_SECRET"),
"default_acl": "public-read", "querystring_auth": False,
"custom_domain": "cdn.example.com",
},
}
from django.core.files.storage import storages
avatar = models.ImageField(upload_to=avatar_path, storage=lambda: storages["public"])
Point a CNAME at the Spaces CDN endpoint and add the custom domain with a certificate in the Spaces settings.
4. CORS for direct uploads
If the browser uploads directly with presigned POST or PUT URLs, add a CORS rule on the Space allowing your origin and the PUT/POST methods with the needed headers.
5. Keys and environment
Store keys as App Platform encrypted environment variables or in your Droplet's environment file. Rotate them if they appear in logs or a repository.
Verification check: upload through the admin and confirm the object appears in the Space; the page link contains X-Amz-Signature and expires; public assets load from cdn.example.com with a cache header.
Explanation
Spaces implements the S3 API, including signature version 4 authentication, presigned URLs, ACLs, and multipart uploads, so S3 clients such as boto3 work by pointing them at a different endpoint. django-storages' S3 backend therefore covers Spaces without a separate implementation. The Spaces CDN caches public objects at edge locations; private objects stay behind presigned URLs that Django issues after checking permissions.
Edge cases / notes
- Bucket names are global per region endpoint and must be DNS-compatible.
- Per-object ACLs default to private; make sure the backend does not set
public-readon private uploads. - Listing performance on very large Spaces is slow; avoid code that lists directories at request time.
- Backups. Spaces does not version objects by default in all regions; copy important media elsewhere with rclone.
Next steps
- Host the app itself with Deploy Django on DigitalOcean App Platform.
- Fix collisions with Fix django-storages Overwriting Files With the Same Name.
FAQ
Does django-storages support DigitalOcean Spaces?
Yes, through the S3 backend with the Spaces regional endpoint URL.
What endpoint URL should I use?
The region endpoint such as https://fra1.digitaloceanspaces.com, not the bucket subdomain.
How do I serve private files from Spaces?
Keep objects private and let django-storages generate presigned URLs.
Can I use the Spaces CDN with Django?
Yes, for public files: enable the CDN on a public Space and set custom_domain.