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