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

Flask Deployment Checklist

If you're deploying a Flask app to production and need a fast validation list, this guide shows you what to verify before going live. Use it to confirm your app is reachable, services start correctly, configuration is production-safe, and common deployment failures are prevented.

Quick Fix / Quick Setup

Run these checks first:

bash
sudo systemctl status gunicorn
sudo systemctl status nginx
sudo nginx -t
curl -I http://127.0.0.1:8000 || curl --unix-socket /run/gunicorn.sock http://localhost/
curl -I https://your-domain.com
journalctl -u gunicorn -n 100 --no-pager
journalctl -u nginx -n 100 --no-pager

If Gunicorn is active, Nginx config passes, the app responds locally, and the public domain returns the expected status over HTTPS, the deployment is usually functional.

What’s Happening

A Flask production deployment depends on multiple layers working together: application config, Python environment, Gunicorn, systemd, Nginx, database access, static file handling, DNS, and HTTPS.

A failure in any layer can present as 502 errors, missing assets, app startup failures, or intermittent downtime.

This checklist validates the full request path from process startup to public traffic.

Step-by-Step Guide

  1. Confirm the application is deployed in the expected path
    Verify the project directory, ownership, and files:
    bash
    ls -lah /path/to/project
    sudo chown -R deployuser:deployuser /path/to/project
    
  2. Verify the virtual environment and installed packages
    Activate the environment and check package health:
    bash
    source /path/to/venv/bin/activate
    pip check
    pip freeze | grep -E 'Flask|gunicorn'
    
  3. Confirm the Gunicorn app entry point
    Make sure the configured target matches your app structure.
    Examples:
    bash
    wsgi:app
    

    or:
    bash
    'wsgi:create_app()'
    
  4. Test Gunicorn manually before using systemd or Nginx
    Start the app directly:
    bash
    source /path/to/venv/bin/activate
    gunicorn -b 127.0.0.1:8000 wsgi:app
    

    In another terminal:
    bash
    curl -I http://127.0.0.1:8000
    
  5. Verify production environment variables
    Confirm required variables exist:
    bash
    printenv | grep -E 'SECRET_KEY|DATABASE|FLASK|REDIS|API'
    

    If using systemd, check the unit file or EnvironmentFile:
    bash
    systemctl cat gunicorn
    
  6. Validate the systemd service file
    Confirm WorkingDirectory, ExecStart, User, Group, and environment settings are correct.
    Example:
    ini
    [Unit]
    Description=Gunicorn for Flask app
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/path/to/project
    Environment="PATH=/path/to/venv/bin"
    EnvironmentFile=/etc/default/myflaskapp
    ExecStart=/path/to/venv/bin/gunicorn --workers 3 --bind unix:/run/gunicorn.sock wsgi:app
    
    [Install]
    WantedBy=multi-user.target
    
  7. Reload systemd and restart Gunicorn
    Apply changes and enable startup on boot:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart gunicorn
    sudo systemctl enable gunicorn
    
  8. Check Gunicorn health and logs
    Validate service state:
    bash
    sudo systemctl status gunicorn
    sudo journalctl -u gunicorn -n 100 --no-pager
    

    Look for:
    • import errors
    • missing packages
    • bad environment variables
    • permission failures
  9. Verify Gunicorn bind target matches Nginx upstream
    If Gunicorn uses TCP:
    bash
    --bind 127.0.0.1:8000
    

    Nginx must proxy to the same target.
    If Gunicorn uses a Unix socket:
    bash
    --bind unix:/run/gunicorn.sock
    

    Nginx must use that exact socket path.
  10. Validate Nginx configuration
    Test and reload:
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  11. Confirm the Nginx server block
    Verify server_name, reverse proxy, headers, and static file locations.
    Example:
    nginx
    server {
        listen 80;
        server_name your-domain.com www.your-domain.com;
    
        location /static/ {
            alias /path/to/project/static/;
        }
    
        location / {
            proxy_pass http://127.0.0.1:8000;
            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;
        }
    }
    
  12. Verify DNS
    Confirm the domain resolves to the server IP:
    bash
    dig your-domain.com +short
    dig www.your-domain.com +short
    
  13. Test local HTTP response through Nginx
    Check Nginx routing locally:
    bash
    curl -I http://localhost
    curl -I -H 'Host: your-domain.com' http://127.0.0.1
    
  14. Validate HTTPS
    Confirm the certificate, TLS listener, and public response:
    bash
    curl -I https://your-domain.com
    sudo certbot certificates
    sudo systemctl status certbot.timer
    
  15. Test database connectivity
    Run an app or migration command inside the production environment:
    bash
    source /path/to/venv/bin/activate
    flask db current
    

    Or test from Python:
    bash
    python -c "from yourapp import app; print('app import ok')"
    
  16. Apply pending migrations
    Use the project’s migration command before launch:
    bash
    flask db upgrade
    
  17. Verify static files
    Confirm the Nginx alias path matches the filesystem and files are readable:
    bash
    ls -lah /path/to/project/static
    curl -I https://your-domain.com/static/app.css
    

    If static files are missing, see Flask Static Files Not Loading in Production.
  18. Verify media or upload directories
    Ensure directories exist and are writable by the application user:
    bash
    mkdir -p /path/to/project/uploads
    sudo chown -R www-data:www-data /path/to/project/uploads
    ls -lah /path/to/project/uploads
    
  19. Check firewall rules
    Allow public web traffic and keep internal app ports private:
    bash
    sudo ufw status
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    
  20. Confirm services start on boot
    Check enabled state:
    bash
    sudo systemctl is-enabled gunicorn
    sudo systemctl is-enabled nginx
    

    Optional reboot validation:
    bash
    sudo reboot
    
  21. Verify logging, monitoring, and backups
    At minimum, confirm logs are accessible and database backups exist.
    bash
    sudo journalctl -u gunicorn -n 50 --no-pager
    sudo journalctl -u nginx -n 50 --no-pager
    
  22. Perform an external end-to-end test
    Validate:
    • home page
    • login route
    • one database-backed route
    • static assets
    • form submission
    • upload flow if used
  23. Record final deployment paths
    Save these values for maintenance:
    • app directory
    • virtualenv path
    • systemd unit path
    • Nginx site file
    • socket path or port
    • domain records

