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:
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
- Confirm the application is deployed in the expected path
Verify the project directory, ownership, and files:bashls -lah /path/to/project sudo chown -R deployuser:deployuser /path/to/project - Verify the virtual environment and installed packages
Activate the environment and check package health:bashsource /path/to/venv/bin/activate pip check pip freeze | grep -E 'Flask|gunicorn' - Confirm the Gunicorn app entry point
Make sure the configured target matches your app structure.
Examples:bashwsgi:app
or:bash'wsgi:create_app()' - Test Gunicorn manually before using systemd or Nginx
Start the app directly:bashsource /path/to/venv/bin/activate gunicorn -b 127.0.0.1:8000 wsgi:app
In another terminal:bashcurl -I http://127.0.0.1:8000 - Verify production environment variables
Confirm required variables exist:bashprintenv | grep -E 'SECRET_KEY|DATABASE|FLASK|REDIS|API'
If using systemd, check the unit file or EnvironmentFile:bashsystemctl cat gunicorn - Validate the systemd service file
ConfirmWorkingDirectory,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 - Reload systemd and restart Gunicorn
Apply changes and enable startup on boot:bashsudo systemctl daemon-reload sudo systemctl restart gunicorn sudo systemctl enable gunicorn - Check Gunicorn health and logs
Validate service state:bashsudo systemctl status gunicorn sudo journalctl -u gunicorn -n 100 --no-pager
Look for:- import errors
- missing packages
- bad environment variables
- permission failures
- 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. - Validate Nginx configuration
Test and reload:bashsudo nginx -t sudo systemctl reload nginx - Confirm the Nginx server block
Verifyserver_name, reverse proxy, headers, and static file locations.
Example:nginxserver { 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; } } - Verify DNS
Confirm the domain resolves to the server IP:bashdig your-domain.com +short dig www.your-domain.com +short - Test local HTTP response through Nginx
Check Nginx routing locally:bashcurl -I http://localhost curl -I -H 'Host: your-domain.com' http://127.0.0.1 - Validate HTTPS
Confirm the certificate, TLS listener, and public response:bashcurl -I https://your-domain.com sudo certbot certificates sudo systemctl status certbot.timer - Test database connectivity
Run an app or migration command inside the production environment:bashsource /path/to/venv/bin/activate flask db current
Or test from Python:bashpython -c "from yourapp import app; print('app import ok')" - Apply pending migrations
Use the project’s migration command before launch:bashflask db upgrade - Verify static files
Confirm the Nginxaliaspath matches the filesystem and files are readable:bashls -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. - Verify media or upload directories
Ensure directories exist and are writable by the application user:bashmkdir -p /path/to/project/uploads sudo chown -R www-data:www-data /path/to/project/uploads ls -lah /path/to/project/uploads - Check firewall rules
Allow public web traffic and keep internal app ports private:bashsudo ufw status sudo ufw allow 80/tcp sudo ufw allow 443/tcp - Confirm services start on boot
Check enabled state:bashsudo systemctl is-enabled gunicorn sudo systemctl is-enabled nginx
Optional reboot validation:bashsudo reboot - Verify logging, monitoring, and backups
At minimum, confirm logs are accessible and database backups exist.bashsudo journalctl -u gunicorn -n 50 --no-pager sudo journalctl -u nginx -n 50 --no-pager - Perform an external end-to-end test
Validate:- home page
- login route
- one database-backed route
- static assets
- form submission
- upload flow if used
- 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_passtarget identical. - Environment variables missing → App starts with missing secrets or database settings and fails at import or runtime → Load variables via systemd
Environment=orEnvironmentFile=and restart Gunicorn. See Flask Environment Variables Not Loading in Production. - Static files not served → Nginx
aliaspath 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
80and443and verify cloud firewall rules.
Debugging Section
Check service and application state:
sudo systemctl status gunicorn
sudo systemctl status nginx
sudo systemctl daemon-reload
systemctl cat gunicorn
sudo nginx -t
Check logs:
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:
ss -ltnp | grep -E ':80|:443|:8000'
ss -lx | grep gunicorn
Test the upstream directly:
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:
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 gunicornshows the service is active. - Gunicorn bind target matches the Nginx upstream target.
-
nginx -tpasses 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.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Production Checklist (Everything You Must Do)
- Flask Environment Variables Not Loading in Production
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.