A solid Python Dockerfile is shorter than the Node equivalent, but it has a couple of Python-specific gotchas: pip caches you don't want in the image, native-wheel mismatches with Alpine, and PYTHONUNBUFFERED — without it, print() output doesn't reach docker logs until the buffer flushes (which is approximately never on a short-lived script).
A working Dockerfile
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
USER 1000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "myapp.wsgi:application"]Build and run:
docker build -t my-py-app .
docker run -d --name my-py-app -p 8000:8000 my-py-appFor an async stack (FastAPI / Starlette), swap the CMD:
CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]Pick the right Python image
python:3.13-slim— Debian-slim, glibc, all native wheels work. The default I reach for.python:3.13— full Debian, much bigger but includes build tools. Use in a build stage if you need to compile native dependencies; almost never as a runtime image.python:3.13-alpine— Alpine + musl. Smaller (~50 MB vs ~150 MB) but trips up many scientific Python packages (NumPy, SciPy, Pandas, cryptography) that ship glibc wheels only. Use only if you've verified your dependencies install cleanly.
Pin to a specific minor version (3.13), not latest. The Python release cadence is yearly; minor versions change behavior.
The four env vars worth setting
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PIP_NO_CACHE_DIR=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1PYTHONUNBUFFERED=1— Python's stdout/stderr are line-buffered when attached to a TTY and block-buffered when attached to a pipe (whichdocker logsis). Without this, yourprint()and logging output can sit in the buffer for minutes. Set it.PYTHONDONTWRITEBYTECODE=1— skip writing.pycfiles. They aren't useful inside a container that gets rebuilt anyway and they add noise.PIP_NO_CACHE_DIR=1— pip caches downloaded wheels. In an image, that cache is wasted space. Disable it.PIP_DISABLE_PIP_VERSION_CHECK=1— stops the "you have pip X, latest is Y" warning during every install. Cosmetic but cleaner logs.
Dependency-cache layer pattern
Same idea as Node: copy requirements.txt (or pyproject.toml / poetry.lock) first, install, then copy the source. Source edits don't invalidate the dependency layer.
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .For Poetry, replace with:
COPY pyproject.toml poetry.lock ./
RUN pip install poetry && \
poetry config virtualenvs.create false && \
poetry install --no-interaction --no-root --only mainFor uv (rapidly gaining adoption):
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-devMulti-stage for compiled dependencies
If you need gcc, g++, or other build tools to compile a wheel (some C extensions), do it in a build stage and copy just the installed packages into the runtime stage:
# Build stage
FROM python:3.13-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ libpq-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt ./
RUN pip install --user --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 && \
rm -rf /var/lib/apt/lists/*
COPY --from=build /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
WORKDIR /app
COPY . .
USER 1000
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "myapp.wsgi:application"]pip install --user installs into /root/.local; the runtime stage copies just that directory plus the runtime libraries (libpq5 for Postgres clients, libffi8 for some crypto libs, etc.). Build toolchain is left behind.
Run as non-root
Python's base images don't ship a non-root user, so either pass USER 1000 (any unprivileged UID works) or RUN useradd -m app && USER app. The simple form:
USER 1000For paths that need to be writable by that UID, set permissions before the USER switch:
RUN mkdir -p /app/logs && chown -R 1000:1000 /app
USER 1000Django: collectstatic in the build, migrate at startup
# In Dockerfile:
RUN python manage.py collectstatic --noinput
# At runtime (in your entrypoint or compose):
python manage.py migrate
gunicorn -w 4 -b 0.0.0.0:8000 myapp.wsgi:applicationcollectstatic runs at build time so the image ships with the gathered static files. migrate runs at startup so it has a database to talk to (which doesn't exist at build time).
.dockerignore for Python
__pycache__
*.pyc
*.pyo
.venv
venv
.env
.env.local
*.log
.git
.gitignore
.pytest_cache
.mypy_cache
.ruff_cache
htmlcov
.coverage
Dockerfile
.dockerignore
Skipping __pycache__ and *.pyc is critical — those are platform-specific and shouldn't ship to the image.
Compose: Python + Postgres
services:
web:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://postgres:hello@db:5432/myapp
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: hello
POSTGRES_DB: myapp
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pg-data:Common pitfalls
- No
PYTHONUNBUFFERED. Logs don't appear indocker logsuntil the buffer flushes. Always set it. - Alpine + scientific packages. NumPy / SciPy / Pandas don't ship musl wheels; pip falls back to source builds that take 10+ minutes and may fail. Use
slimfor scientific Python. - Copying source before
pip install. Every source edit busts the dependency cache. pip installwithout--no-cache-dir. The wheel cache stays in the image. Set the env var or pass the flag.- Forgetting to install
libpq5(or similar) in the runtime stage. If you usedpsycopg2(compiled againstlibpq) in the build stage and didn't install the runtime library in the final stage, you'll get an import error on first start. - Running as root. Add
USER 1000.
What to do next
- How to Write a Dockerfile — fundamentals.
- Docker Image Size Optimization — going smaller.
- How to Run PostgreSQL in Docker — pair this with a database.
- Docker Compose: Getting Started — wiring Python + DB together.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Official Python image — Docker Hubhub.docker.com
- Python command-line optionsdocs.python.org