Common Causes

  • Gunicorn service misconfiguration → Wrong WorkingDirectory, bad virtualenv path, or incorrect app module prevents startup → Fix the systemd unit and restart the service.
  • Nginx upstream mismatch → Nginx points to the wrong socket or port → Make the Gunicorn bind and proxy_pass target identical.
  • Environment variables missing → App starts with missing secrets or database settings and fails at import or runtime → Load variables via systemd Environment= or EnvironmentFile= and restart Gunicorn. See Flask Environment Variables Not Loading in Production.
  • Static files not served → Nginx alias path does not match the actual static directory → Correct the filesystem path and reload Nginx.
  • Database connection failure → Wrong credentials, host, firewall, or missing DB service → Verify connection settings and reachability from the server.
  • Permissions issue on Unix socket → Nginx cannot read the Gunicorn socket → Adjust socket location, user/group, and directory permissions.
  • DNS not pointing to the server → Public domain never reaches Nginx → Update A/AAAA records and wait for propagation.
  • HTTPS incomplete → Certificate exists but Nginx is not serving the right TLS config → Validate certificates, server block, and reload Nginx.
  • Migrations not applied → App errors on routes that expect new tables or columns → Run migrations before launch.
  • Firewall blocks traffic → Server works locally but not externally → Allow ports 80 and 443 and verify cloud firewall rules.

Debugging Section

Check service and application state:

bash
sudo systemctl status gunicorn
sudo systemctl status nginx
sudo systemctl daemon-reload
systemctl cat gunicorn
sudo nginx -t

Check logs:

bash
sudo journalctl -u gunicorn -n 200 --no-pager
sudo journalctl -u nginx -n 200 --no-pager
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log

Check sockets and listening ports:

bash
ss -ltnp | grep -E ':80|:443|:8000'
ss -lx | grep gunicorn

Test the upstream directly:

bash
curl -I http://127.0.0.1:8000
curl --unix-socket /run/gunicorn.sock http://localhost/
curl -I -H 'Host: your-domain.com' http://127.0.0.1
curl -I https://your-domain.com

Check DNS and filesystem permissions:

bash
dig your-domain.com +short
ls -lah /path/to/project
namei -l /run/gunicorn.sock

What to look for:

  • Gunicorn failing to import the app
  • wrong module path in ExecStart
  • Nginx proxying to the wrong port or socket
  • missing environment variables in the systemd context
  • unreadable static files or socket path
  • certificate or DNS mismatches

If the public site returns a 502, go directly to Fix Flask 502 Bad Gateway (Step-by-Step Guide).

Checklist

  • Application code is deployed in the correct directory.
  • Virtual environment exists and contains required packages.
  • Gunicorn starts manually with the configured app entry point.
  • systemd service uses the correct user, group, working directory, and executable paths.
  • systemctl status gunicorn shows the service is active.
  • Gunicorn bind target matches the Nginx upstream target.
  • nginx -t passes without errors.
  • Nginx server block uses the correct domain and reverse proxy settings.
  • Public DNS resolves to the correct server IP.
  • Static file paths in Nginx match actual filesystem locations.
  • Media or upload directories exist and have correct permissions.
  • Environment variables are loaded in production.
  • Database credentials are correct and reachable from the server.
  • Migrations have been applied.
  • HTTPS is configured and the certificate is valid.
  • Firewall allows ports 80 and 443.
  • Gunicorn and Nginx are enabled on boot.
  • Logs are accessible for Nginx and Gunicorn.
  • External requests to the public domain return the expected response.
  • Key app flows work after deploy.

FAQ

Q: Is this checklist enough for a first production launch?
A: It covers the core deployment path and the most common failure points. Add security, monitoring, backup, and scaling checks for a complete production review.

Q: What is the fastest way to verify the stack is healthy?
A: Check Gunicorn and Nginx status, validate Nginx config, test the app locally, then test the public HTTPS endpoint.

Q: Why does the app work with Gunicorn manually but fail through systemd?
A: systemd often runs with a different working directory, user, PATH, and environment variables. Compare the service file with the manual command.

Q: Should static files be served by Flask in production?
A: No. In most production setups, Nginx should serve static and media files directly.

Q: What if the domain works but returns 502?
A: Nginx is reachable, but Gunicorn is not responding correctly. Check upstream target, Gunicorn service status, and application startup logs.

Final Takeaway

A Flask deployment is production-ready only when the full chain is validated: app, environment, Gunicorn, systemd, Nginx, database, static or media paths, DNS, and HTTPS.

Use this checklist to catch mismatches early and confirm the deployment works from both the server and the public domain.