I have been watching uv, the open-source Package manager for Python, for a while.

Earlier this year, I decided that it would be my preferred Python package manager going forward.

I migrated my private as well as public codebases to uv and have since recommended it in my relatively popular article on running Python in production.

Getting uv right inside Docker is a bit tricky and even their official recommendations are not optimal.

Similar to Poetry, I recommend using a two-step build process to eliminate uv from the final image size.

Consider a simple Flask-based web server as an example

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Create a sample package
$ uv init --name=src
$ uv add flask && uv sync
$ touch README.md
$ mkdir src

# Create a file src/server.py in your favorite editor
$ cat src/server.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
  return "<p>Hello, World!</p>"

if __name__ == "__main__":
  app.run()

Let’s finish the build process Now, let’s add a simple Dockerfile Dockerfile1

Dockerfile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM ghcr.io/astral-sh/uv:trixie-slim AS base

WORKDIR /app
# Only copy uv.lock and not pyproject.toml
# This ensures hermiticity of the build
# And prevents Docker image invalidation in case of non-dependency changes
# are made to pyproject.toml
COPY uv.lock /app
# Install dependencies
RUN uv init --name src && uv sync --no-dev --frozen
COPY src /app/src

ENTRYPOINT ["poetry", "run", "python", "src/server.py"]

And let’s build and check its size

Bash
1
2
3
$ docker build -f Dockerfile1 -t example1 . && \
  docker image inspect example1 --format='{{.Size}}' | numfmt --to=iec-i
210Mi

We don’t need uv in the final build, so we can save space via multi-stage Docker builds.

Consider following the multi-stage Docker file Dockerfile2

Dockerfile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
FROM ghcr.io/astral-sh/uv:trixie-slim AS builder

WORKDIR /app
# Only copy uv.lock and not pyproject.toml
# This ensures hermiticity of the build
# And prevents docker image invalidation in case non-dependency changes
# are made to pyproject.toml
COPY uv.lock /app
# Install dependencies
# virtual env is created in "/app/.venv" directory
RUN uv init --name src && uv sync --no-dev --frozen

FROM python:3.13-slim AS runner
COPY src /app/src
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONPATH=/app/.venv/lib/python3.13/site-packages

WORKDIR /app
ENTRYPOINT ["python", "src/server.py"]

And the result

Bash
1
2
3
$ docker build -f Dockerfile2 -t example1 . && \
  docker image inspect example1 --format='{{.Size}}' | numfmt --to=iec-i
143Mi

That’s an extra 77Mi (37%) of savings while reducing the attack surface of the Docker image by eliminating uv from the final image.