Skip to content

Commit 01586ba

Browse files
committed
Update 'Multi-stage Builds' guide to use uv
1 parent 07639cb commit 01586ba

File tree

1 file changed

+27
-20
lines changed

1 file changed

+27
-20
lines changed

python/the-basics/multi-stage-builds.html.md

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,54 @@ In short, we split the process of building and compiling dependencies and runnin
1212
- The resulting image is smaller in size
1313
- The 'attack surface' of your application is smaller
1414

15-
Let's make a multi-stage `Dockerfile` from scratch. Here's part 1:
16-
17-
<div class="note icon">
18-
In this example we assume the use of `poetry`, however you can adapt the file to work with other dependency managers too.
19-
</div>
15+
Let's make a multi-stage `Dockerfile` using uv, based on the [uv-docker-example](https://github.com/astral-sh/uv-docker-example/raw/refs/heads/main/multistage.Dockerfile). Here's part 1:
2016

2117
```dockerfile
22-
FROM python:3.11.9-bookworm AS builder
23-
24-
ENV PYTHONUNBUFFERED=1 \
25-
PYTHONDONTWRITEBYTECODE=1
18+
# Builder stage
19+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
2620

27-
RUN pip install poetry && poetry config virtualenvs.in-project true
21+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
2822

2923
WORKDIR /app
3024

31-
COPY pyproject.toml poetry.lock ./
25+
# Install dependencies
26+
RUN --mount=type=cache,target=/root/.cache/uv \
27+
--mount=type=bind,source=uv.lock,target=uv.lock \
28+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
29+
uv sync --frozen --no-install-project --no-dev
3230

33-
RUN poetry install
31+
# Copy the application code
32+
ADD . /app
33+
34+
# Install the project
35+
RUN --mount=type=cache,target=/root/.cache/uv \
36+
uv sync --frozen --no-dev
3437
```
3538

36-
So what's going on here? First, we use a "fat" python 3.11.9 image and installing and building all dependencies. Defining it as `builder` gives us a way to interact with it later. What essentially happens here is exactly what happens when you install a project locally using poetry: a `.venv/` directory is created and in it are all your built dependencies and binaries. You can inspect your own `.venv/` folder to see what that looks like. This directory is the primary artifact that we want.
39+
So what's going on here? First, we use a slim Python image that includes `uv`. We set some environment variables and install all dependencies using `uv sync`. This stage is defined as `builder`, which gives us a way to interact with it later.
40+
41+
What essentially happens here is similar to what happens when you install a project locally using a package manager: dependencies are installed and the project is set up. The primary artifact we want is the installed project with all its dependencies.
3742

3843
Part 2, the runtime, looks something like this:
3944

4045
```dockerfile
41-
FROM python:3.11.9-slim-bookworm
46+
# Runtime stage
47+
FROM python:3.12-slim-bookworm
4248

43-
WORKDIR /app
49+
# Copy the application from the builder
50+
COPY --from=builder --chown=app:app /app /app
4451

45-
COPY --from=builder /app .
46-
COPY [python-app]/ ./[python-app]
52+
# Place executables in the environment at the front of the path
53+
ENV PATH="/app/.venv/bin:$PATH"
4754

48-
CMD ["/app/.venv/bin/python", "[python-app]/app.py"]
55+
# Run the application
56+
CMD ["python", "/app/src/your_app_name/main.py"]
4957
```
5058

51-
Here we see very little actually going on; instead of the "fat" image, we now pick the slim variant. This one is about 5 times smaller in size, but is unable to compile many of the dependencies we would want compiled. We have already done that part though, so we can copy that `.venv/` folder over to this image without having to compile it again.
59+
Here we see very little actually going on; we use a slim Python image that matches the builder's Python version. We've already done the compilation and installation in the builder stage, so we can copy the entire `/app` directory (which includes the virtual environment) from the builder stage to this runtime image.
5260

5361
With this setup our image will be around 200MB most of the time (depending on what else you include). This setup is used for nearly all Python apps you deploy on Fly.io.
5462

5563
<div class="note icon">
5664
The image size is largely dependent on what files you add in the dockerfile; by default the entire working directory is copied in. If you do not want to add certain files, you can specify them in a `.dockerignore` file.
5765
</div>
58-

0 commit comments

Comments
 (0)