Deploy Django on Azure Container Apps

Deploy a containerized Django app to Azure Container Apps with Azure Container Registry, PostgreSQL Flexible Server, secrets, a migration job, custom domains, and revisions.

Problem statement

Azure Container Apps runs containers with HTTPS ingress, autoscaling, revisions, and scale-to-zero, without operating a Kubernetes cluster. It is a good fit for a Django app that is already containerized and needs to live in Azure.

The common failures are predictable: the container listens on the wrong port, Django rejects the platform hostname, secrets end up as plain environment values, migrations run in every replica at startup, and static files 404 because nothing serves them. This guide builds a clean path from image to production with those pieces handled.

Django on Azure Container AppsContainer Registryversioned imagesIngressTLS, custom domainContainer AppGunicorn replicasPostgreSQLFlexible ServerMigrations run once as a Container Apps job, not in every replica's startup command.
  1. Build a production image (Gunicorn, WhiteNoise, non-root user) and push it to Azure Container Registry.
  2. Create PostgreSQL Flexible Server with a database and application user.
  3. Create a Container Apps environment and a container app with external ingress on port 8000.
  4. Store the secret key and database URL as Container Apps secrets and reference them with secretref:.
  5. Run migrations with a Container Apps job using the same image.
  6. Add a custom domain with a managed certificate and set ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.
  7. Deploy new versions as new revisions and verify before shifting traffic.

Step-by-step solution

1. Prepare the image

The image should start Gunicorn on a fixed port and serve static files itself, because Container Apps has no separate static file server:

FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
RUN useradd --create-home app
USER app
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-"]

Add WhiteNoise to MIDDLEWARE directly after SecurityMiddleware and use its compressed manifest storage.

2. Create the registry and push

az group create --name myapp-rg --location westeurope
az acr create --resource-group myapp-rg --name myappregistry --sku Basic
az acr build --registry myappregistry --image myapp:1.0.0 .

az acr build builds in Azure, which avoids pushing large layers from a laptop. Tag images with a version or commit SHA, not only latest, so every revision maps to an exact build.

3. Create the database

az postgres flexible-server create \
  --resource-group myapp-rg --name myapp-pg --location westeurope \
  --tier Burstable --sku-name Standard_B1ms --version 16 \
  --admin-user pgadmin --admin-password 'use-a-generated-password'
az postgres flexible-server db create --resource-group myapp-rg --server-name myapp-pg --database-name myapp

Connect as the admin once to create an application role that owns the myapp database, and use that role from Django. Flexible Server requires TLS by default, so include sslmode=require in the connection string.

For networking, the simplest secure option is private access through a virtual network shared with the Container Apps environment. Public access with firewall rules works but is harder to lock down, because Container Apps outbound IPs can be shared.

4. Create the environment and the app

az containerapp env create --name myapp-env --resource-group myapp-rg --location westeurope

az containerapp create \
  --name myapp-web --resource-group myapp-rg --environment myapp-env \
  --image myappregistry.azurecr.io/myapp:1.0.0 \
  --registry-server myappregistry.azurecr.io --registry-identity system \
  --target-port 8000 --ingress external \
  --min-replicas 1 --max-replicas 5 \
  --secrets django-secret-key='long-random-value' \
            database-url='postgres://myapp:pw@myapp-pg.postgres.database.azure.com:5432/myapp?sslmode=require' \
  --env-vars DJANGO_SETTINGS_MODULE=config.settings.production \
             DJANGO_SECRET_KEY=secretref:django-secret-key \
             DATABASE_URL=secretref:database-url \
             DJANGO_ALLOWED_HOSTS=example.com,.azurecontainerapps.io
environment, app, revisions, replicasContainer Apps environmentshared network and logsContainer app: webingress on port 8000, secretsRevisionan immutable image plus itssettingsReplicasscale rules, min replicasContainer Apps job: migratesame image, runs to completionEvery change to image or settings creates a new revisiondjango-deployment.com
Setting min replicas to 1 avoids a cold start on the first request after idle time.
  • --target-port must match the port Gunicorn binds.
  • secretref: injects secret values without writing them into the app's visible environment configuration.
  • --registry-identity system uses a managed identity to pull from the registry instead of an admin password.
  • --min-replicas 1 avoids cold starts; 0 saves money for low-traffic internal tools.

For larger setups, reference secrets stored in Azure Key Vault from Container Apps instead of pasting values on the command line.

5. Configure Django for the platform

Container Apps ingress terminates TLS and forwards HTTP to your container with X-Forwarded-Proto:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")

