Store Django Media on Hetzner Object Storage
Use Hetzner Object Storage for Django media and static files with django-storages: create a bucket and S3 credentials, set the regional endpoint and addressing style, choose public or private access with presigned URLs, configure CORS, and keep costs low next to Hetzner Cloud servers.
Problem statement
Your Django app runs on Hetzner Cloud, and uploads are on the server's disk, which complicates scaling to a second server and backups. Hetzner Object Storage provides S3-compatible buckets in the same locations as your servers, so django-storages can store media there with a few settings. The details that trip people up are the endpoint and region values, addressing style, ACL support, and CORS for direct uploads.
- Create a bucket in the same location as your servers (for example
fsn1), and generate S3 credentials in the Cloud Console. - Set
endpoint_urlto the location's endpoint, such ashttps://fsn1.your-objectstorage.com, andregion_nameto the location. - Use virtual-hosted addressing and
signature_version = "s3v4". - For private media, keep
querystring_auth = True(presigned URLs); for public assets, use a public bucket policy. - Configure CORS on the bucket if browsers upload directly.
Step-by-step solution
1. Create the bucket and credentials
In the Hetzner Cloud Console, open Object Storage, create a bucket in the location where your servers run, and create S3 credentials (an access key and secret). Store them as environment variables on the servers. Keeping bucket and servers in the same location avoids latency and cross-location traffic.
2. Configure django-storages
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "shop-media",
"endpoint_url": "https://fsn1.your-objectstorage.com",
"region_name": "fsn1",
"access_key": os.environ["HETZNER_S3_ACCESS_KEY"],
"secret_key": os.environ["HETZNER_S3_SECRET_KEY"],
"signature_version": "s3v4",
"addressing_style": "virtual",
"default_acl": None,
"querystring_auth": True,
"file_overwrite": False,
},
},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
Check Hetzner's documentation for the exact endpoint host names of each location. With these settings, instance.file.url returns a presigned URL valid for an hour by default.
3. Public files
For assets that should be publicly readable, such as product images, use a separate bucket or prefix with a bucket policy allowing s3:GetObject, and set querystring_auth = False and a custom_domain if you front it with a CDN. Avoid relying on object ACLs; policies are clearer and portable.
4. Direct browser uploads
For large files, generate presigned POST or PUT URLs in Django and let the browser upload directly. The bucket needs a CORS configuration that allows your site's origin:
{
"CORSRules": [{
"AllowedOrigins": ["https://shop.example.com"],
"AllowedMethods": ["PUT", "POST", "GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}]
}
Apply it with any S3 tool, for example aws s3api put-bucket-cors --endpoint-url https://fsn1.your-objectstorage.com ....
5. Migrate existing files
Copy existing uploads from the server disk to the bucket with rclone or aws s3 sync using the Hetzner endpoint, keeping the same relative paths so database file names stay valid:
aws s3 sync /srv/app/media s3://shop-media --endpoint-url https://fsn1.your-objectstorage.com
Then switch the storage setting and deploy.
Explanation
Hetzner Object Storage implements the S3 API, so boto3 and django-storages work by pointing at its endpoint. Signature Version 4 signatures include the region, which must match what the service expects for the bucket's location. Moving media off the server makes web servers stateless, which means you can add servers behind a load balancer or rebuild a server without losing uploads.
Edge cases / notes
- Feature differences. Not every AWS S3 feature exists (for example some lifecycle or event options); check Hetzner's supported feature list before relying on one.
- Backups. Object storage is durable but not a backup against deletion; enable versioning where available or copy to a second provider.
- Static files. WhiteNoise on the servers is usually simpler than putting static files in the bucket.
Next steps
- Run the app servers with Deploy Django on Hetzner Cloud.
- General S3 patterns are in Store Django Media Files on S3.
- Fix signature errors with Fix S3 SignatureDoesNotMatch in Django.
FAQ
Does django-storages work with Hetzner Object Storage?
Yes, with the S3 backend and Hetzner's endpoint and region.
Should media be public or private?
Private with presigned URLs for user uploads; public with a bucket policy for assets meant for everyone.
Why do I get SignatureDoesNotMatch?
Usually the region or endpoint does not match the bucket's location, or the signature version is not s3v4.
Can I keep using the same file paths?
Yes. Sync files with the same relative paths and the database values stay valid.