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"]