
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Running Flask’s built-in development server in production is a common mistake that leads to security vulnerabilities, poor performance, and inevitable downtime under load. To properly deploy Flask to production, you must place the application behind a production-grade WSGI server like Gunicorn and reverse proxy it with Nginx for TLS termination and static file handling. This guide provides the exact configuration steps I use when setting up compliant, audit-ready Python environments on Ubuntu servers.
How do you configure Gunicorn to deploy Flask to production?
Gunicorn is the industry-standard WSGI HTTP server for Python applications. Unlike Flask’s development server, it handles concurrency, process management, and graceful restarts. When I harden Ubuntu servers for compliance frameworks like SOC 2 or ISO 27001, Gunicorn’s predictable resource usage and logging capabilities make it auditable and reliable.
Install Gunicorn in an isolated virtual environment
Never install production dependencies globally. Create a dedicated virtual environment to prevent version conflicts and simplify dependency tracking during security audits.
sudo apt update
sudo apt install python3-venv python3-pip -y
mkdir -p /opt/flask-app
cd /opt/flask-app
python3 -m venv venv
source venv/bin/activate
pip install gunicorn flask Create a production-ready Gunicorn configuration
A configuration file is superior to CLI flags because it is version-controlled, reviewable, and consistent across deployments. Save this as /etc/gunicorn/flask-app.py:
# /etc/gunicorn/flask-app.py
bind = "unix:/run/flask-app/flask-app.sock"
workers = 4
worker_class = "gthread"
threads = 2
timeout = 120
accesslog = "/var/log/flask-app/access.log"
errorlog = "/var/log/flask-app/error.log"
loglevel = "info"
user = "flask-app"
group = "www-data" - Unix sockets eliminate TCP overhead and prevent accidental external exposure of the WSGI port.
- Worker count should typically be
(2 × CPU cores) + 1. Adjust based on whether your workload is CPU-bound or I/O-bound. - gthread worker class provides threading within each worker, improving throughput for I/O-heavy Flask apps without the memory cost of additional processes.
How do you create a systemd service for Flask?
Systemd ensures your Flask application starts on boot, restarts on failure, and integrates with journalctl for centralized logging. This is non-negotiable for any system that must pass an infrastructure audit or maintain SLOs.
Define the systemd unit file
Create /etc/systemd/system/flask-app.service with explicit paths and security restrictions:
[Unit]
Description=Flask Application Gunicorn Service
After=network.target
[Service]
User=flask-app
Group=www-data
WorkingDirectory=/opt/flask-app
Environment="PATH=/opt/flask-app/venv/bin"
ExecStart=/opt/flask-app/venv/bin/gunicorn -c /etc/gunicorn/flask-app.py wsgi:app
Restart=always
RestartSec=5
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/run/flask-app /var/log/flask-app
[Install]
WantedBy=multi-user.target Prepare runtime directories and activate the service
sudo mkdir -p /run/flask-app /var/log/flask-app
sudo chown flask-app:www-data /run/flask-app /var/log/flask-app
sudo systemctl daemon-reload
sudo systemctl enable --now flask-app.service
sudo systemctl status flask-app.service The ProtectSystem=strict directive makes the filesystem read-only except for explicitly declared paths. This follows the principle of least privilege and limits blast radius if the application is compromised. For deeper context on service hardening, see my guide on systemd services and timers.
How do you configure Nginx as a reverse proxy for Flask?
Nginx terminates TLS, serves static files directly (bypassing Python entirely), buffers slow clients, and provides rate limiting. Exposing Gunicorn directly to the internet bypasses these protections and is a frequent finding in penetration tests.
Write the Nginx server block
Create /etc/nginx/sites-available/flask-app:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /opt/flask-app/static;
location /static/ {
alias /opt/flask-app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://unix:/run/flask-app/flask-app.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;
proxy_redirect off;
proxy_buffering on;
proxy_request_buffering on;
}
} Enable the site and validate configuration
sudo ln -s /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx Always run nginx -t before reloading. A syntax error in production config can take down all sites on the server. For certificate provisioning, refer to setting up free SSL with Let's Encrypt.
What are the critical security steps when deploying Flask to production?
Security is not optional add-on work; it is foundational to a valid production deployment. Every item below addresses real vulnerabilities I have remediated during audit preparations.
- Dedicated service account: Run Gunicorn as
flask-app, never as root. Restrict file ownership so only this user can write to application directories. - Environment variable isolation: Store secrets in
/etc/flask-app/envwith mode0600, owned byflask-app. Load them viaEnvironmentFile=in systemd, never in source code. - Firewall rules: Allow only ports 80 and 443 inbound. Block direct access to the Gunicorn socket or any debug ports. Use UFW or nftables consistently.
- Disable debug mode: Ensure
FLASK_DEBUG=0andFLASK_ENV=production. Debug mode exposes interactive debuggers that allow arbitrary code execution. - Dependency pinning: Use
pip freeze > requirements.txtor Poetry lock files. Unpinned dependencies introduce supply chain risk and break reproducibility. - Log rotation: Configure logrotate for
/var/log/flask-app/*.log. Unrotated logs consume disk space and may leak sensitive data over time.
| Component | Development Server | Production (Gunicorn + Nginx) |
|---|---|---|
| Concurrency | Single-threaded, single-process | Multi-worker, multi-threaded |
| TLS Support | None (requires ad-hoc certs) | Nginx terminates TLS with modern ciphers |
| Static Files | Served through Python (slow) | Served directly by Nginx (fast) |
| Process Management | Manual restart required | Systemd auto-restart on crash |
| Security Headers | Not configured by default | Nginx adds HSTS, CSP, X-Frame-Options |
| Audit Trail | Console output only | Structured logs via journald + files |
Deploy Flask to Production: Final Checklist and Next Steps
When you deploy Flask to production correctly, the result is a resilient, secure, and observable application that withstands traffic spikes and satisfies compliance requirements. Verify every item on this checklist before considering the deployment complete:
- Gunicorn runs as a non-root user via systemd with
Restart=always - Nginx terminates TLS with modern protocols and forwards headers correctly
- Unix socket connects Nginx to Gunicorn (no exposed TCP ports)
- Secrets are loaded from a protected environment file, not hardcoded
- Debug mode is disabled and
FLASK_ENV=productionis set - Logs are persisted, rotated, and accessible via journalctl
- Firewall allows only ports 80 and 443
- Dependencies are pinned and installed in an isolated virtual environment
If your team needs help designing a production-grade Python deployment pipeline, implementing observability with OpenTelemetry, or preparing infrastructure for SOC 2 or ISO 27001 audits, reach out to discuss your specific requirements. I work with teams across Nepal and globally to build systems that are secure, scalable, and audit-ready from day one.