Deploy Django on Google App Engine

Deploy Django to the Google App Engine standard environment with app.yaml, Gunicorn, Cloud SQL for PostgreSQL, Secret Manager, static handlers, and Cloud Storage media.

Problem statement

Google App Engine's standard environment runs Python web apps without servers to manage: you upload source code, Google builds it, and instances scale up and down with traffic, including to zero. For a Django app that is attractive, but the platform has constraints that break a default Django project: the filesystem is read-only except /tmp, there is no place for uploaded media, the database is reached through a Unix socket, and management commands cannot be run on an instance.

This guide deploys a Django app to App Engine standard with PostgreSQL on Cloud SQL, secrets from Secret Manager, static files served by App Engine, and media on Cloud Storage.

Django on App Engine standardgcloud app deployuploads sourceApp EngineGunicorn instancesCloud SQLUnix socketCloud Storageuploaded mediaInstances are disposable: keep state in Cloud SQL and Cloud Storage, never on local disk.
  1. Create the App Engine app in the region you want (the region cannot be changed later).
  2. Create a Cloud SQL for PostgreSQL instance, database, and user.
  3. Store the Django secret key and database password in Secret Manager.
  4. Add an app.yaml with the Python runtime, a Gunicorn entrypoint, and a static files handler.
  5. Point Django at the Cloud SQL Unix socket under /cloudsql/.
  6. Run migrations from your machine or CI through the Cloud SQL Auth Proxy, then gcloud app deploy.
  7. Store uploads in Cloud Storage with django-storages.

Step-by-step solution

1. Create the project resources

gcloud config set project my-project
gcloud services enable appengine.googleapis.com sqladmin.googleapis.com secretmanager.googleapis.com
gcloud app create --region=europe-west3

Create the database. Choose a tier that matches your load; the smallest shared-core tiers are fine for testing but not for busy production apps.

gcloud sql instances create myapp-db \
  --database-version=POSTGRES_16 --region=europe-west3 --tier=db-custom-1-3840
gcloud sql databases create myapp --instance=myapp-db
gcloud sql users create myapp --instance=myapp-db --password='use-a-generated-password'

Grant the App Engine default service account (PROJECT_ID@appspot.gserviceaccount.com) the Cloud SQL Client and Secret Manager Secret Accessor roles.

2. Store secrets

printf '%s' "$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')" | \
  gcloud secrets create django-secret-key --data-file=-
printf '%s' 'use-a-generated-password' | gcloud secrets create db-password --data-file=-

Read them at startup with the client library (google-cloud-secret-manager):

# config/settings/production.py
import os
from google.cloud import secretmanager

PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
_client = secretmanager.SecretManagerServiceClient()


def secret(name):
    path = f"projects/{PROJECT_ID}/secrets/{name}/versions/latest"
    return _client.access_secret_version(name=path).payload.data.decode()


SECRET_KEY = secret("django-secret-key")
DEBUG = False
ALLOWED_HOSTS = [".appspot.com", "example.com", "www.example.com"]

GOOGLE_CLOUD_PROJECT is set automatically on App Engine. Pinning latest is convenient; pin a specific version if you want secret changes to require a deploy.

3. Configure the database connection

On App Engine standard, Cloud SQL instances in the same project are reachable through a Unix socket at /cloudsql/PROJECT:REGION:INSTANCE:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "myapp",
        "USER": "myapp",
        "PASSWORD": secret("db-password"),
        "HOST": "/cloudsql/my-project:europe-west3:myapp-db",
        "CONN_MAX_AGE": 60,
    }
}

Instances scale out automatically. Each instance opens its own connections, so cap max_instances in app.yaml to keep the total under the database's connection limit.

4. Write app.yaml

runtime: python312
entrypoint: gunicorn -b :$PORT --workers 2 --threads 4 --timeout 60 config.wsgi:application
instance_class: F2

env_variables:
  DJANGO_SETTINGS_MODULE: "config.settings.production"

automatic_scaling:
  min_instances: 1
  max_instances: 5

handlers:
  - url: /static
    static_dir: staticfiles/
    secure: always
  - url: /.*
    script: auto
    secure: always
  • gunicorn must be in requirements.txt; App Engine installs dependencies from it during the build.
  • static_dir makes App Engine serve collected static files directly, so Django never handles them.
  • min_instances: 1 avoids cold starts on the first request after idle periods, at the cost of one always-on instance.
  • Check the list of supported Python runtimes in the App Engine documentation and use the newest one your dependencies support.
app.yaml, line by lineruntime: python312entrypoint: gunicorn -b :$PORT--workers 2 --threads 4 …automatic_scaling:min_instances:1- url: /staticstatic_dir:staticfiles/secure:always12341App Engine sets the port; never hard-code 8000.2One warm instance avoids cold starts, and costs money.3Served by App Engine, not Django: collectstatic before deploy.4Redirects HTTP to HTTPS for this handler.Secrets stay out of app.yaml; it is uploaded with the codedjango-deployment.com
env_variables in app.yaml are for non-secret values only; secrets come from Secret Manager.

