Zero Downtime Flask Deployment (Gunicorn Reload Strategy)
If you're trying to deploy Flask updates without dropping requests, this guide shows you how to reload Gunicorn safely behind Nginx. The goal is to replace application code or workers in production while keeping the site available and validating that the new release is serving traffic.
Quick Fix / Quick Setup
Use this if your Flask app is already running behind Nginx and Gunicorn with a systemd service that supports reloads:
cd /srv/myapp
source .venv/bin/activate
pip install -r requirements.txt
flask db upgrade
sudo systemctl reload myapp
sudo systemctl status myapp --no-pager
curl -I http://127.0.0.1:8000
Use systemctl reload only if your Gunicorn systemd unit defines ExecReload with a HUP signal. This performs a graceful worker reload instead of a full stop/start in simple setups.
What’s Happening
Gunicorn can reload workers gracefully so existing requests finish while new workers start with updated code. Nginx continues proxying to Gunicorn during the reload, which avoids the connection gap caused by a stop/start restart. Zero downtime depends on enough workers, correct signal handling, healthy startup, and backward-compatible database changes.
Step-by-Step Guide
- Confirm the current deployment model
Verify that Flask is already running behind Nginx and Gunicorn with a systemd service.bashsudo systemctl status myapp --no-pager sudo nginx -t - Configure Gunicorn for graceful reload
Edit the systemd unit and defineExecReloadso systemd sendsHUPto the Gunicorn master process.
Example unit:ini[Unit] Description=Gunicorn for myapp After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/srv/myapp/current EnvironmentFile=/etc/myapp.env ExecStart=/srv/myapp/.venv/bin/gunicorn \ --workers 3 \ --bind unix:/run/myapp.sock \ wsgi:app ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=3 [Install] WantedBy=multi-user.target - Reload systemd if the unit changedbash
sudo systemctl daemon-reload - Run more than one Gunicorn worker
Zero-downtime reloads are not reliable with a single worker.
Example:iniExecStart=/srv/myapp/.venv/bin/gunicorn --workers 3 --bind unix:/run/myapp.sock wsgi:app
Minimum practical baseline:--workers 2. - Keep the upstream target stable
Do not change the Unix socket path or TCP port during a normal release. Nginx should continue pointing to the same upstream.
Example Nginx upstream:nginxlocation / { proxy_pass http://unix:/run/myapp.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - Use a stable release path if you deploy versioned releases
A release-directory layout makes rollback easier.
Example:text/srv/myapp/releases/2026-04-21 /srv/myapp/current -> /srv/myapp/releases/2026-04-21
Verify the active release target:bashreadlink -f /srv/myapp/current - Deploy the new code
Update the code in place or deploy a new release directory.
Example with Git:bashcd /srv/myapp/current git fetch --all git checkout <tag-or-commit> - Install dependencies
Install Python package changes before the reload.bashsource /srv/myapp/.venv/bin/activate pip install -r /srv/myapp/current/requirements.txt - Apply safe database migrations
Run migrations before the reload only if the migration is backward-compatible with the currently running code.bashsource /srv/myapp/.venv/bin/activate cd /srv/myapp/current flask db upgrade - Validate environment configuration
Check environment files and required variables before reloading.bashsudo systemctl cat myapp sudo grep -v '^#' /etc/myapp.env - Trigger a graceful Gunicorn reload
This is the core deployment action.bashsudo systemctl reload myapp
This sendsHUPto the Gunicorn master so workers are replaced gracefully. - Verify the service is still healthy
Check service state and logs immediately after the reload.bashsudo systemctl status myapp --no-pager sudo journalctl -u myapp -n 100 --no-pager - Test Gunicorn directly if using TCP
If Gunicorn binds to127.0.0.1:8000, verify direct responses:bashcurl -I http://127.0.0.1:8000
If you use a Unix socket, validate through Nginx instead. - Test through Nginx
Confirm the public path is serving the new release.bashcurl -I https://yourdomain.com
Also test a dynamic route that proves the new code is active. - Update static assets if the release changed them
If your release includes static file updates, sync or collect them before final validation. If static assets fail after deploy, see Flask Static Files Not Loading in Production. - Roll back if new workers fail to boot
Revert to the previous release target and reload again.
Example:bashln -sfn /srv/myapp/releases/<previous-release> /srv/myapp/current sudo systemctl reload myapp
Common Causes
- Using
systemctl restartinstead ofsystemctl reload→ creates a stop/start gap → addExecReloadand use graceful reload. - Gunicorn unit has no
ExecReloaddirective → systemd cannot perform a graceful reload → defineExecReload=/bin/kill -HUP $MAINPID. - Only one Gunicorn worker is running → no spare capacity during replacement → increase workers to at least 2.
- New code fails at import time → new workers never become healthy → check Gunicorn logs and fix app startup errors before reloading again.
- Database migration is not backward-compatible → old workers or new code break during transition → split schema and code changes across multiple releases.
- Environment variables changed but systemd did not load them correctly → new workers boot with missing config → verify
EnvironmentFileand rundaemon-reloadif the unit changed. - Socket or port changed during deploy → Nginx points to the wrong upstream → keep the Gunicorn bind path stable or update Nginx and validate before reload.
- Static files were updated but not collected or synced → app code is live but assets are stale or missing → deploy static assets before validation.
- Long-running requests delay worker turnover → reload appears stuck or slow → review request timeouts and long-running endpoints.
Debugging Section
Check the service definition, service state, worker processes, socket bindings, and proxy status.
Commands to run
sudo systemctl cat myapp
sudo systemctl status myapp --no-pager
sudo journalctl -u myapp -n 200 --no-pager
sudo journalctl -u myapp -f
ps aux | grep gunicorn
sudo ss -ltnp | grep 8000
sudo ss -lx | grep myapp.sock
curl -I http://127.0.0.1:8000
curl -I https://yourdomain.com
sudo nginx -t
sudo journalctl -u nginx -n 100 --no-pager
readlink -f /srv/myapp/current
source /srv/myapp/.venv/bin/activate && flask db current
source /srv/myapp/.venv/bin/activate && flask db upgrade
sudo systemctl reload myapp
What to look for
ExecReload=/bin/kill -HUP $MAINPIDexists in the active unit- Gunicorn master stays running during reload
- New worker PIDs appear after the reload
- No import errors, missing environment variables, or migration failures in
journalctl - Nginx configuration passes
nginx -t - The app still answers through the public domain
- No
502 Bad Gatewayresponses appear during validation
If requests fail during reload, see Fix Flask 502 Bad Gateway (Step-by-Step Guide). If workers do not come back with new code, see Flask App Not Reloading After Deploy. If Gunicorn fails completely, see Flask Gunicorn Service Failed to Start.
Checklist
- Gunicorn systemd unit has
ExecReloadconfigured - Gunicorn runs with multiple workers
- Nginx upstream target did not change during deploy
- New dependencies installed successfully
- Database migrations completed without error
-
systemctl reloadreturns success - No
502errors appear during or after reload - New release responds through Nginx
- Logs show new workers started cleanly
- Rollback path is available if validation fails
For broader production validation, use Flask Production Checklist (Everything You Must Do).
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Production Checklist (Everything You Must Do)
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask App Not Reloading After Deploy
FAQ
What signal should be used for a zero-downtime Gunicorn reload?
Use HUP for a standard graceful reload of Gunicorn workers in most systemd-managed Flask setups.
Can I use this strategy with a Unix socket?
Yes. A stable Unix socket path works well as long as Nginx continues pointing to the same socket.
Why am I still seeing brief 502 errors during reload?
Usually because workers fail to start, only one worker is configured, or Nginx is pointed at the wrong upstream.
Do I need Nginx for zero downtime?
It is strongly recommended. Nginx buffers and proxies requests while Gunicorn workers are replaced.
Should I reload systemd too?
Only run systemctl daemon-reload if you changed the unit file. Use systemctl reload myapp for the application reload itself.
Final Takeaway
Zero-downtime Flask deploys are usually a graceful Gunicorn reload problem, not an Nginx problem. Keep the bind target stable, run safe migrations, use multiple workers, and reload the Gunicorn master through systemd instead of restarting the service.