Store Django Media Files on Google Cloud Storage
Store Django user uploads on Google Cloud Storage with django-storages: create a bucket with uniform access, authenticate with Workload Identity or the runtime service account, serve private files with signed URLs, and set CORS and lifecycle rules.
Problem statement
On Google Cloud, Django apps running on Cloud Run, GKE, or App Engine have no persistent local disk for uploads: files written to the container disappear on the next deploy or scale event. Google Cloud Storage (GCS) is the natural home for media files. django-storages provides a GCS backend, but you still need to decide on bucket access (public or private with signed URLs), authenticate without key files, and avoid filename collisions and missing CORS rules for direct uploads.
pip install "django-storages[google]".- Create a bucket with uniform bucket-level access in the same region as your app; keep public access prevention on for private media.
- Grant the runtime service account
roles/storage.objectUseron the bucket (Workload Identity on GKE, the service identity on Cloud Run). - Configure
STORAGES["default"]withstorages.backends.gcloud.GoogleCloudStorageandbucket_name. - For private files, keep
querystring_authon so the backend generates signed URLs (needs signing permission without key files). - Use unique
upload_topaths and set lifecycle rules for temporary files.
Step-by-step solution
1. Create the bucket
gcloud storage buckets create gs://myapp-media --location=europe-west1 \
--uniform-bucket-level-access --public-access-prevention
Uniform access means permissions are controlled by IAM on the bucket, not per-object ACLs, which django-storages must then not try to set (default_acl left as None).
2. Grant access to the runtime identity
gcloud storage buckets add-iam-policy-binding gs://myapp-media \
--member=serviceAccount:django-run@PROJECT.iam.gserviceaccount.com --role=roles/storage.objectUser
On Cloud Run, that is the service's service account; on GKE, the Google service account bound through Workload Identity. No JSON key files are needed: the client library finds credentials automatically.
3. Configure Django
STORAGES = {
"default": {
"BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
"OPTIONS": {
"bucket_name": "myapp-media",
"querystring_auth": True, # signed URLs for private objects
"expiration": 300, # seconds
"file_overwrite": False,
"default_acl": None, # uniform bucket-level access
},
},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
4. Signing URLs without key files
Signed URLs need a signature from a service account. With key files, the library signs locally; without them (the recommended setup), it calls the IAM signBlob API. Grant the service account roles/iam.serviceAccountTokenCreator on itself so it can sign, and set the signing account if required:
STORAGES["default"]["OPTIONS"]["iam_sign_blob"] = True
STORAGES["default"]["OPTIONS"]["sa_email"] = "django-run@PROJECT.iam.gserviceaccount.com"
Option names have changed across django-storages releases; check the GCS backend docs for your version.
5. Avoid collisions and set lifecycle rules
Generate unique keys in upload_to (a UUID directory) so uploads with the same name never overwrite each other. Add lifecycle rules to delete temporary uploads (for example a tmp/ prefix after a day) and, if you enable object versioning, noncurrent versions after 30 days.
6. Direct browser uploads
For large files, let the browser upload directly with a signed URL for PUT, and configure CORS on the bucket for your origin:
[{ "origin": ["https://example.com"], "method": ["PUT", "GET"], "responseHeader": ["Content-Type"], "maxAgeSeconds": 3600 }]
gcloud storage buckets update gs://myapp-media --cors-file=cors.json
Verification check: upload a file through the admin, confirm it appears in the bucket under a unique path, open its URL from the page (a signed URL with an expiry for private buckets), and confirm the same URL stops working after the expiration.
Explanation
django-storages implements Django's Storage API on top of the google-cloud-storage client, so FileField and ImageField read and write objects in a bucket instead of the local filesystem. On Google Cloud runtimes, application default credentials come from the attached service account through the metadata server, so authentication needs only an IAM role. Signed URLs encode a time-limited permission, signed by a service account, which lets private objects be served directly from GCS without proxying file bytes through Django.
Edge cases / notes
- Static files can also live in GCS, but WhiteNoise or a CDN in front of a public bucket is simpler.
- Large uploads through Django consume worker time and memory; use direct signed uploads for big files.
- Content-Type and caching. Set
Cache-Controlmetadata for public, immutable files to benefit from CDN caching. - Local development can use the filesystem storage or a GCS emulator; keep the backend switch in settings.
Next steps
- The AWS equivalent is Store Django Media Files on S3.
- Validate uploads with the Django File Upload Security Checklist.
FAQ
How does Django authenticate to Google Cloud Storage?
Through the runtime's service account (Cloud Run, GKE Workload Identity), found automatically by the client library.
Do I need a service account key file?
No. Use attached service accounts and IAM signBlob for signed URLs.
Should my media bucket be public?
Only for truly public content. Keep private files in a private bucket and serve signed URLs.
Why do uploads overwrite each other?
Use unique paths in upload_to and set file_overwrite to False.