Add health probes that hit a lightweight endpoint (for example /healthz/) so a replica that cannot reach the database is taken out of rotation. If SECURE_SSL_REDIRECT is on, exempt the health path or make probes send X-Forwarded-Proto: https; otherwise probes receive 301 responses.

6. Run migrations as a job

Do not put migrate in the web container's start command: every replica would run it concurrently on every restart.

migrations as a job, not at start-up1Push the new imageto Azure Container Registry2Point the job at that imageaz containerapp job update3Start the job and waitmanage.py migrate --noinput4Check the execution succeededjob execution list, logs5Update the web appnew revision takes trafficSeveral replicas starting at once must never race to migratedjango-deployment.com
The job shares the app secrets and database access, so it runs exactly like the web container.
az containerapp job create \
  --name myapp-migrate --resource-group myapp-rg --environment myapp-env \
  --trigger-type Manual --replica-timeout 600 --replica-retry-limit 0 \
  --image myappregistry.azurecr.io/myapp:1.0.0 \
  --registry-server myappregistry.azurecr.io --registry-identity system \
  --secrets database-url='postgres://...' django-secret-key='...' \
  --env-vars DATABASE_URL=secretref:database-url DJANGO_SECRET_KEY=secretref:django-secret-key \
             DJANGO_SETTINGS_MODULE=config.settings.production \
  --command "python" --args "manage.py" "migrate" "--noinput"

az containerapp job start --name myapp-migrate --resource-group myapp-rg
az containerapp job execution list --name myapp-migrate --resource-group myapp-rg -o table

For each release: update the job's image, start it, wait for success, then update the web app.

7. Add a custom domain

az containerapp hostname add --hostname example.com --name myapp-web --resource-group myapp-rg
az containerapp hostname bind --hostname example.com --name myapp-web --resource-group myapp-rg \
  --environment myapp-env --validation-method CNAME

Create the DNS records the CLI asks for (a TXT verification record plus a CNAME, or an A record for an apex domain) before binding. The managed certificate is issued and renewed by Azure.

8. Deploy new versions

az acr build --registry myappregistry --image myapp:1.1.0 .
az containerapp job update --name myapp-migrate --resource-group myapp-rg --image myappregistry.azurecr.io/myapp:1.1.0
az containerapp job start --name myapp-migrate --resource-group myapp-rg
az containerapp update --name myapp-web --resource-group myapp-rg --image myappregistry.azurecr.io/myapp:1.1.0

Verification check:

az containerapp revision list --name myapp-web --resource-group myapp-rg -o table
az containerapp logs show --name myapp-web --resource-group myapp-rg --follow
curl -sI https://example.com/ | head -1
how a new version takes trafficSingle revision modenew revision replaces the old✓simple: one version liverollback: redeploy the previous imageMultiple revision modesplit traffic by percentage✓test a revision on its own URL✓rollback: move traffic backBoth versions share one database, so migrations must suit bothdjango-deployment.com
Multiple revision mode is a built-in canary release.

Explanation

Container Apps runs your image as replicas behind a managed ingress. Each update that changes the template creates a revision. In single revision mode, the new revision replaces the old one once it is healthy; in multiple revision mode, you control traffic weights between revisions, which enables canary releases. Migrations run separately so the schema change happens exactly once, before new code receives traffic.

Edge cases / notes

Rollback

In multiple revision mode, shift traffic back to the previous revision:

az containerapp ingress traffic set --name myapp-web --resource-group myapp-rg \
  --revision-weight myapp-web--previous=100

This rolls back code only. Keep migrations backward compatible so the previous revision still runs against the migrated schema.

Media files

Replica filesystems are ephemeral. Store uploads in Azure Blob Storage with django-storages (storages.backends.azure_storage.AzureStorage), preferably authenticated with the app's managed identity.

Celery workers

Run workers as a separate container app without ingress, using the same image and a different command. Scale rules can react to queue length.

When to automate this

The release sequence (build, migrate job, update, verify) is the same every time and fits in one CI workflow. The environment, database, registry, and identity grants are good candidates for Bicep or Terraform so a second environment is a parameter change.

Next steps

FAQ

Should I use Azure Container Apps or App Service for Django?

Container Apps suits teams that already build container images and want revisions, scale rules, and jobs. App Service suits simple code deploys with less container tooling.

Why does my Container App return a 404 or connection error on the default URL?

Usually the ingress target port does not match the port Gunicorn listens on, or ingress is internal. Check --target-port and the container logs.

Where do I run Django migrations on Container Apps?

Use a manual Container Apps job running the same image as the web app, triggered once per release before updating the web app.

Can Container Apps scale Django to zero?

Yes, with --min-replicas 0. The first request after idle waits for a replica to start, so use at least one replica for user-facing production traffic.

Keep going