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

Flask Production Checklist (Everything You Must Do)

If you're preparing a Flask app for production or validating an existing deployment, this guide shows you what must be configured and verified. Use it to catch common deployment gaps before they cause downtime, security issues, or broken app behavior.

Quick Fix / Quick Setup

Run this fast validation pass on the server:

bash
# Validate Nginx and service state
sudo nginx -t
sudo systemctl status gunicorn --no-pager
sudo systemctl status nginx --no-pager

# Test Gunicorn directly
curl -I http://127.0.0.1:8000 || curl --unix-socket /run/gunicorn.sock http://localhost/

# Test public HTTPS
curl -I https://your-domain.com

# Check recent logs
journalctl -u gunicorn -n 50 --no-pager
journalctl -u nginx -n 50 --no-pager

# Verify service environment and migration state
sudo systemctl show gunicorn --property=Environment
source /path/to/venv/bin/activate && flask db current

# Verify static file delivery
curl -I https://your-domain.com/static/app.css

Use this as a pre-launch or post-deploy validation pass. Replace paths, domain, service names, and socket locations to match your setup.

What’s Happening

A Flask app is production-ready only when the full stack is configured correctly: process manager, app server, reverse proxy, TLS, static/media handling, secrets, database, logs, and recovery controls.

Most production issues come from one missing layer rather than from Flask itself.

This checklist validates the full request path from domain to application response.

Step-by-Step Guide

  1. Confirm the app runs with production settings only.
    Activate the correct environment and start the app with production config.
    bash
    source /path/to/venv/bin/activate
    python -c "from yourapp import app; print(app)"
    

    Validate that debug mode is off in your config:
    python
    DEBUG = False
    TESTING = False
    
  2. Disable development mode.
    Do not rely on FLASK_ENV=development or debug mode in production.
    bash
    unset FLASK_ENV
    

    If using environment variables:
    ini
    FLASK_DEBUG=0
    
  3. Store secrets outside source files.
    Put secrets in systemd environment settings, an environment file, or a secrets manager.
    Example systemd override:
    ini
    [Service]
    Environment="SECRET_KEY=replace-with-real-secret"
    Environment="DATABASE_URL=postgresql://user:pass@db-host/dbname"
    Environment="FLASK_DEBUG=0"
    
  4. Verify the WSGI entry point.
    Confirm Gunicorn can import the app module.
    bash
    source /path/to/venv/bin/activate
    gunicorn --bind 127.0.0.1:8000 wsgi:app
    

    Common valid patterns:
    bash
    gunicorn wsgi:app
    gunicorn app:app
    gunicorn 'yourapp:create_app()'
    
  5. Create and enable a Gunicorn service.
    Example systemd unit:
    ini
    # /etc/systemd/system/gunicorn.service
    [Unit]
    Description=Gunicorn for Flask app
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/var/www/yourapp
    EnvironmentFile=/etc/yourapp.env
    ExecStart=/var/www/yourapp/venv/bin/gunicorn \
        --workers 3 \
        --bind unix:/run/gunicorn.sock \
        wsgi:app
    Restart=always
    RestartSec=3
    
    [Install]
    WantedBy=multi-user.target
    

    Reload and enable:
    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now gunicorn
    
  6. Confirm Gunicorn is listening on the expected socket or port.
    For TCP:
    bash
    ss -ltnp | grep ':8000'
    curl -I http://127.0.0.1:8000
    

    For Unix socket:
    bash
    sudo ss -lx | grep gunicorn
    ls -la /run/gunicorn.sock
    curl --unix-socket /run/gunicorn.sock http://localhost/
    
  7. Configure Nginx as the reverse proxy.
    Example Nginx server block:
    nginx
    server {
        listen 80;
        server_name your-domain.com www.your-domain.com;
    
        location /static/ {
            alias /var/www/yourapp/static/;
        }
    
        location /uploads/ {
            alias /var/www/yourapp/uploads/;
        }
    
        location / {
            proxy_pass http://unix:/run/gunicorn.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;
        }
    }
    
  8. Validate and reload Nginx only after config passes.
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  9. Verify DNS points to the correct server.
    bash
    dig +short your-domain.com
    dig +short www.your-domain.com
    

    Confirm the returned IP matches the server.
  10. Enable HTTPS.

If using Certbot with Nginx:

bash
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Test the result:

bash
curl -I https://your-domain.com
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null
  1. Verify certificate renewal.
bash
sudo certbot renew --dry-run
  1. Serve static files directly from Nginx.

Confirm the static directory exists and contains files:

bash
ls -la /var/www/yourapp/static
curl -I https://your-domain.com/static/app.css

If static files fail, see Flask Static Files Not Loading in Production.

  1. Handle media or uploads separately from static files.

Do not mix build-time static assets and runtime uploads.

Example:

nginx
location /uploads/ {
    alias /var/www/yourapp/uploads/;
}

Validate:

bash
ls -la /var/www/yourapp/uploads
  1. Apply database migrations before serving traffic.
bash
source /path/to/venv/bin/activate
flask db upgrade
flask db current
  1. Verify database connectivity from the app host.

Confirm credentials, host, port, TLS, and network access are correct.

Example environment:

ini
DATABASE_URL=postgresql://user:pass@db-host:5432/dbname

If needed, test with the database client directly.

  1. Set production security configuration.

At minimum, use a real secret key and secure cookie settings.

Example:

python
SECRET_KEY = os.environ["SECRET_KEY"]
SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
  1. Enable persistent logging.

Check Gunicorn and Nginx logs are available and readable.

bash
sudo journalctl -u gunicorn -n 100 --no-pager
sudo journalctl -u nginx -n 100 --no-pager

