Docker Images for Production

This guide uses uv as the package manager with pyproject.toml. See the uv Docker integration guide for comprehensive documentation.

Multi-Stage Build with UV

This is the recommended approach for production. It separates dependency installation from project installation to maximize Docker layer caching:

# Dockerfile
FROM python:3.13-slim-bookworm AS builder

ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

# Install dependencies only (cached layer)
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project

# Copy source code
COPY . /app

# Sync project (includes the application itself)
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

# Final stage
FROM python:3.13-slim-bookworm

ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv

# Make sure scripts in .venv are in PATH
ENV PATH="/app/.venv/bin:$PATH"

# Copy application code
COPY --from=builder /app /app

EXPOSE 8000

# Disable access log for production falling back to default stario logging
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]

Single-Stage Build (Development-Friendly)

For simpler setups or development environments:

FROM python:3.13-slim-bookworm

ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

# Copy project files
COPY pyproject.toml uv.lock ./

# Install dependencies and project
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

# Copy application
COPY . .

EXPOSE 8000

# Disable access log for production falling back to default stario logging
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]

Security Best Practices

If you're running a production environment, you should run the container as a non-root user. This is a good practice for security and isolation. You can do this by adding a USER directive to the Dockerfile.

FROM python:3.13-slim-bookworm

ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1

WORKDIR /app

# Don't run as root
RUN useradd -m -u 1000 appuser

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project

COPY --chown=appuser:appuser . .

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

USER appuser
EXPOSE 8000

ENV PATH="/app/.venv/bin:$PATH"

# Disable access log for production falling back to default stario logging
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]

Non-Editable Install (Smaller Final Image)

For production where you don't need editable installs:

FROM python:3.13-slim-bookworm AS builder

ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1

WORKDIR /app

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --no-editable

COPY . /app

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-editable

# Final stage - only copy venv, not source
FROM python:3.13-slim-bookworm

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY --from=builder /app/.venv /app/.venv

ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8000

# Disable access log for production falling back to default stario logging
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]

Docker Compose for Production

version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql+asyncpg://user:pass@db:5432/app
      LOG_LEVEL: info
    depends_on:
      - db
    restart: always

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: always

volumes:
  db_data:

Build and Push

# Build
docker build -t myapp:latest .

# Push to registry
docker push registry.example.com/myapp:latest

# Run with environment
docker run -p 8000:8000 \
  -e DATABASE_URL=postgresql://... \
  myapp:latest

Key UV Docker Benefits

  • Faster builds - Bytecode compilation with UV_COMPILE_BYTECODE=1
  • Smaller images - No pip overhead, efficient caching
  • Better caching - Lockfile-based builds with --frozen
  • Reproducibility - uv.lock ensures exact versions
  • No editable installs - --no-editable for smaller final images

See uv Docker Integration Guide for more details on caching strategies and optimization.