We use cookies to understand how people use Depot.
Container Builds

Best practice Dockerfile for Python with poetry

Below is an example Dockerfile that we use and recommend at Depot when we are building Docker images for Python applications that use poetryas their package manager.

FROM python:3.12-slim as base
ENV  POETRY_VERSION=1.6.1 \
  PYTHONUNBUFFERED=1 \
  PYTHONDONTWRITEBYTECODE=1 \
  PIP_NO_CACHE_DIR=off \
  PIP_DISABLE_PIP_VERSION_CHECK=on \
  PIP_DEFAULT_TIMEOUT=100 \
  POETRY_HOME="/opt/poetry" \
  POETRY_VIRTUALENVS_IN_PROJECT=true \
  POETRY_NO_INTERACTION=1 \
  PYSETUP_PATH="/opt/pysetup" \
  VENV_PATH="/opt/pysetup/.venv"
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
 
 
FROM base as builder
RUN --mount=type=cache,target=/root/.cache \
  pip install "poetry==$POETRY_VERSION"
WORKDIR $PYSETUP_PATH
COPY ./poetry.lock ./pyproject.toml ./
RUN --mount=type=cache,target=$POETRY_HOME/pypoetry/cache \
  poetry install --no-dev
 
 
FROM base as production
ENV FASTAPI_ENV=production
COPY --from=builder $VENV_PATH $VENV_PATH
COPY ./app /app
WORKDIR /app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Explanation of the Dockerfile

Poetry is a popular Python package manager that helps manage dependencies on your local machine using virtual environments to isolate dependency versions between projects. In a Docker environment, we don't need to use virtual environments, and we can instead use a more straightforward approach to installing and managing dependencies. Assuming your project is currently using Poetry and has a pyproject.toml file, you can use the following Dockerfile to build your project with multi-stage builds to produce an efficient build and optimized final image.

Stage 1: FROM python:3.12-slim-bookworm AS base

Using a common base image for all stages ensures compatibility between the build and deployment stages and allows us to take advantage of Docker's layer caching to produce fewer layers in the build. An -alpine image can also be used for an even smaller final image, but some projects may require additional dependencies to be installed.

ENV  POETRY_VERSION=1.6.1 \
  PYTHONUNBUFFERED=1 \
  PYTHONDONTWRITEBYTECODE=1 \
  PIP_NO_CACHE_DIR=off \
  PIP_DISABLE_PIP_VERSION_CHECK=on \
  PIP_DEFAULT_TIMEOUT=100 \
  POETRY_HOME="/opt/poetry" \
  POETRY_VIRTUALENVS_IN_PROJECT=true \
  POETRY_NO_INTERACTION=1 \
  PYSETUP_PATH="/opt/pysetup" \
  VENV_PATH="/opt/pysetup/.venv"
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
  • POETRY_VERSION=1.6.1 specifies the version of Poetry to install.
  • PYTHONUNBUFFERED=1 tells Python to not buffer the output. This is useful for ensuring logs are output in real-time, so a crash doesn't obscure the logs that would otherwise be in a buffer.
  • POETRY_HOME specifies a deterministic location for Poetry to install itself.
  • PYSETUP_PATH specifies a deterministic location for Poetry to install the project's dependencies.
  • VENV_PATH specifies a deterministic location for the virtual environment to be created.
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"

After setting the environment variables, we add the Poetry and virtual environment paths to the PATH environment variable so that we can run Poetry and the project's dependencies without specifying the full path.

Stage 2: FROM base AS builder

The builder stage efficiently installs Poetry and the project's production dependencies with caching enabled. A similar stage can be used for development dependencies if needed by changing the --no-dev flag in the poetry install command.

RUN --mount=type=cache,target=/root/.cache \
  pip install "poetry==$POETRY_VERSION"
WORKDIR $PYSETUP_PATH
COPY ./poetry.lock ./pyproject.toml ./
RUN --mount=type=cache,target=$POETRY_HOME/pypoetry/cache \
  poetry install --no-dev

We use pip to install poetry so we can cache the installation. Then, we copy over only the poetry.lock and pyproject.toml files to the $PYSETUP_PATH directory and run poetry install to install the project's dependencies. By using the --no-dev flag, we ensure that only production dependencies are installed.

Stage 3: FROM base AS production

In the production stage, we copy the virtual environment from the builder stage and the project source code into the final image. We then set the working directory to the project source code and expose the port the application listens on. Finally, we define the command to run the application.

ENV FASTAPI_ENV=production
COPY --from=builder $VENV_PATH $VENV_PATH
COPY ./app /app
WORKDIR /app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Using this Dockerfile pattern, we are able to avoid installing poetry in the final image. In fact, as we are only copying in the previously installed production dependencies and source code, the final stage is extremely fast, even in the event the project source changes.

Your project may require additional tweaks to this Dockerfile, but if you are a poetry user, this is a great starting point for efficiently building your project with Docker. Consider adding an additional development stage for development dependencies and adding more stages for linting, testing, or other tasks as needed.