Dockerfile for a Python Application

Building a production Python Docker image requires understanding several Python-specific considerations that don't apply to other languages. Python's handling of bytecode files, its virtual environment isolation, and its output buffering behavior all need explicit configuration in a container context. This example demonstrates the patterns that the Python community has settled on for production Dockerfiles. The two ENV variables at the top address Python's default behaviors that work poorly in containers. PYTHONDONTWRITEBYTECODE=1 prevents Python from writing .pyc bytecode cache files to disk — in a container, these files add unnecessary image size and serve no benefit since the container is rebuilt rather than restarted. PYTHONUNBUFFERED=1 tells Python to write stdout and stderr output immediately rather than buffering it, which is critical for log visibility in container environments where you expect to see log messages in real time via docker logs or a log aggregation platform. Non-root user setup: the addgroup and adduser commands create a system user (--system means no password, no shell, no home directory) that the application runs as. Running as root inside a container is a security risk because it means container escape vulnerabilities (rare but not impossible) land the attacker with root on the host. The --chown=appuser:appgroup flag on the COPY instruction transfers ownership of all copied files to the application user. Dependency layer ordering follows the same logic as Node.js Dockerfiles: copy requirements.txt first, run pip install, then copy the rest of the application. Since requirements.txt changes far less frequently than application code, this ordering ensures the slow pip install layer is cached and reused on the majority of builds. python:3.12-slim vs python:3.12-alpine: slim images are based on Debian and include glibc, while Alpine images use musl libc. Many Python packages with native C extensions (numpy, cryptography, psycopg2) compile correctly on Debian/glibc but fail to install or require complex workarounds on Alpine/musl. For Python, slim is usually the better production choice unless image size is a critical constraint. Gunicorn configuration: CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"] starts the Gunicorn WSGI server with 2 worker processes. The standard recommendation for CPU-bound applications is 2× number of CPU cores + 1 workers. For I/O-bound web applications (most web APIs), you can use more workers with --worker-class gevent or uvicorn.workers.UvicornWorker for async workers. HEALTHCHECK using curl: this example uses curl to check the /health endpoint, which requires curl to be installed in the image. If curl isn't available, use Python itself: CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]. Tips: use Docker build secrets (--secret flag) or multi-stage builds to access private PyPI registries during the pip install step without embedding credentials in the final image or its build history.

Example
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser

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

COPY --chown=appuser:appgroup . .

USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]
[ open in Dockerfile Generator → ]

FAQ

What does PYTHONDONTWRITEBYTECODE do?
It prevents Python from writing .pyc bytecode cache files to disk. In a container, these files add unnecessary size and are regenerated each time the container starts anyway.
Should I use python:slim or python:alpine?
python:slim (based on Debian) is generally preferred over alpine for Python because many Python packages compile native extensions that require glibc, which Alpine (using musl libc) does not provide without extra workarounds.
How do I reduce pip install time in CI/CD?
Use --no-cache-dir in pip install to avoid storing the pip cache in the image. For CI speed, mount the pip cache as a Docker build cache using --mount=type=cache,target=/root/.cache/pip.

Related Examples