Metadata-Version: 2.4
Name: pdm-dockerize
Version: 0.8.0
Summary: Help generating docker images from PDM projects
Keywords: pdm,docker
Author-Email: Axel Haustant <noirbizarre@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Plugins
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Code Generators
Classifier: Typing :: Typed
Project-URL: Homepage, https://github.com/noirbizarre/pdm-dockerize
Project-URL: Documentation, https://github.com/noirbizarre/pdm-dockerize#readme
Project-URL: Repository, https://github.com/noirbizarre/pdm-dockerize
Project-URL: Issues, https://github.com/noirbizarre/pdm-dockerize/issues
Requires-Python: >=3.10
Requires-Dist: pdm>=2.28
Description-Content-Type: text/markdown

# pdm-dockerize

[![CI](https://github.com/noirbizarre/pdm-dockerize/actions/workflows/ci.yml/badge.svg)](https://github.com/noirbizarre/pdm-dockerize/actions/workflows/ci.yml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/noirbizarre/pdm-dockerize/main.svg)](https://results.pre-commit.ci/latest/github/noirbizarre/pdm-dockerize/main)
[![PyPI](https://img.shields.io/pypi/v/pdm-dockerize)](https://pypi.org/project/pdm-dockerize/)
[![PyPI - License](https://img.shields.io/pypi/l/pdm-dockerize)](https://pypi.org/project/pdm-dockerize/)
[![codecov](https://codecov.io/gh/noirbizarre/pdm-dockerize/graph/badge.svg?token=P949azXcf0)](https://codecov.io/gh/noirbizarre/pdm-dockerize)

Help generating docker image from PDM projects.

## Installation

Install `pdm-dockerize`:

### With `uv`

If you installed `pdm` with `uv`:

```console
uv tool install pdm --with pdm-dockerize
```

### With `pipx`

If you installed `pdm` with `pipx` and want to have the command for all projects:

```console
pipx inject pdm pdm-dockerize
```

### With `pip`

If you manually installed `pdm` with `pip`, just install the extra dependency in the same environment:

```console
pip install pdm-dockerize
```

### With `pdm`

You can also install it as a standard `pdm` plugin.

Either globally:

```console
pdm self add pdm-dockerize
```

Either as a local plugin in your project:

```toml
[tool.pdm]
plugins = [
    "pdm-dockerize",
]
```

Then:

```console
pdm install --plugins
```

## Usage

Just use `pdm dockerize` in your multistage build:

```dockerfile
# syntax=docker/dockerfile:1
ARG PY_VERSION=3.12

##
# Build stage: build and install dependencies
##
FROM python:${PY_VERSION} AS builder

ARG VERSION=0.dev
ENV PDM_BUILD_SCM_VERSION=${VERSION}

WORKDIR /project

# install PDM
RUN pip install -U pip setuptools wheel
RUN pip install pdm pdm-dockerize

RUN --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=pdm.lock,target=pdm.lock \
    --mount=type=cache,target=$HOME/.cache,uid=$UUID \
    pdm dockerize --prod -v

##
# Run stage: create the final runtime container
##
FROM python:${PY_VERSION} AS runtime

WORKDIR /app

# Fetch built dependencies
COPY --from=builder /project/dist/docker /app
# Copy needed files from your project (filter using `.dockerignore`)
COPY  . /app

ENTRYPOINT ["/app/entrypoint"]
CMD ["your-default-command"]
```

### Using `uv`

When PDM is configured with `use_uv = true`, `pdm-dockerize` automatically uses `uv pip install --target`
instead of the default pip-based installer for faster dependency installation.
If `uv` is not found at runtime, it falls back gracefully to the pip-based installer.

Lockfiles generated by `uv` (which store extras as separate entries) are automatically handled.

Here is an example Dockerfile using `uv`:

```dockerfile
# syntax=docker/dockerfile:1
ARG PY_VERSION=3.12

##
# Build stage: build and install dependencies
##
FROM python:${PY_VERSION} AS builder

ARG VERSION=0.dev
ENV PDM_BUILD_SCM_VERSION=${VERSION}

WORKDIR /project

# install uv, PDM and pdm-dockerize
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN uv pip install --system pdm pdm-dockerize

# enable uv as PDM downloader
RUN pdm config use_uv true

RUN --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=pdm.lock,target=pdm.lock \
    --mount=type=cache,target=$HOME/.cache,uid=$UUID \
    pdm dockerize --prod -v

##
# Run stage: create the final runtime container
##
FROM python:${PY_VERSION} AS runtime

WORKDIR /app

# Fetch built dependencies
COPY --from=builder /project/dist/docker /app
# Copy needed files from your project (filter using `.dockerignore`)
COPY  . /app

ENTRYPOINT ["/app/entrypoint"]
CMD ["your-default-command"]
```

### Command line options

```text
pdm dockerize [OPTIONS] [TARGET]
```

| Option                                | Description                                          |
|---------------------------------------|------------------------------------------------------|
| `TARGET`                              | Output directory (default: `dist/docker`)            |
| `--prod` / `--production`             | Select production dependencies only                  |
| `-G GROUP` / `--group GROUP`          | Select a specific dependency group (can be repeated) |
| `--no-default`                        | Do not include the default dependencies              |
| `--dry-run`                           | Preview packages and entrypoint script without writing |
| `-L LOCKFILE` / `--lockfile LOCKFILE` | Use an alternative lockfile                          |

### Selecting scripts

By default, the `dockerize` command will render a script without any command as it does not select any script by default.

You can select scripts with the `include` and `exclude` properties of the `tool.pdm.dockerize` section.
Those properties are optional, can be either a string or list of string.
Each string is a [`fnmatch` filter pattern](https://docs.python.org/3/library/fnmatch.html)

Dockerize first select script based on the include patterns and then filter-out those matching with any exclude pattern.

#### Include all scripts

```toml
[tool.pdm.dockerize]
include = "*"
```

#### Include some specific scripts

```toml
[tool.pdm.dockerize]
include = ["my-script", "my-other-script"]
```

#### Include all scripts excluding those matching `prefix-*`

```toml
[tool.pdm.dockerize]
include = "*"
exclude = "prefix-*"
```

#### Include all scripts matching a prefix but two

```toml
[tool.pdm.dockerize]
include = "prefix-*"
exclude = ["prefix-not-you", "prefix-you-neither"]
```

### Selecting binaries

By default, the `dockerize` command will not copy any python executable provided by your dependencies.
You can select binaries with the `include_bins` and `exclude_bins` properties of the `tool.pdm.dockerize` section.
Syntax and behavior are exactly the exact sames than `include`/`exclude` for script selection.

#### Include all python executables

```toml
[tool.pdm.dockerize]
include_bins = "*"
```

#### Include some specific executables

Most of the time, it will look like this:

```toml
[tool.pdm.dockerize]
include_bins = ["uvicorn"]
```

### Generated entrypoint

The `dockerize` command generates a POSIX `sh` entrypoint script alongside the installed packages.
This entrypoint exposes selected PDM scripts as subcommands. Here is what it does:

- **`PYTHONPATH`**: automatically set to include `lib/`. For `pdm.backend`-based projects using src-layout, the package source directory is also added.
- **`PATH`**: automatically set to include `bin/`, so any installed console-script binaries are available.
- **Pre/post scripts**: if you define `pre_<script>` or `post_<script>` hooks in your PDM scripts, they are executed before and after the main script respectively.
- **`{args}` interpolation**: the PDM `{args}` and `{args:defaults}` placeholder syntax is supported. Arguments passed to the entrypoint subcommand are forwarded through `"$@"`. When no `{args}` placeholder is present, arguments are appended automatically.
- **Script kinds**: all PDM script kinds are supported: `cmd`, `call`, `shell`, and `composite`.

### Controlling environment

`pdm-dockerize` respects defined environment variables:

- scripts `env` variables are properly set
- shared `_.env` variables are properly set
- scripts `env_file` are properly loaded
- shared `_.env_file` are properly loaded

In addition, you can define some docker-only environment variables using the `tool.pdm.dockerize.env` table
or some docker-only `.env` files using `tool.pdm.dockerize.env_file`

#### Defining docker-only environment variables

Those environment variables will only be effective in the docker entrypoint.

```toml
[tool.pdm.dockerize.env]
VAR = "value"
```

#### Loading docker-only environment files

This file will only be loaded in the docker entrypoint.

```toml
[tool.pdm.dockerize]
env_file = "docker.env"
```

## Workspaces

[PDM workspaces](https://pdm-project.org/en/stable/usage/workspace/) (requires `pdm>=2.28`) are supported.

Given a workspace root declaring its members:

```text
my-workspace/
├── pyproject.toml          # [tool.pdm.workspace] members = ["packages/*"]
├── pdm.lock                # a single, shared lockfile
└── packages/
    ├── api/pyproject.toml
    └── worker/pyproject.toml
```

```toml
[tool.pdm.workspace]
members = ["packages/*"]

[tool.pdm.dockerize]
include = ["serve", "migrate"]
include_bins = "*"

[tool.pdm.scripts]
serve = "uvicorn api.main:app --host 0.0.0.0"
migrate = "alembic upgrade head"
```

### Building a single image for the whole workspace

Running `pdm dockerize` from the workspace root installs every workspace member,
along with their transitive dependencies, into the output directory,
and exposes their console scripts (subject to `include_bins`/`exclude_bins`).

Members are installed as regular (**non-editable**) packages,
so the resulting image is self-contained and never references build-time host paths.

A member is installed when it is matched by `[tool.pdm.workspace] members`,
has a `project.name` and is a distribution
(ie. neither `distribution = false` nor `package-type = "application"`).

Members are treated as implicit `default` group dependencies,
so they are excluded by selections dropping that group, such as `--no-default`.

Members must be present in the build stage so their wheels can be built:
make sure your `.dockerignore` does not exclude the members directory.

```dockerfile
COPY pyproject.toml pdm.lock ./
COPY packages/ ./packages/
RUN pdm dockerize
```

### Building one image per member

Running `pdm dockerize` from a member, or targeting it with `--project`,
builds an image for **that member only**:

```console
$ cd packages/api && pdm dockerize
$ pdm --project packages/api dockerize    # equivalent
```

- the member dependencies are installed into `packages/api/dist/docker/lib`,
  including the **sibling members it depends on**, as non-editable packages;
- the members it does not depend on are **not** installed;
- like any dockerized project, the member itself is **not** installed:
  its sources are exposed through `PYTHONPATH`, so they have to be in the image.

```dockerfile
COPY packages/api /app
COPY packages/api/dist/docker /app
WORKDIR /app
ENTRYPOINT ["/app/entrypoint"]
```

Declare a dependency on a sibling member by name:

```toml
[project]
name = "api"
dependencies = ["shared"]
```

### Where the configuration comes from

| From the targeted project        | From the workspace root                |
|----------------------------------|----------------------------------------|
| `tool.pdm.dockerize`             | the `pdm.lock` lock file               |
| `tool.pdm.scripts`               | the Python interpreter                 |
| the dependencies and the groups  | the `tool.pdm.source` package indexes  |
| the output directory             | the `use_uv` setting                   |

**Nothing is inherited from the workspace root**: the `tool.pdm.dockerize` and
`tool.pdm.scripts` sections of the root are ignored when targeting a member,
and vice versa. A member `pdm.toml` is ignored too.

> [!NOTE]
> Selecting a group only defined in a member, using `--group`, is rejected:
> the workspace lock file only records the groups of the workspace root.

## Internals

This plugin works by subclassing some `pdm` classes to reuse the installation process:

- `DockerizeEnvironment`, a `pdm` `PythonLocalEnvironment` targeting the dockerize output directory
- `DockerizeInstallManager`, a `pdm` `InstallManager` filtering binaries
- `DockerizeSynchronizer`, a `pdm` `Synchronizer` using a `DockerizeInstallManager` as `InstallManager`
- `DockerizeUvSynchronizer`, an alternative synchronizer that invokes `uv pip install --target` as a subprocess when `use_uv` is enabled
- `FilteringDestination`, a `pdm` `InstallDestination` filtering binaries

This way, the dockerization reuses the same installation process, just tuned for docker and augmented with `pdm-dockerize` specifics.

## Contributing

Read the [dedicated contributing guidelines](./CONTRIBUTING.md).
