Reference Guide Detailed deployment notes with production context and concrete examples.

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:

bash
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

  1. Install system packages
    bash
    sudo apt update && sudo apt install -y python3-venv python3-pip nginx
    
  2. Create the application directory
    bash
    sudo mkdir -p /var/www/flaskapp
    sudo chown -R $USER:$USER /var/www/flaskapp
    cd /var/www/flaskapp
    
  3. Create a virtual environment and install dependencies
    bash
    python3 -m venv .venv
    . .venv/bin/activate
    pip install --upgrade pip
    pip install flask gunicorn
    
  4. Create a minimal Flask app
    Create app.py:
    python
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/")
    def hello():
        return "Flask + Gunicorn + Nginx is working"
    

    Create wsgi.py:
    python
    from app import app
    
  5. Test Gunicorn directly before adding systemd or Nginx
    bash
    . .venv/bin/activate
    gunicorn --bind 127.0.0.1:8000 wsgi:app
    

    In another shell:
    bash
    curl -I http://127.0.0.1:8000
    

    Stop Gunicorn with Ctrl+C after the test succeeds.
  6. 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
    
  7. Reload systemd and start the service
    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now flaskapp
    sudo systemctl status flaskapp --no-pager
    
  8. Create the Nginx server block
    Create /etc/nginx/sites-available/flaskapp:
    nginx
    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;
        }
    }
    
  9. Enable the site
    bash
    sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
    sudo rm -f /etc/nginx/sites-enabled/default
    
  10. Validate and reload Nginx
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  11. Test the app through both layers
    Test Gunicorn directly:
    bash
    curl -I http://127.0.0.1:8000
    

    Test through Nginx:
    bash
    curl -I http://localhost
    
  12. Serve static files directly with Nginx if your app uses them
    Add this inside the same server block:
    nginx
    location /static/ {
        alias /var/www/flaskapp/static/;
    }
    

    Ensure the directory exists:
    bash
    mkdir -p /var/www/flaskapp/static
    

    Then reload Nginx:
    bash
    sudo nginx -t && sudo systemctl reload nginx
    
  13. Update the Gunicorn app import path for your real project
    Examples:
    ini
    ExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
    

    or
    ini
    ExecStart=/var/www/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:app
    

    After editing the unit:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart flaskapp
    
  14. Add environment variables if required
    Option A, inline in the unit:
    ini
    Environment="FLASK_ENV=production"
    Environment="SECRET_KEY=replace-me"
    

    Option B, environment file:
    ini
    EnvironmentFile=/etc/flaskapp.env
    

    Example /etc/flaskapp.env:
    bash
    SECRET_KEY=replace-me
    DATABASE_URL=postgresql://user:pass@host/dbname
    

    Then apply changes:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart flaskapp
    
  15. 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.

Common Causes

  • Wrong WSGI import path → Gunicorn starts or restarts with import errors → fix the module reference such as wsgi:app or package.module:app.
  • Nginx proxy_pass does not match Gunicorn bind target → Nginx returns 502 Bad Gateway or 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 absolute ExecStart path.
  • Missing environment variables under systemd → app works manually but fails as a service → add Environment= or EnvironmentFile=.
  • 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 a location /static/ block with alias.
  • 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 80 and 443.

Debugging Section

Check Gunicorn service state:

bash
sudo systemctl status flaskapp --no-pager -l

Read recent Gunicorn logs:

bash
sudo journalctl -u flaskapp -n 100 --no-pager

Follow Gunicorn logs live:

bash
sudo journalctl -u flaskapp -f

Validate Nginx syntax:

bash
sudo nginx -t

Check Nginx service status:

bash
sudo systemctl status nginx --no-pager

Read the Nginx error log:

bash
sudo tail -n 100 /var/log/nginx/error.log

Read the Nginx access log:

bash
sudo tail -n 100 /var/log/nginx/access.log

Verify Gunicorn is listening:

bash
sudo ss -ltnp | grep 8000

Test the app without Nginx:

bash
curl -I http://127.0.0.1:8000

Test through Nginx locally:

bash
curl -I http://localhost

Inspect the loaded systemd unit:

bash
sudo systemctl cat flaskapp

Dump active Nginx config:

bash
sudo nginx -T | less

If using UFW, confirm ports are open:

bash
sudo ufw status

Check running Gunicorn processes:

bash
ps aux | grep gunicorn

If using a Unix socket instead of TCP, verify the socket path and permissions:

bash
sudo ls -lah /run/
sudo ss -lx | grep gunicorn

What to look for:

  • ModuleNotFoundError or import errors in journalctl
  • connect() failed (111: Connection refused) in Nginx error logs
  • missing listener on 127.0.0.1:8000
  • wrong server_name or duplicate Nginx server blocks in nginx -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:8000 returns a valid app response.
  • nginx -t passes without warnings or errors.
  • curl http://localhost returns the app through Nginx.
  • Correct domain or server_name is 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 journalctl and /var/log/nginx/.

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.