A practical, copy-pasteable checklist for taking a Flask app to production without forgetting the things that cause 2 a.m. incidents — process supervision, HTTPS, secrets, backups, and monitoring.
Copy the list below into a GitHub issue or PR and tick items off as you go. Each section links to a step-by-step guide on flask-deployment.com for the details.
📖 Prefer an interactive version? See the full production checklist.
- Provisioned a server (or PaaS) and can reach it over SSH. → Ubuntu VPS guide
- Created a non-root user to own and run the app.
- System packages updated (
sudo apt update && sudo apt upgrade). - Firewall allows only
22,80, and443. → Security checklist - Python virtual environment created and dependencies pinned in
requirements.txt.
-
FLASK_CONFIG=productionandFLASK_DEBUG=0(debug is off). → Config basics - A strong, unique
SECRET_KEYloaded from the environment, not source. → Env vars & secrets - Per-environment config classes (dev / staging / production). → Config environments
- Secrets stored in a systemd
EnvironmentFile(chmod 600) or a secret manager — never committed.
- App served by Gunicorn, not the Flask dev server. → Gunicorn vs Waitress
- Correct
module:appimport path verified (gunicorn wsgi:app). - Worker count and class tuned for the workload. → Gunicorn tuning
- Bound to
127.0.0.1/socket (not0.0.0.0) behind the proxy.
- systemd unit runs Gunicorn as the app user with the right
WorkingDirectory. → systemd + Gunicorn -
Restart=alwaysand the service isenabled(starts on boot). - Graceful reload wired (
ExecReload=/bin/kill -s HUP $MAINPID). → Zero-downtime deploys -
systemctl statusisactive (running)and survives a reboot.
- Nginx proxies to the Gunicorn socket/port;
proxy_passmatches the bind exactly. → Nginx + Gunicorn - Correct
server_name; default site disabled. - Forwarded headers set (
Host,X-Forwarded-For,X-Forwarded-Proto). -
nginx -tpasses and Nginx is reloaded. → Nginx reverse proxy explained
- DNS A/AAAA records point to the server. → Domain & DNS
- TLS certificate issued via Certbot/Let's Encrypt. → HTTPS setup
- HTTP redirects to HTTPS; certificate auto-renewal tested (
certbot renew --dry-run). - Secure cookies enabled (
SESSION_COOKIE_SECURE,HttpOnly,SameSite).
- Production database provisioned with a dedicated, least-privilege user. → Flask + PostgreSQL
-
DATABASE_URLinjected via environment. - Migrations applied (
flask db upgrade) — neverflask db migrateon the server. → Migrations fix - Connection pool sized sensibly relative to Gunicorn workers.
- Static files served by Nginx, not Flask. → Static & media in production
-
location /static/(and media) configured with correct paths/permissions. - User uploads stored on a persistent path outside the code directory. → Static vs media explained
- Debug mode disabled; no stack traces exposed to users. → Security checklist
- Gunicorn/app runs as a non-root user.
- Security headers and request size limits set in Nginx.
- Dependencies scanned (
pip-audit) and kept up to date.
- Centralized logs (journald and/or files with rotation). → Production logging
- Error tracking configured (e.g. Sentry). → Monitoring & error tracking
- Uptime/health check hitting a
/healthzendpoint. → Monitoring checklist - Automated database + media backups, with a tested restore. → Backup & recovery
-
curl -I https://your-domainreturns200/expected status over HTTPS. - Gunicorn reachable directly on its socket/port (rules out proxy issues). → Fix 502
- Key user flows work against production config and data.
- Logs are clean of errors after a few real requests.
- Flask Production Starter — a ready-made baseline that satisfies most of this list out of the box.
- Awesome Flask Deployment — curated deployment resources.
MIT — see LICENSE. Reuse freely; a link back to flask-deployment.com is appreciated.