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.
- Create the App Engine app in the region you want (the region cannot be changed later).
- Create a Cloud SQL for PostgreSQL instance, database, and user.
- Store the Django secret key and database password in Secret Manager.
- Add an
app.yamlwith the Python runtime, a Gunicorn entrypoint, and a static files handler. - Point Django at the Cloud SQL Unix socket under
/cloudsql/. - Run migrations from your machine or CI through the Cloud SQL Auth Proxy, then
gcloud app deploy. - 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
gunicornmust be inrequirements.txt; App Engine installs dependencies from it during the build.static_dirmakes App Engine serve collected static files directly, so Django never handles them.min_instances: 1avoids 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.
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
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/returns200or your expected redirect.- A static file URL returns
200with 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
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
- Compare with a container platform in Deploy Django on Google Cloud Run.
- Review database settings in How to Connect Django to PostgreSQL in Production.
- Organise credentials with Manage Django Secrets in Production.
- If the app returns errors after deploy, use Fix Django 500 Internal Server Error in Production.
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.