# How to use uv in a Dockerfile


Docker images for Python projects have traditionally suffered from slow builds and bloated layers. [uv](https://pydevtools.com/handbook/reference/uv.md) eliminates both problems: its speed and [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) 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:

```dockerfile
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:

```text
.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:

```dockerfile
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:

```dockerfile
# 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:

```dockerfile
# 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
```

### Pre-compile bytecode for faster container startup

`UV_COMPILE_BYTECODE=1` is the highest-impact setting for production images. By default, Python compiles `.py` source files to `.pyc` bytecode the first time each module is imported, then caches the result in `__pycache__/`. In a long-running process this cost is paid once. In containerized workloads (serverless platforms, autoscaled Kubernetes pods, short-lived job runners) every fresh container pays it again on cold start.

Setting `UV_COMPILE_BYTECODE=1` shifts that work to image build time. uv compiles bytecode for every installed package, embedding the `.pyc` files in the image layer. Builds run a few seconds slower; cold starts drop, often noticeably for apps with heavy import graphs.

Set it once near the top of the Dockerfile and leave it on for production images.

### Configure cache linking

`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:

```dockerfile
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:

```bash
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:

```dockerfile
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):

```dockerfile
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:

```dockerfile
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.

## Learn more

- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, the core workflows, and recent releases.
