Blue-Green Deployments for Django
Run blue-green deployments for Django with Nginx: two release environments, smoke tests, instant traffic switching, migration rules, and fast rollback.
Problem statement
Rolling restarts avoid downtime, but they have two weaknesses: during the rollout, old and new code serve traffic at the same time, and rolling back means rolling through all servers again. For releases where you want to test the exact production setup before users see it, and switch back in seconds if something goes wrong, blue-green deployment is a better fit.
In blue-green deployment, two complete copies of the application run side by side:
- blue: the version serving users now
- green: the new version, deployed and tested without user traffic
When green passes checks, the router switches traffic from blue to green. Blue stays running, unchanged, as the rollback target.
The pattern is simple for stateless application code. The hard part for Django is the shared state both colors use, above all the database schema, plus sessions, caches, background workers, and scheduled jobs.
Quick answer
- Run two app instances (colors) on separate sockets, ports, or servers, each with its own release directory or image.
- Point the router (Nginx upstream, load balancer target group) at the active color only.
- Deploy the new release to the idle color, run migrations that are backward compatible, and smoke test the idle color directly.
- Switch traffic by changing the upstream and reloading Nginx (or swapping target groups).
- Keep the old color running for a rollback window; switch back if errors rise.
- Follow expand/contract migrations so both colors work with the same database.
- Run Celery workers and Beat per active color, or design tasks to be compatible across versions.
Step-by-step solution
1. Layout on a single server
For one server, run the two colors as separate systemd units with separate sockets:
/srv/myapp/
├── blue/ # release directory (code + virtualenv)
├── green/
└── active # file containing "blue" or "green"
A template unit serves both colors:
# /etc/systemd/system/myapp@.service
[Unit]
Description=myapp %i
After=network.target
[Service]
User=myapp
Group=www-data
WorkingDirectory=/srv/myapp/%i/app
EnvironmentFile=/etc/myapp/env
RuntimeDirectory=myapp-%i
ExecStart=/srv/myapp/%i/venv/bin/gunicorn config.wsgi:application \
--bind unix:/run/myapp-%i/gunicorn.sock \
--workers 3 \
--access-logfile -
Restart=on-failure
[Install]
WantedBy=multi-user.target
Both colors can run at the same time: myapp@blue and myapp@green. The server needs enough memory for both sets of workers during the switch window.
2. Route traffic with an Nginx include
Keep the active upstream in a small file Nginx includes:
# /etc/nginx/conf.d/myapp-upstream.conf (managed by the deploy script)
upstream myapp_active {
server unix:/run/myapp-blue/gunicorn.sock;
}
server {
listen 443 ssl;
server_name www.example.com;
location / {
proxy_pass http://myapp_active;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Add a separate, restricted server block that always points at the idle color for smoke testing:
server {
listen 443 ssl;
server_name preview.internal.example.com;
allow 10.0.0.0/16;
deny all;
location / {
proxy_pass http://unix:/run/myapp-green/gunicorn.sock;
proxy_set_header Host www.example.com;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Setting Host to the production hostname lets Django's ALLOWED_HOSTS and URL generation behave exactly as for real traffic. The deploy script rewrites this block too, so it always targets the idle color.
3. Deploy to the idle color
#!/bin/bash
set -euo pipefail
ACTIVE=$(cat /srv/myapp/active)
IDLE=$([ "$ACTIVE" = "blue" ] && echo green || echo blue)
RELEASE_SHA=$1
echo "active=$ACTIVE deploying to $IDLE ($RELEASE_SHA)"
rm -rf "/srv/myapp/$IDLE"
mkdir -p "/srv/myapp/$IDLE"
git -C /srv/myapp/repo fetch --quiet origin
git -C /srv/myapp/repo worktree prune
git -C /srv/myapp/repo worktree add --detach "/srv/myapp/$IDLE/app" "$RELEASE_SHA"
python3 -m venv "/srv/myapp/$IDLE/venv"
"/srv/myapp/$IDLE/venv/bin/pip" install --quiet -r "/srv/myapp/$IDLE/app/requirements.txt"
set -a; . /etc/myapp/env; set +a
cd "/srv/myapp/$IDLE/app"
"/srv/myapp/$IDLE/venv/bin/python" manage.py collectstatic --noinput
"/srv/myapp/$IDLE/venv/bin/python" manage.py migrate --noinput
sudo systemctl restart "myapp@$IDLE"
Deploy scripts vary; the essential steps are a clean release for the idle color, dependencies installed, static files collected, migrations applied once, and the idle service started.
Collect static files into a location both colors can serve, and keep old hashed files, so pages rendered by blue still find their assets after green's collectstatic runs.
4. Smoke test the idle color
Before switching, test green through the preview host or directly through its socket:
curl -sf --unix-socket /run/myapp-green/gunicorn.sock -H "Host: www.example.com" \
-H "X-Forwarded-Proto: https" http://localhost/healthz/
curl -sf --unix-socket /run/myapp-green/gunicorn.sock -H "Host: www.example.com" \
-H "X-Forwarded-Proto: https" http://localhost/ | grep -q "<title>"
Automate a short list of critical checks: health endpoint, homepage, login page, one API endpoint, and admin static assets. Stop the deploy if any fails; users never saw green.
5. Switch traffic
sudo tee /etc/nginx/conf.d/myapp-upstream.conf >/dev/null <<EOF
upstream myapp_active {
server unix:/run/myapp-$IDLE/gunicorn.sock;
}
EOF
sudo nginx -t
sudo systemctl reload nginx
echo "$IDLE" | sudo tee /srv/myapp/active >/dev/null
nginx -s reload (or systemctl reload nginx) starts new worker processes with the new configuration and lets old workers finish their current requests, so in-flight requests complete on blue while new requests go to green.
Watch error rates and latency for the rollback window (for example 15 to 30 minutes, or until the next release).
6. Roll back
Rolling back is the same switch in the other direction:
sudo tee /etc/nginx/conf.d/myapp-upstream.conf >/dev/null <<EOF
upstream myapp_active {
server unix:/run/myapp-$ACTIVE/gunicorn.sock;
}
EOF
sudo nginx -t && sudo systemctl reload nginx
echo "$ACTIVE" | sudo tee /srv/myapp/active >/dev/null
Because blue kept running unchanged, rollback takes seconds and needs no rebuild. Only stop blue after the rollback window passes.
7. Database migrations: expand and contract
Both colors share one database. At switch time and during rollback, both versions must work with the current schema. Split breaking changes across releases:
Renaming a column title to name:
- Release 1 (expand): add
nameas nullable; code writes bothtitleandname, readstitle. Backfillnamefromtitle. - Release 2: code reads and writes
name, still writestitlefor compatibility. - Release 3 (contract): stop writing
title; after rollback is no longer needed, remove it in a migration.
Operations that break the previous version if applied in one step: dropping or renaming columns or tables in use, adding NOT NULL columns without defaults, and changing column types the old code relies on. Django's migrate does not know about blue-green; the discipline must be in how migrations are planned.
8. Sessions, caches, and background work
- Sessions: use database or shared cache sessions so users stay logged in when traffic moves between colors. Both colors need the same
SECRET_KEY. - Cache keys: if green changes the format of cached values, use a new
KEY_PREFIXor cacheVERSIONfor green so blue does not read incompatible data (and vice versa during rollback). - Celery workers: tasks queued by blue may be executed by green workers. Keep task signatures backward compatible, or run workers per color on separate queues and switch producers with the traffic switch.
- Celery Beat and cron: run on one color only (the active one) to avoid duplicates; move them as part of the switch.
9. Blue-green with load balancers and containers
The same pattern maps to cloud infrastructure:
- AWS: two target groups; switch the ALB listener rule's forward action from blue to green.
- Kubernetes: two Deployments with different labels; switch the Service selector from
color: bluetocolor: green. - Platforms with slots: Azure App Service deployment slots swap staging and production.
10. Verify
Verification check:
- the idle color passes automated smoke tests before the switch
- the switch completes with no increase in 5xx responses
- a rollback drill in staging returns traffic to the previous color in under a minute
- migrations in each release are compatible with the previous release
- scheduled jobs run only on the active color
Explanation
Blue-green separates deploying from releasing: new code is fully started and verified before any user reaches it, and releasing is a routing change that is equally fast to reverse. The cost is running two environments during the switch window and the discipline to keep shared state compatible across versions. For Django, that mainly means expand-and-contract migrations, shared sessions, and careful handling of background tasks.
Edge cases / notes
Long-running requests and WebSockets
Nginx reloads let existing connections finish on old workers, but WebSocket connections can stay on blue for a long time. Plan client reconnects or wait longer before stopping blue.
Memory during switch
Running two full sets of Gunicorn workers doubles app memory on the server. Size the server for it, or use fewer workers per color.
Canary releases
Instead of switching 100 percent of traffic, you can weight upstream servers or target groups to send a small share to green first. That adds complexity in comparing metrics per color.
Common mistakes
- Running a destructive migration during a blue-green deploy, which breaks blue as a rollback target.
- Stopping blue immediately after switching.
- Running Celery Beat on both colors.
- Collecting static files in a way that deletes blue's hashed assets.
Next steps
- Compare with rolling restarts in How to Set Up Zero-Downtime Deployments for Django.
- Plan schema changes with Django Migration Strategy for Safe Deployments.
- Practice recovery with How to Roll Back a Django Release Safely.
- Decouple releases further with Use Feature Flags for Safer Django Releases.
FAQ
What is a blue-green deployment for Django?
Two complete app environments run side by side. The new version is deployed and tested on the idle one, then traffic is switched to it, keeping the old version running for instant rollback.
How do database migrations work with blue-green deployments?
Both versions share one database, so migrations must be backward compatible. Use expand-and-contract: add new structures first, switch code, and remove old structures in a later release.
How do I switch traffic between blue and green with Nginx?
Keep the active upstream in an included file, rewrite it to point at the other color's socket, run nginx -t, and reload Nginx. In-flight requests finish on the old workers.
Is blue-green better than rolling deployments?
It gives faster rollback and lets you test the exact production build before release, at the cost of running two environments and stricter compatibility rules. Rolling deployments are simpler and use fewer resources.