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.

Media on Cloud StorageUpload to DjangoFileField saveGCS backenddjango-storagesBucketuniform accessSigned URLfor private filesUploads go to a bucket; private files are served through short-lived signed URLs.
  1. pip install "django-storages[google]".
  2. Create a bucket with uniform bucket-level access in the same region as your app; keep public access prevention on for private media.
  3. Grant the runtime service account roles/storage.objectUser on the bucket (Workload Identity on GKE, the service identity on Cloud Run).
  4. Configure STORAGES["default"] with storages.backends.gcloud.GoogleCloudStorage and bucket_name.
  5. For private files, keep querystring_auth on so the backend generates signed URLs (needs signing permission without key files).
  6. Use unique upload_to paths 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"},
}
two ways to serve uploads from GCSPublic bucketallUsers objectViewer on the bucket✓stable URLs, CDN-friendly✕anyone with the URL can readPrivate + signed URLs✓public access prevention on✓URLs expire after minutes✓Django decides who gets a URLUse public buckets only for content that is truly publicdjango-deployment.com
Many apps use two buckets: one public for avatars, one private for documents.

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
GCS media setupUniform access, public prevention onIAM controls accessRuntime service account has objectUserno key filesSigned URLs via signBlobTokenCreator on itselfUnique upload_to pathsfile_overwrite offLifecycle and CORS rules settmp cleanup, direct uploadsIdentity-based access keeps credentials out of images and reposdjango-deployment.com
Keep the bucket in the same region as the app to avoid egress costs and latency.

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-Control metadata 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

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.

Keep going