If using file-based logs, confirm file paths and write permissions.

  1. Add a health check endpoint.

Example Flask route:

python
@app.route("/health")
def health():
    return {"status": "ok"}, 200

Validate:

bash
curl -I https://your-domain.com/health
  1. Configure monitoring and alerting.

At minimum, monitor:

  • uptime
  • 5xx response rate
  • CPU and memory
  • disk space
  • certificate expiry
  1. Create backups and verify restore capability.

Back up:

  • database
  • uploaded files
  • critical config

The checklist is incomplete until restore steps are documented and tested.

  1. Restrict public network exposure.

Only expose required ports.

Typical public ports:

  • 80
  • 443

Internal-only services should not be publicly reachable.

  1. Run an end-to-end smoke test.

Validate all critical paths:

  • domain resolves
  • HTTPS loads
  • app returns 200/expected responses
  • login works
  • database reads/writes succeed
  • static assets load
  • uploads work if used
  1. Document operational details.

Record:

  • deploy path
  • service names
  • rollback command
  • environment file location
  • log locations
  • backup location
  1. Re-run validation after every deployment.

Repeat the same server, app, static, HTTPS, and log checks after each release.

Common Causes

  • Gunicorn not running or failing on startup → Nginx returns 502 or upstream errors → check systemd status, import path, virtualenv, and logs.
  • Nginx points to the wrong socket or port → requests never reach Flask → align proxy_pass with Gunicorn bind settings.
  • Environment variables missing in the service context → app starts with bad config or crashes → define Environment or EnvironmentFile in systemd and reload the daemon.
  • Static root misconfigured → CSS/JS return 404 even though app loads → map /static/ to the correct filesystem directory in Nginx.
  • HTTPS incomplete or broken after certificate install → browser warnings or redirect loops → verify server block order, certificate paths, and redirect logic.
  • Database migrations not applied → runtime 500 errors or missing tables → run migrations and confirm current revision.
  • Socket or directory permissions incorrect → Gunicorn or Nginx cannot access required files → fix ownership, group membership, and mode bits.
  • DNS not updated or pointing to the wrong host → public site fails while localhost works → verify A/AAAA records and name resolution.
  • Uploads stored in a non-served path → media appears broken after deploy → configure separate media storage and Nginx alias or object storage.
  • Logs not configured → incidents are hard to diagnose → enable journald, Gunicorn access/error logs, and Nginx logs.

Debugging Section

Check these in order:

  1. Validate Nginx configuration
    bash
    sudo nginx -t
    
  2. Check service status
    bash
    sudo systemctl status nginx --no-pager
    sudo systemctl status gunicorn --no-pager
    
  3. Read recent logs
    bash
    sudo journalctl -u nginx -n 100 --no-pager
    sudo journalctl -u gunicorn -n 100 --no-pager
    
  4. Inspect effective environment variables
    bash
    sudo systemctl show gunicorn --property=Environment
    
  5. Test Gunicorn directly
    bash
    curl -I http://127.0.0.1:8000
    curl --unix-socket /run/gunicorn.sock http://localhost/
    
  6. Test the public domain
    bash
    curl -I https://your-domain.com
    curl -I https://your-domain.com/static/app.css
    
  7. Verify DNS
    bash
    dig +short your-domain.com
    
  8. Inspect TLS certificate
    bash
    openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null
    
  9. Confirm migration state
    bash
    source /path/to/venv/bin/activate && flask db current
    
  10. Check process and file permissions
    bash
    ps aux | grep gunicorn
    ls -la /run/gunicorn.sock
    ls -la /path/to/app /path/to/app/static /path/to/app/uploads
    

What to look for:

  • import errors
  • missing environment variables
  • 502 Bad Gateway
  • socket permission errors
  • wrong static or upload paths
  • expired or mismatched certificates
  • missing migrations
  • incorrect DNS records

If upstream requests fail, use Fix Flask 502 Bad Gateway (Step-by-Step Guide).

Checklist

  • App runs with production config and debug is disabled.
  • Gunicorn starts successfully and remains active after restart.
  • Nginx config passes validation and reloads cleanly.
  • Domain resolves to the correct server IP.
  • HTTPS certificate is valid and auto-renewal is configured.
  • Nginx proxies requests to Gunicorn correctly.
  • Static files return 200 from Nginx.
  • Media/uploads are stored and served from the intended path or backend.
  • Environment variables are loaded by the running service.
  • Database connection succeeds in production.
  • Latest migrations are applied.
  • Logs are writable and readable in a known location.
  • Monitoring or error tracking is enabled.
  • Backups exist and restore steps are documented.
  • Firewall and exposed ports are limited to required services.
  • A full application smoke test succeeds over the public domain.

FAQ

Q: What are the minimum production components for Flask?
A: A production WSGI server, reverse proxy, environment-based secrets, database configuration, HTTPS, logging, and a basic validation process.

Q: Can I use this checklist for Docker deployments too?
A: Yes, but replace systemd checks with container health, logs, networking, volumes, and restart policy checks.

Q: How often should I run this checklist?
A: Before launch, after every deployment, after infrastructure changes, and during incident review.

Q: What should I verify first if the site is down?
A: Check nginx -t, Nginx and Gunicorn service status, then inspect recent logs and test the local upstream response.

Q: Is HTTPS optional for production?
A: No for public applications. HTTPS should be treated as a standard production requirement.

Final Takeaway

A Flask production deployment is only complete when the entire stack is validated, not just when the app starts.

Use this checklist to verify runtime, proxying, HTTPS, files, database, observability, and recovery before calling the deployment done.