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.