Blog
3 min read

How to Deploy a Flask App to Production

Flask's built-in server is for development only. How to deploy a Flask app properly: the app factory and config from environment variables, Gunicorn, a reverse proxy with HTTPS, running it as a systemd service, a Docker option, and the mistakes that cause 502s.

When you run flask run (or app.run()), Flask prints:

WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.

Take it at its word. Here's how to deploy properly. (Flask vs Django vs FastAPI)

The production setup

Browser → Caddy/Nginx (HTTPS) → Gunicorn (several workers) → your Flask app

Gunicorn is a production WSGI server: it runs multiple copies of your app to handle requests in parallel and restarts workers that crash.

Step 1: configuration from the environment

Don't hardcode secrets or debug mode:

# app.py
import os
from flask import Flask

def create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
    app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]
    # register blueprints, extensions...
    return app

app = create_app()
  • Never run with debug=True in production — the debugger lets anyone who sees an error page run Python code on your server.
  • SECRET_KEY signs session cookies; it must be long, random and secret. (Environment variables explained)

Step 2: dependencies

pip install gunicorn
pip freeze > requirements.txt

(requirements.txt explained)

Step 3: try Gunicorn locally

gunicorn --bind 127.0.0.1:8000 --workers 3 app:app

app:app means "the app object in app.py". A common rule of thumb is 2–4 workers per CPU core.

Step 4: on the server

git clone https://github.com/you/myflask.git /srv/myflask
cd /srv/myflask
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Create /srv/myflask/.env with your secrets, readable only by the app user (chmod 600). (Python virtual environments)

Step 5: run as a systemd service

# /etc/systemd/system/myflask.service
[Unit]
Description=My Flask app
After=network.target

[Service]
User=app
WorkingDirectory=/srv/myflask
EnvironmentFile=/srv/myflask/.env
ExecStart=/srv/myflask/.venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 app:app
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myflask
journalctl -u myflask -f

(What is systemd?)

Step 6: HTTPS

With Caddy:

example.com {
    reverse_proxy 127.0.0.1:8000
}

Caddy gets the certificate automatically. (What is Caddy?) With Nginx, use proxy_pass http://127.0.0.1:8000; and Certbot. (Nginx reverse proxy config)

Tell Flask it's behind a proxy so it sees the real scheme and client IP:

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)

Alternative: Docker

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nobody
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "app:app"]

Note 0.0.0.0 inside a container. (Dockerfile explained)

Troubleshooting

  • 502 Bad Gateway — Gunicorn isn't running or isn't on the port the proxy expects. Check journalctl -u myflask. (502 Bad Gateway)
  • Worker timeout — a request took longer than Gunicorn's default 30 seconds. Make it faster or move the work to a background job. (504 Gateway Timeout)
  • ModuleNotFoundError — the service uses a different Python than where you installed packages; point ExecStart at the venv's gunicorn. (ModuleNotFoundError)
  • Static files missing — serve static/ from the proxy, or use WhiteNoise.

EasySpawn runs your Flask app behind HTTPS with Gunicorn, Postgres and daily backups set up — push your code and it's live. See how it works or join the waitlist.

Related: Flask vs Django vs FastAPI · How to Deploy a Django App · Deploy a FastAPI App to Production · How to Run a Python Script 24/7

Keep reading