Deploy Django with Docker Swarm
Deploy Django with Docker Swarm: a stack file with replicas, Docker secrets, rolling start-first updates with automatic rollback, one-off migrations, and Nginx in front.
Problem statement
Docker Compose on one server is a solid Django deployment until you need zero-downtime updates, several replicas, or a second machine. Kubernetes solves those problems but brings a lot of operational weight. Docker Swarm sits in between: it uses almost the same file format as Compose and adds replicas, rolling updates with rollback, secrets, and multi-node scheduling, all built into Docker.
This guide converts a Django Compose setup into a Swarm stack with the pieces that matter in production: secrets instead of plain environment values, start-first rolling updates gated on health checks, migrations run exactly once, and persistent data pinned to the right node.
docker swarm initon the manager; join workers if you have more than one node.- Create secrets with
docker secret createand read them from/run/secrets/in settings. - Write a stack file with
deploy.replicas,update_config.order: start-first,failure_action: rollback, and a health check. - Pin PostgreSQL to one node with a placement constraint and a local volume.
- Run migrations as a one-off container on an attachable overlay network before updating the web service.
- Deploy with
docker stack deploy -c stack.yml myappand update images by tag.
Step-by-step solution
1. Initialise the swarm
docker swarm init --advertise-addr <manager-private-ip>
docker node ls
For more than one server, run the docker swarm join command printed by init on each worker. Swarm needs TCP 2377, TCP/UDP 7946, and UDP 4789 open between nodes on the private network only. Label the node that will hold the database:
docker node update --label-add db=true <manager-node-name>
2. Create secrets
python3 -c "import secrets; print(secrets.token_urlsafe(50))" | docker secret create django_secret_key -
openssl rand -base64 32 | tr -d '\n' | docker secret create postgres_password -
docker secret ls
Secrets are stored encrypted in the swarm's Raft log and mounted as files at /run/secrets/<name> only inside services that request them. Read them in Django:
from pathlib import Path
def read_secret(name, default=None):
path = Path("/run/secrets") / name
return path.read_text().strip() if path.exists() else default
SECRET_KEY = read_secret("django_secret_key")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "myapp",
"USER": "myapp",
"PASSWORD": read_secret("postgres_password"),
"HOST": "db",
"PORT": 5432,
"CONN_MAX_AGE": 60,
}
}
3. Write the stack file
# stack.yml
services:
web:
image: ghcr.io/example/myapp:${TAG}
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --access-logfile -
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
DJANGO_ALLOWED_HOSTS: example.com
secrets: [django_secret_key, postgres_password]
networks: [backend]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen(u.Request('http://127.0.0.1:8000/healthz/', headers={'Host': 'example.com'}), timeout=3)"]
interval: 10s
timeout: 5s
retries: 3
start_period: 20s
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
monitor: 30s
rollback_config:
parallelism: 1
order: start-first
restart_policy:
condition: on-failure
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets: [postgres_password]
volumes: [pgdata:/var/lib/postgresql/data]
networks: [backend]
deploy:
placement:
constraints: [node.labels.db == true]
nginx:
image: nginx:1.27
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
configs:
- source: nginx_conf
target: /etc/nginx/conf.d/default.conf
networks: [backend]
deploy:
placement:
constraints: [node.role == manager]
networks:
backend:
driver: overlay
attachable: true
volumes:
pgdata:
secrets:
django_secret_key:
external: true
postgres_password:
external: true
configs:
nginx_conf:
file: ./nginx.conf
Notes:
order: start-firststarts a new replica, waits for it to become healthy, then stops an old one. Withfailure_action: rollback, a replica that fails its health check duringmonitorrolls the service back.- The health check sends a
Hostheader that is inALLOWED_HOSTS; a bare127.0.0.1request gets400. IfSECURE_SSL_REDIRECTis on, addSECURE_REDIRECT_EXEMPT = [r"^healthz/$"]so the check is not redirected. - The PostgreSQL volume is local to a node, which is why the service is pinned with a placement constraint. A managed database removes this constraint entirely.
mode: hostpublishing preserves the client IP for Nginx logs and Django; the default ingress mode hides it behind the routing mesh.- Swarm configs are immutable. To change
nginx.conf, rename the config (for examplenginx_conf_v2) in the stack file and redeploy.
In nginx.conf, proxy to http://web:8000. Swarm's DNS resolves the service name to a virtual IP that balances across healthy replicas.
4. Deploy the stack
export TAG=1.4.0
docker stack deploy -c stack.yml --with-registry-auth myapp
docker stack services myapp
docker service ps myapp_web
--with-registry-auth forwards your registry login to the nodes so they can pull private images. Swarm prefixes names with the stack name: the web service becomes myapp_web and the network myapp_backend.
5. Run migrations once per release
Run migrations with the new image before rolling the web service. Because the network is attachable, a plain container can join it:
docker run --rm --network myapp_backend \
--env DJANGO_SETTINGS_MODULE=config.settings.production \
--env DJANGO_SECRET_KEY="$(cat ~/secrets/django_secret_key)" \
--env POSTGRES_PASSWORD="$(cat ~/secrets/postgres_password)" \
ghcr.io/example/myapp:1.5.0 python manage.py migrate --noinput
Plain docker run containers cannot mount Swarm secrets, so this step reads credentials from a protected file on the manager and passes them as environment variables. Make the settings accept either source (read_secret("postgres_password") or os.environ.get("POSTGRES_PASSWORD")). Keep migrations backward compatible, since old replicas keep serving traffic while new ones start.
6. Roll out and roll back
docker service update --image ghcr.io/example/myapp:1.5.0 --with-registry-auth myapp_web
docker service ps myapp_web
docker service rollback myapp_web
Or set TAG=1.5.0 and rerun docker stack deploy, which keeps the stack file as the source of truth.
Verification check:
docker service ls
docker service logs --tail 50 myapp_web
curl -sI https://example.com/healthz/
All web replicas should show 3/3, and the update state in docker service inspect --pretty myapp_web should report completed.
Explanation
Swarm turns each stack service into a desired state: an image, a replica count, and update rules. The manager continuously reconciles running containers to that state. Rolling updates replace replicas one at a time and use the container health check to decide whether the new version works. Secrets and configs are distributed by the manager, so no credentials sit in the stack file or image. The overlay network gives every service a stable DNS name across nodes.
Edge cases / notes
Static files and media
Serve static files from the image with WhiteNoise or from Nginx with a shared build step. Media must go to object storage once replicas run on more than one node, because local volumes are not shared.
Manager count
Run one or three manager nodes (an odd number) for Raft consensus. With a single manager, losing it means you cannot change the swarm until it returns, although running containers keep serving.
When to automate this
The release sequence (build and push image, run the migration container, docker stack deploy with the new tag, check service state) fits one CI job with SSH access to the manager or a Docker context pointing at it. Secret creation stays a controlled manual or provisioning step.
Next steps
- Start from a working single-host setup with Deploy Django with Docker Compose in Production.
- Compare approaches in How to Set Up Zero-Downtime Deployments for Django.
- Plan for more nodes with Scale Django Horizontally Behind a Load Balancer.
- If containers cannot reach the database, see Fix Django Cannot Connect to Postgres in Docker Compose.
FAQ
Is Docker Swarm still a reasonable choice for Django?
Yes, for small teams that want rolling updates and a few nodes without running Kubernetes. It is included in Docker Engine and uses a Compose-style file.
How do Docker secrets reach Django?
As files under /run/secrets/ inside containers of services that list them. Read the file in settings; the official PostgreSQL image supports _FILE variables for the same purpose.
How do I run Django migrations in Docker Swarm?
Run a one-off container with the new image on an attachable overlay network before updating the web service, so migrations run exactly once.
Why does Django see the wrong client IP in Swarm?
Ports published through the ingress routing mesh are source-NATed. Publish the proxy's ports in host mode and forward X-Forwarded-For to Django.