Skip to content

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_cache

Without 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 --no-editable

# Runtime stage
FROM python:3.13-slim

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

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

CMD ["python", "-m", "myapp"]

The --no-editable flag installs the project into the virtual environment as a regular package instead of an editable install. This means the .venv directory is self-contained and can be copied to the runtime stage without also copying the source code. 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

# Recommended when using cache mounts (see below)
ENV UV_LINK_MODE=copy

# Pin the Python version uv should use
ENV UV_PYTHON=3.13

UV_COMPILE_BYTECODE=1 pre-compiles .pyc files during installation, trading slower builds for faster application startup in the running container.

UV_LINK_MODE=copy tells uv to copy files instead of hard-linking them. When using Docker cache mounts, the cache and the target directory live on separate filesystems, so uv falls back to copying anyway. Setting this explicitly avoids the warning message.

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.

Using cache mounts for faster builds

Docker BuildKit cache mounts let uv reuse its download cache across builds, avoiding redundant network requests:

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

When using cache mounts, set UV_LINK_MODE=copy to suppress warnings about cross-filesystem linking (uv falls back to copying automatically, but the warning is noisy):

ENV UV_LINK_MODE=copy

Add this alongside the other environment variables in your Dockerfile. Cache mounts require BuildKit, which is the default builder in Docker Desktop and recent Docker Engine versions.

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.10.9 /uv /uvx /bin/

A major.minor tag like 0.10 floats to the latest patch release in that series. For full reproducibility, pin to a specific patch version or use a digest.

Get Python tooling updates

Subscribe to the newsletter
Last updated on

Please submit corrections and feedback...