Python, Poetry and Docker.
Building docker images for Python projects that use Poetry is surprisingly unclear, considering to it should be the default way to build and deploy Python projects. This is my attempt at clearing things up and providing a simple default way to build images.
Layers of a cake
First off we’ll be using a multistage build for our docker image, that will
allow us to keep the final image nice and light. We’ll have a base
layer, a
builder
layer, and a runtime
layer. The base
layer will contain all the shared
dependencies for the builder
and runtime layers
FROM python:3.11-alpine as base
ARG DEV=false
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
RUN apk update && \
apk add libpq
Next the builder
layer is where we install poetry
and all it’s build
dependencies.
FROM base as builder
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN apk update && \
apk add musl-dev build-base gcc gfortran openblas-dev
WORKDIR /app
# Install Poetry
RUN pip install poetry==1.6.1
# Install the app
COPY pyproject.toml poetry.lock ./
RUN if [ $DEV ]; then \
poetry install --with dev --no-root && rm -rf $POETRY_CACHE_DIR; \
else \
poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR; \
fi
This installs all the dependencies to build my project, installs poetry
,
copies across my pyproject.toml
and poetry.lock
and installs it all.
Finally we can setup our final runtime layer.
FROM base as runtime
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY src ./src
WORKDIR /app/src
ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
Which simply involves coping across the VIRTUAL_ENV
from the builder image,
copy our source code into the image and run the application. Notice how we don’t
need to install any build time dependencies into our final image. It’s just the
python base image, some runtime dependencies, our source code and the
virtual_env
.
Hold on, I Dev in Docker.
Some wierdo’s like to run there development environments in the docker container.
Our stripped down runtime image is no place to be writing code but the builder
stage is perfect.
First we are going to want to add docker-compose.yml
to easily run our dev
server.
---
version: "3.7"
services:
app:
build:
context: .
target: builder
args:
DEV: true
command: uvicorn src.app.main:app --reload --host 0.0.0.0
ports:
- 8000:8000
volumes:
- ./:/app
- /app/.venv
Next you’ll want to configure PyCharm to use the Python interpreter from the Docker Image. But that is beyond to scope of me caring and is left as an excercise for the reader.
Conclusion
Poetry and Docker actually play very nicely together if you know how, I find it strange that the Poetry documentation is not clearer on how to combine them.
Using a builder
stage install your virtual env and simply coping that across
to a pretty bare python runtime image is the perfect way to keep your final
image lean and production ready without having to maintain multiply Dockerfile’s.
Final Dockerfile
FROM python:3.11-alpine as base
ARG DEV=false
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
RUN apk update && \
apk add libpq
FROM base as builder
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN apk update && \
apk add musl-dev build-base gcc gfortran openblas-dev
WORKDIR /app
# Install Poetry
RUN pip install poetry==1.6.1
# Install the app
COPY pyproject.toml poetry.lock ./
RUN if [ $DEV ]; then \
poetry install --with dev --no-root && rm -rf $POETRY_CACHE_DIR; \
else \
poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR; \
fi
FROM base as runtime
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY src ./src
WORKDIR /app/src
ENTRYPOINT ["python", "-m", "app.main"]