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:
# 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
- Confirm the app runs with production settings only.
Activate the correct environment and start the app with production config.bashsource /path/to/venv/bin/activate python -c "from yourapp import app; print(app)"
Validate that debug mode is off in your config:pythonDEBUG = False TESTING = False - Disable development mode.
Do not rely onFLASK_ENV=developmentor debug mode in production.bashunset FLASK_ENV
If using environment variables:iniFLASK_DEBUG=0 - 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" - Verify the WSGI entry point.
Confirm Gunicorn can import the app module.bashsource /path/to/venv/bin/activate gunicorn --bind 127.0.0.1:8000 wsgi:app
Common valid patterns:bashgunicorn wsgi:app gunicorn app:app gunicorn 'yourapp:create_app()' - 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:bashsudo systemctl daemon-reload sudo systemctl enable --now gunicorn - Confirm Gunicorn is listening on the expected socket or port.
For TCP:bashss -ltnp | grep ':8000' curl -I http://127.0.0.1:8000
For Unix socket:bashsudo ss -lx | grep gunicorn ls -la /run/gunicorn.sock curl --unix-socket /run/gunicorn.sock http://localhost/ - Configure Nginx as the reverse proxy.
Example Nginx server block:nginxserver { 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; } } - Validate and reload Nginx only after config passes.bash
sudo nginx -t sudo systemctl reload nginx - 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. - Enable HTTPS.
If using Certbot with Nginx:
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
Test the result:
curl -I https://your-domain.com
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null
- Verify certificate renewal.
sudo certbot renew --dry-run
- Serve static files directly from Nginx.
Confirm the static directory exists and contains files:
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.
- Handle media or uploads separately from static files.
Do not mix build-time static assets and runtime uploads.
Example:
location /uploads/ {
alias /var/www/yourapp/uploads/;
}
Validate:
ls -la /var/www/yourapp/uploads
- Apply database migrations before serving traffic.
source /path/to/venv/bin/activate
flask db upgrade
flask db current
- Verify database connectivity from the app host.
Confirm credentials, host, port, TLS, and network access are correct.
Example environment:
DATABASE_URL=postgresql://user:pass@db-host:5432/dbname
If needed, test with the database client directly.
- Set production security configuration.
At minimum, use a real secret key and secure cookie settings.
Example:
SECRET_KEY = os.environ["SECRET_KEY"]
SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
- Enable persistent logging.
Check Gunicorn and Nginx logs are available and readable.
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.
- Add a health check endpoint.
Example Flask route:
@app.route("/health")
def health():
return {"status": "ok"}, 200
Validate:
curl -I https://your-domain.com/health
- Configure monitoring and alerting.
At minimum, monitor:
- uptime
- 5xx response rate
- CPU and memory
- disk space
- certificate expiry
- Create backups and verify restore capability.
Back up:
- database
- uploaded files
- critical config
The checklist is incomplete until restore steps are documented and tested.
- Restrict public network exposure.
Only expose required ports.
Typical public ports:
80443
Internal-only services should not be publicly reachable.
- 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
- Document operational details.
Record:
- deploy path
- service names
- rollback command
- environment file location
- log locations
- backup location
- 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_passwith Gunicorn bind settings. - Environment variables missing in the service context → app starts with bad config or crashes → define
EnvironmentorEnvironmentFilein 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:
- Validate Nginx configurationbash
sudo nginx -t - Check service statusbash
sudo systemctl status nginx --no-pager sudo systemctl status gunicorn --no-pager - Read recent logsbash
sudo journalctl -u nginx -n 100 --no-pager sudo journalctl -u gunicorn -n 100 --no-pager - Inspect effective environment variablesbash
sudo systemctl show gunicorn --property=Environment - Test Gunicorn directlybash
curl -I http://127.0.0.1:8000 curl --unix-socket /run/gunicorn.sock http://localhost/ - Test the public domainbash
curl -I https://your-domain.com curl -I https://your-domain.com/static/app.css - Verify DNSbash
dig +short your-domain.com - Inspect TLS certificatebash
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null - Confirm migration statebash
source /path/to/venv/bin/activate && flask db current - Check process and file permissionsbash
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
200from 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.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
- Flask Deployment Checklist
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.