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

Flask + Docker Production Setup (Complete Guide)

If you're trying to run Flask in production with Docker, this guide shows you how to build, configure, and start the app step-by-step. It covers a minimal production-safe container setup, Gunicorn process configuration, environment handling, persistent storage decisions, and Nginx reverse proxy support.

Quick Fix / Quick Setup

Use this minimal production-style setup first.

Dockerfile

dockerfile
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["gunicorn", "wsgi:app", "--bind", "0.0.0.0:8000", "--workers", "3"]

.dockerignore

gitignore
__pycache__/
*.pyc
*.pyo
*.pyd
venv/
.env
.git

Build and run

bash
docker build -t flask-app .

docker run -d \
  --name flask-app \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file .env \
  flask-app

Test

bash
curl http://127.0.0.1:8000

This gets a Flask app running in a production-style container with Gunicorn. For public traffic, place Nginx in front of the container and terminate HTTPS there. If you need the reverse proxy layer, use Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).

What’s Happening

In production, Flask should not run with the built-in development server inside Docker. Docker packages the app and dependencies into a repeatable image, and Gunicorn serves the application inside the container.

Most failures come from one of these: wrong Gunicorn app target, missing environment variables, binding to the wrong address, port mismatch, or assuming files written inside the container will persist after redeploy.

Step-by-Step Guide

1. Create a production WSGI entrypoint

Create a wsgi.py file at the project root.

If your app uses an application factory:

python
from yourapp import create_app

app = create_app()

If your app already exposes app directly:

python
from yourapp import app

Gunicorn must be able to import the target used in the command:

bash
gunicorn wsgi:app --bind 0.0.0.0:8000 --workers 3

2. Pin production dependencies

Create or update requirements.txt.

txt
Flask==3.0.3
gunicorn==22.0.0
psycopg2-binary==2.9.9

Add only the packages your app actually needs. If you use database migrations, include the migration package as well.

3. Create a production Dockerfile

Use a slim Python image, install dependencies, copy the app, and start Gunicorn.

dockerfile
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["gunicorn", "wsgi:app", "--bind", "0.0.0.0:8000", "--workers", "3"]

If your app needs extra OS libraries, install them in the same apt-get install line.

4. Exclude local and secret files from the image

Create .dockerignore.

gitignore
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
venv/
.env
.git
.gitignore

Do not bake .env into the image.

5. Configure Flask for production via environment variables

Use runtime environment variables instead of hardcoded values.

Example .env:

env
SECRET_KEY=change-me
DATABASE_URL=postgresql://user:password@db-host:5432/appdb
FLASK_ENV=production

Example config usage in Flask:

python
import os

class Config:
    SECRET_KEY = os.environ["SECRET_KEY"]
    SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
    DEBUG = False

For a full environment-variable setup, use Flask Environment Variables and Secrets Setup.

6. Build the image

bash
docker build -t flask-app .

Confirm the build completes without copying secrets or local virtualenv files.

7. Run the container with restart policy and environment file

bash
docker run -d \
  --name flask-app \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file .env \
  flask-app

This publishes container port 8000 to host port 8000.

8. Verify the container is running and reachable

Check container state:

bash
docker ps -a

Test the app:

bash
curl http://127.0.0.1:8000

If the container exits immediately, inspect logs:

bash
docker logs flask-app --tail 100

9. Mount persistent storage if the app writes files

Do not rely on the container filesystem for uploads or generated files that must survive container replacement.

Example with a host-mounted directory:

bash
docker run -d \
  --name flask-app \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file .env \
  -v /srv/flask/uploads:/app/uploads \
  flask-app

Example with a named volume:

bash
docker volume create flask_uploads

docker run -d \
  --name flask-app \
  --restart unless-stopped \
  -p 8000:8000 \
  --env-file .env \
  -v flask_uploads:/app/uploads \
  flask-app

Use external storage for long-term or multi-instance deployments.

10. Put Nginx in front of the container

For public production traffic, use Nginx as the reverse proxy and terminate HTTPS there.

Basic host-level Nginx upstream example:

nginx
server {
    listen 80;
    server_name example.com;

    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;
    }
}

Then test and reload:

bash
sudo nginx -t
sudo systemctl reload nginx

If Nginx returns a 502, use Fix Flask 502 Bad Gateway (Step-by-Step Guide).

11. Add a health endpoint

Create a lightweight health route.

python
@app.get("/health")
def health():
    return {"status": "ok"}, 200

Validate it:

bash
curl http://127.0.0.1:8000/health

