Flask Dockerfile for Production with Gunicorn (Multi-Stage)

This Flask Dockerfile installs dependencies in a builder stage and serves the app with Gunicorn on python:3.13-slim as a non-root user. It listens on port 8000 with two worker processes, which suits small APIs and server-rendered sites behind a reverse proxy. Flask's built-in development server is never used in production.

CI status
Build passing
Image size (uncompressed)
124.9 MB
Build time
14 s
Runtime image
python:3.13-slim
Port
8000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN useradd --create-home --uid 10001 app
USER app
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]
.dockerignore
.git
__pycache__/
*.pyc
.venv/
.env
.pytest_cache/
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM python:3.13-slim AS builder

    Dependencies are installed in a separate stage, so pip's leftovers and any build tools never reach the runtime image.

  2. COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

    Copying only requirements.txt first caches this layer until dependencies change. Installing into /install makes the result easy to copy out.

  3. COPY --from=builder /install /usr/local

    Brings Flask, Gunicorn and the rest into the runtime image at the location Python and the PATH already use.

  4. ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

    Avoids writing .pyc files and flushes log output immediately, so request logs appear in docker logs right away.

  5. RUN useradd --create-home --uid 10001 app USER app

    Runs Gunicorn and your code as an unprivileged user with a fixed UID instead of root.

  6. CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]

    Gunicorn loads the app object from app.py, binds to all interfaces and supervises two worker processes, restarting any that crash.

Build and run

terminal
docker build -t flask-app .
docker run --rm -p 8000:8000 flask-app

Build from the project root, then open http://localhost:8000. Configuration such as SECRET_KEY or a database URL should be passed with -e flags when the container starts.

Other Flask Dockerfile variants

Gunicorn with synchronous workers is the standard way to serve Flask and fits most apps. Projects with long-polling or many slow outbound calls may prefer gevent workers or an ASGI server. Verified variants appear in the table below.

VariantRuntime imageImage sizeCI
Python slim + Gunicornpython:3.13-slim124.9 MBPassing

Troubleshooting

Failed to find attribute 'app' in 'app'
Gunicorn imported app.py but found no variable named app. Rename your Flask instance, or change app:app to module:variable to match your code.
ModuleNotFoundError: No module named 'app'
Your entry file has a different name or lives in a package. Point the CMD at the real module path, for example myproject.wsgi:app.
Requests time out with WORKER TIMEOUT in the logs
A request ran longer than Gunicorn's default timeout. Raise it with --timeout, or move slow work to a background job queue.

FAQ

Why not use flask run in the container?
Flask's built-in server is designed for development. It handles one request at a time by default and isn't hardened for production traffic, while Gunicorn manages multiple worker processes.
How many Gunicorn workers should I use?
The file starts with two. A common starting point is two to four per CPU core, adjusted after load testing and within the container's memory limit.
Do I need Nginx in front of Gunicorn?
In production a reverse proxy or load balancer usually handles TLS, slow clients and static files, while Gunicorn focuses on running your Python code.