Python Docker Container with ``uv`` =================================== We divide our Docker workflow into different layers. This allows us to provide new builds more quickly. We start with the layers that change the least so that we can cache the artefacts for as long as possible. This is also the reason why we keep the installations of the dependencies from :file:`uv.lock` and the installation of our :doc:`application ` strictly separate – our code probably changes faster than that of the dependencies. .. seealso:: * `Order your layers `_ #. First, we build an initial build container that prevents us from having to deliver our build tools. .. seealso:: * `Multi-stage builds `_ .. code-block:: docker :linenos: :emphasize-lines: 4 # syntax=docker/dockerfile:1.9 FROM ubuntu:noble AS build SHELL ["sh", "-exc"] RUN <`_, you should use the Docker compatibility mode. Line 6: If necessary, you can also prefix each ``RUN`` script with ``set -ex``, which makes troubleshooting easier. .. seealso:: * https://github.com/containers/podman/issues/8477 #. We then build a :ref:`virtual Python environment ` with our application in the :file:`/app` directory and copy it to our runtime container. One of the advantages of this is that the same base container can be used for different Python versions and :ref:`virtual environments `. With :doc:`/productive/envs/uv/index` 0.4.4 this has become much easier thanks to the environment variable ``UV_PROJECT_ENVIRONMENT``: .. code-block:: docker :linenos: :emphasize-lines: 1, 3, 4, 6, 7 COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv ENV UV_LINK_MODE=copy \ UV_COMPILE_BYTECODE=1 \ UV_PYTHON_DOWNLOADS=never \ UV_PYTHON=python3.12 \ UV_PROJECT_ENVIRONMENT=/app Line 1: Safety-conscious organisations should check and pack ``uv`` themselves. Line 3: This prevents :doc:`/productive/envs/uv/index` from complaining about not being able to use hardlinks. Line 4: The Python packages are byte-compiled to shorten the start times of the container. Line 6: Select Python version. Line 7: Declare :file:`/app` as target for ``uv sync``. #. Now we create the ``app`` Dockerfile: .. code-block:: docker :linenos: :emphasize-lines: 1-2, 6-9, 14 COPY pyproject.toml /_lock/ COPY uv.lock /_lock/ RUN --mount=type=cache,target=/root/.cache <` from having to be rebuilt if the layer with your dependencies has to be rebuilt. .. seealso:: * `Use cache mounts `_ Lines 6–9: The dependencies are synchronised without the application itself. This layer is cached until the :ref:`uv_lock` file or :file:`pyproject.toml` changes. Line 14: ``myapp`` is installed from :file:`/src` without any dependencies. #. Finally, we create the runtime container: .. code-block:: docker :linenos: :emphasize-lines: 4, 6-9, 13, 20-21, 29-30, 32, 37-41 FROM python:3.12-slim SHELL ["sh", "-exc"] ENV PATH=/app/bin:$PATH RUN <` to the search path. Lines 6–9: Runs the application as a service user ``app``. Line 13: In the Python ecosystem, it is not necessarily common for your application to respond to a ``SIGTERM``. ``STOPSIGNAL SIGINT`` is an easy way to work around this. .. seealso:: * `Why Your Dockerized Application Isn’t Receiving Signals `_ Lines 20–21: Note that the dependencies at runtime are different from the dependencies at build time. Also, there is no ``uv``. Lines 29–30: If your application is not a :doc:`Python package ` installed with ``uv sync``, you must copy your application into the container here. Line 32: This copies the pre-built directory :file:`/app` into the runtime container and changes the permissions on the service user ``app`` and the group ``app`` in one step. Lines 37–41: Optional: I usually use this introspection for a smoke test, which ensures that the application can actually be imported.