Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
If you're trying to deploy a Flask app with Nginx and Gunicorn in production, this guide shows you the minimal working setup and the exact steps to get it running. The outcome is a Flask application served by Gunicorn, managed by systemd, and exposed safely through Nginx.
Quick Fix / Quick Setup
Use this baseline to get a single-server Flask deployment running quickly:
sudo apt update && sudo apt install -y python3-venv python3-pip nginx
mkdir -p /var/www/flaskapp && cd /var/www/flaskapp
python3 -m venv .venv
. .venv/bin/activate
pip install flask gunicorn
cat > app.py <<'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Flask + Gunicorn + Nginx is working'
EOF
cat > wsgi.py <<'EOF'
from app import app
EOF
sudo tee /etc/systemd/system/flaskapp.service > /dev/null <<'EOF'
[Unit]
Description=Gunicorn for Flask app
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/flaskapp
Environment="PATH=/var/www/flaskapp/.venv/bin"
ExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now flaskapp
sudo tee /etc/nginx/sites-available/flaskapp > /dev/null <<'EOF'
server {
listen 80;
server_name _;
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;
}
}
EOF
sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
sudo nginx -t && sudo systemctl reload nginx
curl -I http://127.0.0.1:8000
curl -I http://localhost
This is the fastest working baseline for a single-server deployment. Replace the sample app, domain, paths, and service name with your real project values before production use.
What’s Happening
- Gunicorn runs the Flask WSGI app as the application server.
- Nginx sits in front as the reverse proxy, handles client connections, and forwards requests to Gunicorn.
- systemd keeps Gunicorn running across reboots and restarts it if the process exits.
- Static and media paths should usually be served by Nginx directly instead of Flask in production.
Step-by-Step Guide
- Install system packagesbash
sudo apt update && sudo apt install -y python3-venv python3-pip nginx - Create the application directorybash
sudo mkdir -p /var/www/flaskapp sudo chown -R $USER:$USER /var/www/flaskapp cd /var/www/flaskapp - Create a virtual environment and install dependenciesbash
python3 -m venv .venv . .venv/bin/activate pip install --upgrade pip pip install flask gunicorn - Create a minimal Flask app
Createapp.py:pythonfrom flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Flask + Gunicorn + Nginx is working"
Createwsgi.py:pythonfrom app import app - Test Gunicorn directly before adding systemd or Nginxbash
. .venv/bin/activate gunicorn --bind 127.0.0.1:8000 wsgi:app
In another shell:bashcurl -I http://127.0.0.1:8000
Stop Gunicorn withCtrl+Cafter the test succeeds. - Create the systemd service
Create/etc/systemd/system/flaskapp.service:ini[Unit] Description=Gunicorn for Flask app After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/var/www/flaskapp Environment="PATH=/var/www/flaskapp/.venv/bin" ExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app Restart=always [Install] WantedBy=multi-user.target - Reload systemd and start the servicebash
sudo systemctl daemon-reload sudo systemctl enable --now flaskapp sudo systemctl status flaskapp --no-pager - Create the Nginx server block
Create/etc/nginx/sites-available/flaskapp:nginxserver { listen 80; server_name _; 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; } } - Enable the sitebash
sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp sudo rm -f /etc/nginx/sites-enabled/default - Validate and reload Nginxbash
sudo nginx -t sudo systemctl reload nginx - Test the app through both layers
Test Gunicorn directly:bashcurl -I http://127.0.0.1:8000
Test through Nginx:bashcurl -I http://localhost - Serve static files directly with Nginx if your app uses them
Add this inside the sameserverblock:nginxlocation /static/ { alias /var/www/flaskapp/static/; }
Ensure the directory exists:bashmkdir -p /var/www/flaskapp/static
Then reload Nginx:bashsudo nginx -t && sudo systemctl reload nginx - Update the Gunicorn app import path for your real project
Examples:iniExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
oriniExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:app
After editing the unit:bashsudo systemctl daemon-reload sudo systemctl restart flaskapp - Add environment variables if required
Option A, inline in the unit:iniEnvironment="FLASK_ENV=production" Environment="SECRET_KEY=replace-me"
Option B, environment file:iniEnvironmentFile=/etc/flaskapp.env
Example/etc/flaskapp.env:bashSECRET_KEY=replace-me DATABASE_URL=postgresql://user:pass@host/dbname
Then apply changes:bashsudo systemctl daemon-reload sudo systemctl restart flaskapp - Add HTTPS only after HTTP works
First confirm:- Gunicorn responds on
127.0.0.1:8000 - Nginx responds on
localhost - your domain points to the server
Then move to TLS setup separately. - Gunicorn responds on
Common Causes
- Wrong WSGI import path → Gunicorn starts or restarts with import errors → fix the module reference such as
wsgi:apporpackage.module:app. - Nginx
proxy_passdoes not match Gunicorn bind target → Nginx returns502 Bad Gatewayor connection refused → make both use the same host/port or socket path. - Incorrect virtual environment path in systemd → Gunicorn binary or Python packages cannot be found → correct
Environment="PATH=..."and the absoluteExecStartpath. - Missing environment variables under systemd → app works manually but fails as a service → add
Environment=orEnvironmentFile=. - Default Nginx site conflicts with the Flask config → requests hit the wrong server block → disable conflicting default configs.
- Static files not mapped in Nginx → CSS, JS, or images return
404→ add alocation /static/block withalias. - Permissions are too restrictive → Gunicorn or Nginx cannot read project files or connect to a socket → fix ownership and mode for the service user.
- Firewall or cloud security rules block traffic → app works locally but not externally → allow inbound
80and443.
Debugging Section
Check Gunicorn service state:
sudo systemctl status flaskapp --no-pager -l
Read recent Gunicorn logs:
sudo journalctl -u flaskapp -n 100 --no-pager
Follow Gunicorn logs live:
sudo journalctl -u flaskapp -f
Validate Nginx syntax:
sudo nginx -t
Check Nginx service status:
sudo systemctl status nginx --no-pager
Read the Nginx error log:
sudo tail -n 100 /var/log/nginx/error.log
Read the Nginx access log:
sudo tail -n 100 /var/log/nginx/access.log
Verify Gunicorn is listening:
sudo ss -ltnp | grep 8000
Test the app without Nginx:
curl -I http://127.0.0.1:8000
Test through Nginx locally:
curl -I http://localhost
Inspect the loaded systemd unit:
sudo systemctl cat flaskapp
Dump active Nginx config:
sudo nginx -T | less
If using UFW, confirm ports are open:
sudo ufw status
Check running Gunicorn processes:
ps aux | grep gunicorn
If using a Unix socket instead of TCP, verify the socket path and permissions:
sudo ls -lah /run/
sudo ss -lx | grep gunicorn
What to look for:
ModuleNotFoundErroror import errors injournalctlconnect() failed (111: Connection refused)in Nginx error logs- missing listener on
127.0.0.1:8000 - wrong
server_nameor duplicate Nginx server blocks innginx -T - file permission errors when Nginx or Gunicorn reads app or socket paths
Checklist
- Flask app starts under Gunicorn without import errors.
- systemd unit is enabled and stays active after restart.
-
curl http://127.0.0.1:8000returns a valid app response. -
nginx -tpasses without warnings or errors. -
curl http://localhostreturns the app through Nginx. - Correct domain or
server_nameis configured for production. - Static file paths are served by Nginx if the app uses them.
- Firewall allows HTTP and HTTPS traffic as required.
- Environment variables are loaded in the systemd service.
- Logs are readable in
journalctland/var/log/nginx/.
Related Guides
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
- Flask Production Checklist (Everything You Must Do)
- Flask systemd + Gunicorn Service Setup
FAQ
Q: Should I use a TCP port or Unix socket for Gunicorn?
A: Either works. TCP on 127.0.0.1:8000 is simpler to debug, while a Unix socket is common for production if permissions are configured correctly.
Q: Why does the app work with flask run but not with Gunicorn?
A: flask run often uses a different environment and is not the same as the production WSGI import path. Validate the exact Gunicorn module reference and systemd environment.
Q: Do I need systemd for Gunicorn?
A: For production on a typical Linux server, yes. systemd provides startup on boot, restart policies, and centralized logs.
Q: Can Nginx serve static files directly?
A: Yes. That is the standard approach in production and reduces load on Flask and Gunicorn.
Q: When should I configure HTTPS?
A: After HTTP deployment is working and the domain points to the server.
Q: Should Gunicorn bind to 0.0.0.0 behind Nginx?
A: Usually no. Bind Gunicorn to 127.0.0.1 or a Unix socket and let Nginx handle public traffic.
Q: How many Gunicorn workers should I start with?
A: Start with 2-4 small workers or 2 * CPU + 1 as a baseline, then tune based on memory usage and request patterns.
Final Takeaway
A reliable Flask production deployment uses Gunicorn for the app process, systemd for process supervision, and Nginx as the public reverse proxy. Build and validate the stack in order—Flask app, Gunicorn, systemd, then Nginx—so deployment issues are isolated quickly and fixed with less guesswork.