Julian Mehne

Poetry for local Python development and deployment with Docker.

Alright, there are 100 ways in Python land to do dependency management and it seems the mind share of projects shifts every two years. So, whatever, a summary of how I like to use poetry.

Installation

Poetry should not be installed in the same virtual environment as the project, because otherwise it may break when installing additional dependencies. My two go-to solutions:

  1. Use a platform-specific package manager: e.g., on Arch pacman, or brew on MacOS:
    pacman -Syu python-poetry
    
    I prefer this on my local machine.
  2. Use pipx. pipx installs tools into their own virtual environments; quite nice and preferrable whenever I need a specific poetry version
    $ sudo pacman -Syu install pipx
    $ pipx install poetry
    

The basic Poetry workflow

  1. List the top-level dependnecies in the pyproject.toml file (plus some additional fluff):
    [tool.poetry]
    name = "project-name"
    version = "0.1.0"
    description = ""
    readme = "README.md"
    package-mode = false
    
    [tool.poetry.dependencies]
    python = "^3.11"
    Flask = "^3.0.3"
    pandas = "^2.0"
    
    [build-system]
    requires = ["poetry-core>=1.0.0"]
    build-backend = "poetry.core.masonry.api"
    
    ^x.y means <(x+1, so no major upgrades - okay for an application, bad if you develop a library, because you lock all your consumers out of a version upgrade. For libraries, >=x.y is more appropriate.
  2. Let poetry resolve the dependencies: poetry lock. This writes a poetry.lock file that lists the exact dependency versions.
  3. Install the dependencies to a virtual environment. See the next section.

Using Poetry for local development

Poetry can manage virtual environments, but because I keep forgetting the details and because I prefer to have the virtual environment located in the project root, this is good to know:

Because old habits die hard, I usually create the virtual environments manually: python3 -m venv .venv and then poetry can do its thing.

My second requirement is that I mostly want dependency management to be done, but I don't need to install my project as a package ("deployment"). In old poetry versions you would poetry install --no-root, but nowadays you can just define your package-mode = false in your pyproject.toml:

[tool.poetry]
name = "project-name"
version = "0.1.0"
description = ""
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.11"
Flask = "^3.0.3"
pandas = "^2.0"

[tool.poetry.group.test]
optional = true

[tool.poetry.group.test.dependencies]
pytest = "^8.3.3"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Poetry always resolves the version dependnecies for all groups in combination, but you can still only install a subset with --only, --without and --with:

poetry install --only main --no-root
poetry install --without test --no-root

Poetry and Docker

Let's get our application into a Docker container with poetry. The idea in a nutshell:

Some things to keep in mind about docker files:

Example:

# General setup:
# Build:
# sudo docker buildx build -t poetry_testing:latest [--progress=plain] .
# Run image and drop in the shell to debug:
# sudo docker run -it poetry_testing:latest bash

ARG SOURCE_IMAGE="python:3.12-slim-bookworm"
ARG PROJECT_VENV_PATH="/opt/venv"
# Poetry binary pipx installs.
ARG POETRY_BIN_PATH="/root/.local/bin/poetry"

# 1. Install poetry as a build tool via pipx
FROM "$SOURCE_IMAGE" AS poetry
RUN apt-get update \
    && apt-get install --no-install-suggests --no-install-recommends --yes pipx \
    && rm -rf /var/lib/apt/lists/*
ARG POETRY_VERSION=1.8.3
RUN pipx install "poetry==${POETRY_VERSION}"

# 2. Install the base dependencies in a virtual environment
FROM poetry AS base
ARG BUILD_DIR="/build"
WORKDIR "$BUILD_DIR"
COPY ./pyproject.toml ./poetry.lock
ENV POETRY_REQUESTS_TIMEOUT=60 \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
ARG PROJECT_VENV_PATH
RUN python3 -m venv --without-pip "${PROJECT_VENV_PATH}"
# Activate venv in this stage - poetry will use it.
ENV VIRTUAL_ENV="$PROJECT_VENV_PATH"
COPY ./pyproject.toml ./poetry.lock .
ARG POETRY_BIN_PATH
RUN "$POETRY_BIN_PATH" install \
    --no-root \
    --no-cache \
    --no-interaction \
    --no-ansi \
    --without=test

# 3. Final image: Use clean image, just copy the virtual environment.
FROM "$SOURCE_IMAGE"
WORKDIR /opt/app
ARG PROJECT_VENV_PATH
COPY --from=base "$PROJECT_VENV_PATH" "$PROJECT_VENV_PATH"
COPY . .
# Don't create pyc files. If we want them, we should create them during the
# build, so that the image can be read-only.
ENV PYTHONDONTWRITEBYTECODE=1
# Don't buffer stdout/stderr
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["/opt/venv/bin/python", "/opt/app/main.py"]

Some open/missing points:

Userful resources