Add a .gcloudignore so the virtual environment, .git, and local media are not uploaded:

.git/
venv/
.venv/
__pycache__/
media/
*.sqlite3

5. Collect static files before deploying

App Engine serves the staticfiles/ directory from your upload, so run collectstatic locally or in CI before deploying:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput

A small build settings module that does not read Secret Manager keeps this step independent of cloud credentials.

6. Run migrations through the Cloud SQL Auth Proxy

Migrations cannot be run on an App Engine instance. Run them from CI or your workstation:

./cloud-sql-proxy my-project:europe-west3:myapp-db --port 5432 &
DATABASE_HOST=127.0.0.1 python manage.py migrate --noinput
two ways into Cloud SQLApp Engine in productionUnix socket /cloudsql/PROJECT:REGION:DB✓connection built in, no proxyservice account needs Cloud SQL ClientMigrations from your machinecloud-sql-proxy on 127.0.0.1:5432✓IAM-authenticated, encryptedsame settings, different HOSTNever open the database to 0.0.0.0/0 just to run migratedjango-deployment.com
Settings usually switch on an environment variable to choose the socket or the local proxy.

Make the settings accept an override for local proxy use (for example HOST = os.environ.get("DATABASE_HOST", "/cloudsql/...")). Run migrations before deploying code that depends on them, and keep migrations backward compatible so the old version keeps working until traffic switches.

7. Store media in Cloud Storage

pip install "django-storages[google]"
gcloud storage buckets create gs://myapp-media --location=europe-west3 --uniform-bucket-level-access
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
        "OPTIONS": {"bucket_name": "myapp-media"},
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

Grant the service account object permissions on the bucket. For private files, keep the bucket private and let django-storages generate signed URLs.

8. Deploy and verify

gcloud app deploy app.yaml
gcloud app browse
gcloud app logs tail -s default

Map a custom domain with gcloud app domain-mappings create example.com; App Engine provisions managed certificates for mapped domains.

Verification check:

  • curl -sI https://PROJECT_ID.REGION_ID.r.appspot.com/ returns 200 or your expected redirect.
  • A static file URL returns 200 with a long cache header.
  • The admin login works (confirms database, sessions, and CSRF over HTTPS).
  • A test upload lands in the bucket.

Explanation

App Engine standard runs your code in sandboxed instances that can be created and destroyed at any moment. Everything that must survive (data, uploads, secrets) lives in managed services, and everything that is built (dependencies, static files) is produced before or during deployment. Gunicorn still serves Django; App Engine sits in front of it, routes requests, serves static files, and terminates TLS.

Because App Engine terminates HTTPS and forwards requests over HTTP, set SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") so Django recognises secure requests. secure: always in the handlers already redirects HTTP to HTTPS at the platform level.

Edge cases / notes

Versions and rollback

Each deploy creates a new version. Roll back by routing traffic to a previous version:

gcloud app versions list
gcloud app services set-traffic default --splits=PREVIOUS_VERSION=1
deploy first, promote second1gcloud app deploy --no-promotenew version, no traffic2Test the version URLVERSION-dot-PROJECT.appspot.com3Run compatible migrationsold version still serving4Migrate trafficgcloud app services set-traffic5Keep the old versionrollback is a traffic switchDelete old versions later, not during the rolloutdjango-deployment.com
Idle old versions cost nothing on automatic scaling with min_instances 0.

Old versions accumulate; delete ones you no longer need. If the new version ran migrations that the old one cannot handle, rolling back code is not enough, so keep migrations additive.

Background work and scheduled jobs

App Engine standard is request-driven. Use Cloud Tasks for background work (delivered as HTTP requests to a handler) and Cloud Scheduler for periodic jobs. Protect those handlers so only the platform can call them.

Request timeouts

Automatic scaling enforces a request deadline, so long reports or imports must move to background processing.

When to automate this

Put collectstatic, the proxy-backed migrate, and gcloud app deploy in one CI job so every deploy follows the same order. The secret setup and IAM grants are one-time steps that belong in Terraform or a documented bootstrap script.

Next steps

FAQ

Should I use App Engine standard or Cloud Run for Django?

Cloud Run gives you a container with full control over system packages and is usually the better default for new projects. App Engine standard suits apps that fit its runtime and benefit from source-based deploys with built-in static file handlers and version traffic splitting.

Why can't Django write files on App Engine?

The standard environment filesystem is read-only except /tmp, which is instance-local and disappears with the instance. Store uploads in Cloud Storage.

How do I run manage.py commands on App Engine?

Run them from your machine or CI connected to Cloud SQL through the Cloud SQL Auth Proxy, with production settings. Scheduled commands should be exposed as protected handlers triggered by Cloud Scheduler.

Why do I get too many database connections on App Engine?

Each instance keeps its own connections, and automatic scaling can start many instances. Lower max_instances, reduce workers per instance, or add a connection pooler.

Keep going