Use this during deploy validation and reverse-proxy checks.

12. Run database migrations explicitly

Do not assume schema changes apply automatically.

Example:

bash
docker exec -it flask-app flask db upgrade

If migrations are part of deployment, run them as a one-off task before shifting production traffic.

13. Validate startup logs and runtime behavior

Check startup logs:

bash
docker logs flask-app --tail 100

Look for:

  • successful Gunicorn worker boot
  • correct app import path
  • no missing environment variables
  • no database connection failures
  • no permission errors on mounted paths

14. Use Compose when the stack grows

A single-container setup is fine for a simple app. If you add Nginx, PostgreSQL, Redis, workers, or scheduled jobs, move to Compose for service orchestration.

See Flask + Docker Compose Production Setup.

Common Causes

  • Wrong Gunicorn target → Gunicorn cannot import the Flask app object → fix the module path, such as wsgi:app or package.wsgi:app
  • Binding to 127.0.0.1 inside the container → app is unreachable from outside the container → bind Gunicorn to 0.0.0.0:8000
  • Using flask run in production → unstable or unsafe behavior → replace it with Gunicorn
  • Missing environment variables → app crashes at startup or loads wrong config → pass values with --env-file or deployment environment injection
  • Copying .env into the image → secrets leak into image layers → exclude .env with .dockerignore
  • Using the container filesystem for uploads → files disappear after redeploy → use named volumes or external object storage
  • Port mismatch → Docker publishes one port but Gunicorn listens on another → align EXPOSE, --bind, and -p
  • Missing build dependencies → pip install fails for compiled packages → install required OS packages during image build
  • Database host set to localhost incorrectly → the container cannot reach the real database → use the actual external host or service name
  • Nginx upstream points to the wrong port → proxy returns 502 → verify container listener and proxy target

Debugging Section

Check container state:

bash
docker ps -a

Inspect logs:

bash
docker logs flask-app --tail 100

Open a shell inside the container:

bash
docker exec -it flask-app sh

Inspect environment variables:

bash
docker exec flask-app env | sort

Inspect container metadata:

bash
docker inspect flask-app

Inspect image metadata:

bash
docker image inspect flask-app

Test the app directly on the published port:

bash
curl http://127.0.0.1:8000

Check a specific environment variable:

bash
docker exec -it flask-app python -c "import os; print(os.getenv('FLASK_ENV'))"

If the app exits immediately, start a shell instead of the default command:

bash
docker run --rm -it --entrypoint sh flask-app

What to look for:

  • ModuleNotFoundError or ImportError → wrong app import path or missing package
  • KeyError for config values → missing environment variables
  • bind errors → wrong port or process already using the port
  • connection refused to the database → wrong DB host, port, credentials, or firewall
  • missing files on mounted paths → incorrect volume target or permissions

If Nginx is involved, test both layers separately:

  1. Curl the container port directly
  2. Curl through Nginx

This isolates whether the failure is in the app container or the reverse proxy.

Checklist

  • Docker image builds successfully without copying secrets into the image
  • Gunicorn starts the correct Flask app target
  • The container listens on 0.0.0.0:8000
  • Required environment variables are provided at runtime
  • Debug mode is disabled in production
  • Static and media storage paths are handled correctly for production
  • Persistent data is stored in volumes or external services
  • The app responds successfully on the published port
  • Nginx can proxy to the container if used
  • Logs show clean startup with no import, permission, or database errors

For final production validation, use Flask Production Checklist (Everything You Must Do).

FAQ

Can I deploy Flask with only Docker and no Docker Compose?

Yes. A single-container setup works. Compose becomes useful when you add Nginx, PostgreSQL, Redis, or worker services.

Should I expose Flask directly to the internet from the container?

Not recommended. Put Nginx or another reverse proxy in front for TLS and request handling.

What is the correct Gunicorn bind address in Docker?

Use 0.0.0.0 with the internal port, such as 0.0.0.0:8000.

Where should secrets go in a Docker-based Flask deployment?

Inject them at runtime through environment variables, secret managers, or deployment platform configuration. Do not bake them into the image.

Why does my app work locally but fail in the container?

Usually due to an incorrect import path, missing dependencies, missing environment variables, or assumptions about local files and services.

Final Takeaway

A production Flask Docker setup is mainly about using Gunicorn correctly, injecting runtime configuration safely, and avoiding ephemeral-container assumptions. If the app target, bind address, environment variables, reverse proxy wiring, and storage strategy are correct, the setup is usually stable and repeatable.