Skip to content

Commit

Permalink
Update 'Multi-stage Builds' guide to use uv
Browse files Browse the repository at this point in the history
  • Loading branch information
mattt committed Oct 19, 2024
1 parent 07639cb commit 23ff6b2
Showing 1 changed file with 35 additions and 24 deletions.
59 changes: 35 additions & 24 deletions python/the-basics/multi-stage-builds.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,58 @@ In short, we split the process of building and compiling dependencies and runnin
- The resulting image is smaller in size
- The 'attack surface' of your application is smaller

Let's make a multi-stage `Dockerfile` from scratch. Here's part 1:

<div class="note icon">
In this example we assume the use of `poetry`, however you can adapt the file to work with other dependency managers too.
</div>
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 the complete Dockerfile:

```dockerfile
FROM python:3.11.9-bookworm AS builder
# Builder stage
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

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

WORKDIR /app

COPY pyproject.toml poetry.lock ./
# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev

RUN poetry install
```
# Copy the application code
ADD . /app

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.
# Install the project
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

Part 2, the runtime, looks something like this:
# Runtime stage
FROM python:3.12-slim-bookworm

```dockerfile
FROM python:3.11.9-slim-bookworm
# Copy the application from the builder
COPY --from=builder --chown=app:app /app /app

WORKDIR /app

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

CMD ["/app/.venv/bin/python", "[python-app]/app.py"]
# Run the application
CMD ["python", "/app/src/your_app_name/main.py"]
```

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.
Let's break down what's happening in this Dockerfile:

1. **Builder Stage**:
- We start with the `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` image, which includes uv.
- We set environment variables to compile bytecode and use copy mode for linking.
- We use `uv sync` to install dependencies, utilizing caching for faster builds.
- We copy the application code and install the project.

2. **Runtime Stage**:
- We use a slim Python image that matches the builder's Python version.
- We copy the application from the builder stage.
- We set the `PATH` to include the virtual environment's `bin` directory.
- Finally, we set the command to run our application.

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.

<div class="note icon">
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.
</div>

0 comments on commit 23ff6b2

Please sign in to comment.