Django Docker Production Checklist
Review a Dockerized Django deployment before production: image build, non-root user, secrets, health checks, volumes, logging, resource limits, and release steps.
Problem statement
Docker makes a Django app easy to run anywhere, and just as easy to run insecurely or unreliably. A container that works on a laptop can still ship with a root user, secrets baked into image layers, the development server as its command, migrations that run in every replica, logs written to files inside the container, and a database volume that disappears with docker compose down -v.
This checklist reviews a Dockerized Django deployment from image build to runtime operations. It applies to Docker Compose on a server and translates directly to Kubernetes, ECS, and other orchestrators.
Quick answer
- Image: slim, pinned base image; multi-stage build;
.dockerignore; dependencies pinned; static files collected at build. - Runtime process: Gunicorn (or an ASGI server) in the foreground as PID 1 via
exec; neverrunserver. - User: non-root user; read-only root filesystem where possible.
- Configuration: all settings and secrets from environment variables or secret mounts, never baked into images.
- Health: container health check and orchestrator readiness checks.
- Data: database and media on persistent volumes or managed services; backups outside the host.
- Networking: only the reverse proxy publishes ports; app and database containers stay internal.
- Operations: logs to stdout/stderr with rotation, resource limits, restart policies, migrations as a separate release step, immutable image tags.
Checklist
1. Base image and build
What to check in the Dockerfile:
- base image pinned to a specific version (for example
python:3.13-slim-bookworm, or a digest) - multi-stage build if compilers are needed, so they stay out of the runtime image
- dependencies installed from a lockfile or fully pinned requirements before copying code, for layer caching
.dockerignoreexcludes.git,.env, virtualenvs,node_modules, media, and local databasescollectstaticruns during the build with a throwaway key
FROM python:3.13-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN DJANGO_SECRET_KEY=build-only python manage.py collectstatic --noinput
RUN useradd --uid 10001 --create-home app && chown -R app:app /app/staticfiles
USER app
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-"]
PYTHONUNBUFFERED=1 ensures logs appear immediately in docker logs.
2. The main process
What to check:
CMDruns Gunicorn, Uvicorn, or Daphne, notpython manage.py runserver- entrypoint scripts end with
exec "$@"so the server becomes PID 1 and receives stop signals - no
--daemonflags or background process managers inside the container - worker count sized for the container's CPU and memory limits, not the host's
Verify:
docker compose exec web ps -o pid,user,args
PID 1 should be the Gunicorn master, running as the non-root user.
3. Non-root and filesystem
What to check:
docker compose exec web id
docker inspect --format '{{.Config.User}}' myapp-web-1
Guidelines:
- a dedicated UID (for example 10001), not root
- code files not writable by that user where practical
read_only: truefor the root filesystem in Compose, withtmpfsfor/tmpand volumes for directories that need writes
services:
web:
read_only: true
tmpfs:
- /tmp
- no
privileged: true; drop capabilities the app does not need (cap_drop: [ALL]works for most Django containers)
4. Configuration and secrets
What to check:
docker history --no-trunc myorg/myapp:1.4.2 | grep -i -E "secret|password|key" || echo "no obvious secrets in history"
docker compose config | grep -i -E "SECRET|PASSWORD" | head
Guidelines:
- no secrets in
ENVorARGinstructions; build arguments can persist in image metadata - runtime secrets from
env_filewith restrictive permissions, Docker or Compose secrets mounted as files, or an orchestrator's secret store .envfiles excluded from the build context and from version control- the Compose
.envfile (used for variable substitution) not confused with application environment files
5. Health checks
services:
web:
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz/', timeout=3)"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
- the health endpoint handles requests to
127.0.0.1withoutDisallowedHosterrors (answer it before host validation, or allow that host for the health path only) - dependencies wait for health:
depends_onwithcondition: service_healthyfor the database - orchestrators use readiness checks to avoid routing traffic to starting containers
Using Python for the check avoids installing curl in slim images.
6. Database and persistent data
What to check:
docker volume ls
docker compose config | sed -n '/volumes:/,$p'
Guidelines:
- PostgreSQL data on a named volume or, better for production, a managed database
- media files in object storage or on a persistent volume, never in the container filesystem
- database container not publishing port 5432 on public interfaces
- backups copied off the host; a volume on the same disk is not a backup
- no
docker compose down -vin deploy scripts
7. Networking
Guidelines:
- only the reverse proxy (Nginx, Traefik, Caddy) publishes ports 80 and 443
- the Django container is reachable only on the internal network
- published ports bound to
127.0.0.1when a host-level proxy is used, because Docker's port publishing bypasses host firewalls such as ufw
services:
web:
ports:
- "127.0.0.1:8000:8000"
ALLOWED_HOSTS,CSRF_TRUSTED_ORIGINS, andSECURE_PROXY_SSL_HEADERconfigured for the proxy in front
8. Logging
What to check:
docker info --format '{{.LoggingDriver}}'
cat /etc/docker/daemon.json 2>/dev/null
Guidelines:
- Django, Gunicorn, and Celery log to stdout and stderr
- log rotation configured for the
json-filedriver, or logs shipped to a central system
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "5" }
}
Without rotation, container logs grow until the disk fills.
9. Resources and restarts
services:
web:
restart: unless-stopped
deploy:
resources:
limits:
cpus: "2"
memory: 1g
Guidelines:
- memory limits sized from measured usage per worker
- Gunicorn worker counts match the container limits
- restart policy for crash recovery, plus monitoring of restart counts so crash loops are noticed
Docker Compose applies deploy.resources.limits when running with current Compose versions; verify with docker stats.
10. Release process
Guidelines:
- images tagged with immutable identifiers (Git SHA or version), not only
latest - migrations run once per release as a one-off container or job before replacing web containers:
docker compose run --rm web python manage.py migrate --noinput
docker compose up -d --no-deps web
- rollback documented: deploy the previous tag; migrations kept backward compatible
- Celery workers and Beat updated with the same image tag as web containers
- images scanned for vulnerabilities and rebuilt regularly for base image updates
11. Background workers
Guidelines:
- Celery worker and Beat run as separate services from the same image with different commands
- exactly one Beat container
- workers have the same environment variables and secrets as the web service
Verify
docker compose ps
docker compose exec web python manage.py check --deploy
docker inspect --format '{{.State.Health.Status}}' myapp-web-1
docker stats --no-stream
Verification check:
- containers run as non-root with health status
healthy - no secrets appear in image history or layers
- only the proxy publishes ports publicly
- database and media survive
docker compose downandup - logs rotate and appear in
docker compose logs - a deploy of a new tag and a rollback to the previous tag both work
Explanation
Containers package the application consistently, but production qualities (least privilege, secret handling, persistence, observability, and safe releases) still need explicit configuration. Most issues found in Dockerized Django deployments are defaults carried over from development: root users, the development server, secrets in images, missing log rotation, and migrations tied to container startup. Checking each area turns a working container into a production-ready one.
Edge cases / notes
Rootless Docker and Podman
Rootless container engines add another security layer and change UID mappings for bind mounts. Test file permissions for volumes when switching.
Single-host Compose versus orchestrators
Compose on one server is simple and effective for many sites, but a host failure takes everything down. Plan backups and a documented rebuild path, or move to multiple hosts when uptime requirements grow.
Image size versus debuggability
Minimal images without shells are harder to debug. Keep a debug approach ready (for example running a debug container sharing the network and process namespace) before removing shells.
Common mistakes
- Running
runserverin production containers. - Baking
.envfiles or secret build arguments into images. - Publishing PostgreSQL or Gunicorn ports on all interfaces.
- Running migrations in the web container's startup with several replicas.
Next steps
- Build the image step by step with How to Dockerize a Django App for Production.
- Run the full stack with Deploy Django with Docker Compose in Production.
- Reduce build time and size with Build Smaller, Faster Django Docker Images.
- Troubleshoot startup failures with Fix a Django Docker Container That Exits Immediately.
FAQ
Should a Django Docker container run as root?
No. Create a dedicated user in the Dockerfile and switch to it with USER. Root inside a container increases the impact of vulnerabilities and container escapes.
How should secrets be passed to Django containers?
At runtime, through environment files with restricted permissions, mounted secret files, or an orchestrator's secret store. Never with ENV or ARG in the Dockerfile.
Where should migrations run in a Docker deployment?
In a one-off container or job using the new image, once per release, before replacing the web containers. Avoid running them in every web container's startup.
Do I need log rotation for Docker containers?
Yes, when using the default json-file logging driver. Configure max-size and max-file, or ship logs to a central logging service.