Zero-Downtime Deploys
Users should never see an error page during deployment. Here's how to achieve seamless updates.
The Problem
A naive deploy looks like:
git pull
pip install -r requirements.txt
systemctl restart gunicorn # ← Users see errors here
During the restart, requests fail. With enough traffic, this is unacceptable.
Strategy 1: Gunicorn Graceful Reload
Gunicorn can reload workers without dropping connections:
# Instead of restart (drops connections)
sudo systemctl restart gunicorn
# Use reload (graceful worker replacement)
sudo systemctl reload gunicorn
# Or send HUP signal directly
kill -HUP $(cat /run/gunicorn/pid)
How it works: 1. Master process spawns new workers with updated code 2. Old workers finish their current requests 3. Old workers shut down 4. No requests dropped
Strategy 2: Rolling Restart with Nginx
Use Nginx as a reverse proxy with upstream health checks:
upstream django {
server 127.0.0.1:8000;
server 127.0.0.1:8001 backup;
}
server {
location / {
proxy_pass http://django;
proxy_next_upstream error timeout;
}
}
Deploy process: 1. Start new version on port 8001 2. Health check passes → add to upstream 3. Drain connections from old version 4. Stop old version
Strategy 3: Blue-Green Deployment
Maintain two identical environments:
┌─────────┐
│ Nginx │
└────┬─────┘
┌─────┴─────┐
┌────▼───┐ ┌────▼───┐
│ Blue │ │ Green │
│ (live) │ │ (idle) │
└────────┘ └────────┘
- Deploy new code to the idle environment
- Run migrations and tests
- Switch Nginx to point at the new environment
- Old environment becomes idle
Database Migrations
The trickiest part. Rules for zero-downtime migrations:
- Adding columns/tables is safe (old code ignores new columns)
- Removing columns requires two deploys: stop using → then remove
- Renaming columns: add new → copy data → update code → drop old
- Always use
--fakecarefully and test migrations on staging first
# Safe: adding a nullable column
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name="post",
name="subtitle",
field=models.CharField(max_length=255, null=True, blank=True),
),
]