How to use uv in a Dockerfile
Docker images for Python projects have traditionally suffered from slow builds and bloated layers. uv eliminates both problems: its speed and lockfile support make Docker builds faster and more reproducible.
A minimal Dockerfile
This Dockerfile installs uv, copies the project files, syncs dependencies, and runs the application:
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
RUN uv sync --frozen
CMD ["uv", "run", "python", "-m", "myapp"]The two-stage uv sync pattern is deliberate. The first uv sync --frozen --no-install-project installs dependencies without the project itself, creating a cached Docker layer. The second uv sync --frozen installs the project after copying source code. When only source code changes (not dependencies), Docker reuses the cached dependency layer and skips reinstalling packages.
The --frozen flag tells uv to use the existing lockfile without updating it. This ensures the Docker build installs the exact versions specified in uv.lock.
Add a .dockerignore file
Create a .dockerignore to prevent unnecessary files from being copied into the image:
.venv
.git
__pycache__
*.pyc
.ruff_cache
.mypy_cacheWithout this file, COPY . . sends the entire directory context to Docker, including the local .venv (which can be hundreds of megabytes) and version control history.
Choosing a base image
The python:3.13-slim image provides a minimal Debian environment with Python pre-installed. For even smaller images, python:3.13-alpine is available, though Alpine’s musl libc can cause compatibility issues with some Python packages.
Alternatively, let uv install Python itself by starting from a non-Python base image:
FROM debian:bookworm-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_PYTHON_INSTALL_DIR="/usr/local"
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
RUN uv sync --frozen
CMD ["uv", "run", "python", "-m", "myapp"]The .python-version file or requires-python in pyproject.toml tells uv which Python version to download and install.
Multi-stage builds for smaller images
A multi-stage build separates dependency installation from the final runtime image:
# Build stage
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
RUN uv sync --frozen
# Runtime stage
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "myapp"]The runtime stage copies only the virtual environment and application source, leaving behind uv, build tools, and compilation artifacts. The final CMD runs Python directly from .venv/bin since uv is not present in the runtime image.
Configuring uv behavior in Docker
Several environment variables control how uv behaves inside a container:
# Suppress progress bars in build logs
ENV UV_NO_PROGRESS=1
# Compile Python bytecode for faster startup
ENV UV_COMPILE_BYTECODE=1
# Pin the Python version uv should use
ENV UV_PYTHON=3.13UV_COMPILE_BYTECODE=1 pre-compiles .pyc files during installation, trading slower builds for faster application startup in the running container.
Installing from a private index
To install packages from a private PyPI index, use Docker’s secret mounts to keep credentials out of image layers:
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN --mount=type=secret,id=uv_index_url \
UV_EXTRA_INDEX_URL=$(cat /run/secrets/uv_index_url) \
uv sync --frozen --no-install-project
COPY . .
RUN --mount=type=secret,id=uv_index_url \
UV_EXTRA_INDEX_URL=$(cat /run/secrets/uv_index_url) \
uv sync --frozen
CMD ["uv", "run", "python", "-m", "myapp"]Build with:
echo "https://token:secret@private.pypi.org/simple/" > /tmp/uv_index_url
docker build --secret id=uv_index_url,src=/tmp/uv_index_url -t myapp .The --mount=type=secret flag makes the secret available only during that RUN step without persisting it in any image layer. Using ARG or ENV for credentials would bake them into the image history.
Pinning the uv version
The ghcr.io/astral-sh/uv:latest tag always pulls the newest uv release. For reproducible builds, pin to a specific version:
COPY --from=ghcr.io/astral-sh/uv:0.7 /uv /uvx /bin/This ensures builds produce the same result regardless of when they run.