Reference Guide Detailed deployment notes with production context and concrete examples.

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:

bash
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

  1. Confirm the current deployment model
    Verify that Flask is already running behind Nginx and Gunicorn with a systemd service.
    bash
    sudo systemctl status myapp --no-pager
    sudo nginx -t
    
  2. Configure Gunicorn for graceful reload
    Edit the systemd unit and define ExecReload so systemd sends HUP to 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
    
  3. Reload systemd if the unit changed
    bash
    sudo systemctl daemon-reload
    
  4. Run more than one Gunicorn worker
    Zero-downtime reloads are not reliable with a single worker.
    Example:
    ini
    ExecStart=/srv/myapp/.venv/bin/gunicorn --workers 3 --bind unix:/run/myapp.sock wsgi:app
    

    Minimum practical baseline: --workers 2.
  5. 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:
    nginx
    location / {
        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;
    }
    
  6. 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:
    bash
    readlink -f /srv/myapp/current
    
  7. Deploy the new code
    Update the code in place or deploy a new release directory.
    Example with Git:
    bash
    cd /srv/myapp/current
    git fetch --all
    git checkout <tag-or-commit>
    
  8. Install dependencies
    Install Python package changes before the reload.
    bash
    source /srv/myapp/.venv/bin/activate
    pip install -r /srv/myapp/current/requirements.txt
    
  9. Apply safe database migrations
    Run migrations before the reload only if the migration is backward-compatible with the currently running code.
    bash
    source /srv/myapp/.venv/bin/activate
    cd /srv/myapp/current
    flask db upgrade
    
  10. Validate environment configuration
    Check environment files and required variables before reloading.
    bash
    sudo systemctl cat myapp
    sudo grep -v '^#' /etc/myapp.env
    
  11. Trigger a graceful Gunicorn reload
    This is the core deployment action.
    bash
    sudo systemctl reload myapp
    

    This sends HUP to the Gunicorn master so workers are replaced gracefully.
  12. Verify the service is still healthy
    Check service state and logs immediately after the reload.
    bash
    sudo systemctl status myapp --no-pager
    sudo journalctl -u myapp -n 100 --no-pager
    
  13. Test Gunicorn directly if using TCP
    If Gunicorn binds to 127.0.0.1:8000, verify direct responses:
    bash
    curl -I http://127.0.0.1:8000
    

    If you use a Unix socket, validate through Nginx instead.
  14. Test through Nginx
    Confirm the public path is serving the new release.
    bash
    curl -I https://yourdomain.com
    

    Also test a dynamic route that proves the new code is active.
  15. 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.
  16. Roll back if new workers fail to boot
    Revert to the previous release target and reload again.
    Example:
    bash
    ln -sfn /srv/myapp/releases/<previous-release> /srv/myapp/current
    sudo systemctl reload myapp
    

Common Causes

  • Using systemctl restart instead of systemctl reload → creates a stop/start gap → add ExecReload and use graceful reload.
  • Gunicorn unit has no ExecReload directive → systemd cannot perform a graceful reload → define ExecReload=/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 EnvironmentFile and run daemon-reload if 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

bash
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 $MAINPID exists 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 Gateway responses 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 ExecReload configured
  • Gunicorn runs with multiple workers
  • Nginx upstream target did not change during deploy
  • New dependencies installed successfully
  • Database migrations completed without error
  • systemctl reload returns success
  • No 502 errors 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).

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.