A builder for a lean, multi-stage Dockerfile for Python web apps — FastAPI, Flask or Django. Choose your Python version, framework and port, and the tool emits an image that builds dependency wheels in one stage and ships a slim runtime that runs as a non-root user.
How it works
The generated Dockerfile uses two stages. The builder stage runs pip wheel to compile every dependency from requirements.txt into a /wheels directory, keeping compilers and headers out of the final image. The runtime stage installs those pre-built wheels with pip install --no-index --find-links=/wheels, sets PYTHONUNBUFFERED=1 and PYTHONDONTWRITEBYTECODE=1, creates a dedicated non-root app user, copies your code, exposes the port and defines an ENTRYPOINT.
Framework start commands
FastAPI is served with uvicorn app.main:app --host 0.0.0.0, Flask with gunicorn --bind 0.0.0.0:PORT app:app, and Django with gunicorn --bind 0.0.0.0:PORT myproject.wsgi:application. Override the start command if your module path differs. The optional HEALTHCHECK calls your /health endpoint using the standard-library urllib, so it adds no extra dependency.
Tips
Keep dependencies in a pinned requirements.txt so builds are reproducible. Add a .dockerignore so caches and secrets are not baked into the image:
__pycache__
*.pyc
.venv
.git
.env
The Debian-based slim base image is chosen deliberately: it works with the prebuilt manylinux wheels most packages ship, avoiding the slow source builds and musl-libc issues common with alpine.
What the generated Dockerfile looks like
A FastAPI app on Python 3.12 might produce:
# Builder stage
FROM python:3.12-slim AS builder
WORKDIR /wheels
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# Runtime stage
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*.whl
COPY --chown=app:app . .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Why two stages for Python
The wheel-build stage may require C headers, compilers, and Rust (for packages like cryptography or pydantic-core). These can add hundreds of megabytes to a dev-sized base image. The runtime stage only needs the precompiled .whl files — the compilers never touch it. The result is a final image that is typically 60–70% smaller than a single-stage build with the same dependencies.
Key environment variables explained
PYTHONUNBUFFERED=1 forces Python to flush stdout and stderr immediately without buffering, which is essential in containers so that log lines appear in real time rather than being batched. PYTHONDONTWRITEBYTECODE=1 prevents Python from writing .pyc files to disk, keeping the container filesystem clean and avoiding permission issues when the source is mounted read-only.
Scaling uvicorn and gunicorn workers
For production workloads, pass the worker count explicitly:
- uvicorn:
--workers 4runs four processes. As a rule of thumb,2 × CPU_count + 1is a common starting point for I/O-heavy apps. - gunicorn:
--workers 4 --worker-class uvicorn.workers.UvicornWorkergives you async uvicorn workers managed by gunicorn’s process supervisor, which handles graceful restarts and worker recycling.
For containerised deployments, set workers to a small fixed number (2–4) and scale via container replicas rather than packing many workers into one container — this keeps memory usage predictable and crash recovery fast.