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.
- Build a production image (Gunicorn, WhiteNoise, non-root user) and push it to Azure Container Registry.
- Create PostgreSQL Flexible Server with a database and application user.
- Create a Container Apps environment and a container app with external ingress on port 8000.
- Store the secret key and database URL as Container Apps secrets and reference them with
secretref:. - Run migrations with a Container Apps job using the same image.
- Add a custom domain with a managed certificate and set
ALLOWED_HOSTSandCSRF_TRUSTED_ORIGINS. - 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
--target-portmust match the port Gunicorn binds.secretref:injects secret values without writing them into the app's visible environment configuration.--registry-identity systemuses a managed identity to pull from the registry instead of an admin password.--min-replicas 1avoids cold starts;0saves 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.
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
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
- Build a solid image first with How to Dockerize a Django App for Production.
- Compare with the code-based option in Deploy Django on Azure App Service.
- Serve static files from the container with How to Serve Django Static Files with WhiteNoise.
- If replicas keep restarting, see Fix Load Balancer Health Checks Failing for Django.
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.