Imperative context-manager DSL for authoring multi-stage Dockerfiles.
Project description
docker-dsl
An imperative, context-manager-based Python DSL for authoring multi-stage
Dockerfiles. Write builds the way you write Python — with blocks,
conditionals, comprehensions, and reusable helpers — and render one recipe
into many Dockerfile variants.
Install
No install needed — run everything through uvx:
uvx docker-dsl --help
uvx fetches docker-dsl into a throwaway environment and runs it. To add it
to a project instead:
uv add docker-dsl
Quickstart
Write a recipe module — for example my_recipe.py:
from docker_dsl import Stage, context as ctx
ctx.register("gpu", bool)
with Stage("nvcr.io/nvidia/pytorch:26.03-py3" if ctx.gpu else "ubuntu:24.04") as s:
s.arg("PYTHON_VERSION", "3.13", env=True)
s.path("/root/.local/bin")
s.workdir("/root")
with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True):
s.apt_install("software-properties-common")
s.add_apt_ppa("ppa:apt-fast/stable")
s.apt_install("curl", "git", "wget", fast=True)
with s.cache("/root/.cache/uv"), s.run() as r:
r.uv("pip", "install", "-U", "numpy", "pandas")
Render it:
docker-dsl my_recipe --gpu=true --out Dockerfile
Or from Python:
from docker_dsl import Dockerfile
import my_recipe
with Dockerfile(my_recipe) as d:
gpu_text = d.render(gpu=True)
cpu_text = d.render(gpu=False)
What problems does this solve?
RUNchains glued with&&are fragile. The run builder accumulates commands in Python —r.git("clone", url),r.make("-j$(nproc)")— and emits one correctRUNinstruction, withcdscoping restored automatically.- Variants drift apart. A GPU and a CPU image usually means two copied
Dockerfiles diverging over time. Here one recipe takes
--gpu=true|falseand renders both from the same code path. - No abstraction in raw Dockerfiles. Recipes are plain Python modules: factor repeated setup into context managers and helper functions, loop and branch where the build genuinely varies.
- BuildKit mounts are easy to get wrong.
s.cache(...),s.secret(...), ands.bind(...)are scoped context managers — everyRUNinside the scope picks up the active mounts, and nothing leaks outside it.
Core concepts
Two-pass execution
When a recipe is first imported, docker-dsl is in discovery pass: the
module body runs but every DSL call is a no-op. ctx.register(...) is used
in this pass to populate a schema of config fields.
When Dockerfile(module).render(**config) is called, the DSL enters the
render pass: it validates config against the registered schema, sets
up ContextVars, and re-executes the module. Every with Stage(...) as s:
and s.<method>(...) now accumulates into the active graph. ctx.gpu
returns the validated config value.
This design lets you write the recipe as plain top-level Python — no magic, no decorators, no entrypoint functions.
Config fields
ctx.register("gpu", bool)
ctx.register("tag", str)
Every registered field is required at render time. Pydantic validates the values before the render pass runs, so type errors surface with clear messages.
Stages
with Stage("ubuntu:24.04") as base: # FROM ubuntu:24.04 AS base
base.workdir("/root")
with base.stage() as builder: # FROM base AS builder
builder.run("make", "all")
with base.stage() as release: # FROM base AS release
release.copy("/out/bin", stage=builder)
Child stage names are inferred from the as <name> target via the executing
library. Pass name="..." to override.
Run builder
s.run() as a context manager accumulates shell commands into a single
RUN instruction:
with s.run() as r:
r.git("clone", "https://example.com/repo.git")
with r.cd("repo"):
r.make("-j$(nproc)")
r.make("install")
r("(cd subdir && python setup.py install)") # raw fallback
Command methods dispatch via __getattr__ — any attribute name becomes the
shell binary. r.cd(path) works both as a statement (subsequent commands
stay in that directory) and as a context manager (scope-restores on exit via
cd -).
Echo redirects compose naturally:
with s.run() as r:
r.echo("pillow>=11.1.0") >> "/root/overrides.txt" # append
r.echo("numpy<3.0.0,>=1.26.4") > "/etc/pip/constraint.txt" # truncate
Mounts
s.cache(target, *, lock=False), s.secret(id, *, target=...),
s.bind(source=..., target=...) return context managers that push a mount
onto the stage's stack. Any RUN emitted inside the scope picks up all
active mounts.
with s.cache("/root/.cache/uv"), s.secret("aws", target="/root/.aws/credentials"):
with s.run() as r:
r.uv("pip", "install", "-U", "torch")
Smart apt
r.apt_install(...), r.add_apt_ppa(...), and r.add_apt_repo(...) are
methods on RunBuilder that write directly to the command list.
apt-get update -y is inserted automatically before the first install and
again after any dirty-marking operation (new PPA, new repo). Arbitrary
commands (cleanup, setup scripts) are just r(...) calls.
with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True), s.run() as r:
r.apt_install("software-properties-common")
r.add_apt_ppa("ppa:apt-fast/stable")
r.apt_install("apt-fast", "curl", "wget", fast=True)
r("rm -rf /tmp/*")
Reusable helpers
Recipes can compose their own context managers:
from contextlib import contextmanager
@contextmanager
def sccache(stage):
with stage.secret("aws", target="/root/.aws/credentials"):
yield
with Stage("ubuntu:24.04") as s:
with sccache(s), s.run() as r:
r.uv("pip", "install", "-U", "torch")
CLI
docker-dsl <module.path> [--<field>=<value> ...] [--out PATH]
(python -m docker_dsl is equivalent.) Arguments are built dynamically from
the fields registered by the recipe. A --out argument is always available;
if omitted, the rendered Dockerfile is written to stdout. Bool fields accept
true/false/1/0/yes/no.
Validation errors surface with structured Pydantic messages naming the missing or wrong-typed fields.
Examples
See examples/ for self-contained recipes:
minimal.py— the shortest possible recipemulti_stage.py— builder + release pattern withCOPY --fromapt_smart.py— demonstrating the apt buffer flush rules
Run them via the CLI:
docker-dsl examples.minimal --tag=dev --out Dockerfile.min
docker-dsl examples.multi_stage --release=true --out Dockerfile.ms
Editor completions
RunBuilder uses __getattr__ for dynamic shell-command dispatch, which
means editors have no static information about available commands. To fix
this, docker-dsl ships a generated builder.pyi type stub that declares
every system command as a method on RunBuilder.
Commands with bash programmable completions get @overload stubs with
Literal subcommands and typed flag kwargs. Commands without completions
get their flags extracted from man pages. All others get a plain
*args: str, **kwargs: str | bool catch-all.
Regenerate after installing new tools:
python -m docker_dsl.stubgen
The generator:
- Runs
bash -lic 'compgen -c'to enumerate commands - Invokes bash completion functions to extract subcommands + flags
- Falls back to
man <cmd> | col -b(parallelized across CPU cores) for commands without bash completions - Parses
builder.pyviaastto preserve real method signatures - Writes
builder.pyiwith@generatedheader
Flags become keyword arguments with underscore-to-hyphen conversion:
r.git("clone", url, depth="1", verbose=True) → git clone <url> --depth 1 --verbose.
Docs
Read the docs for the full guide and API reference.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file docker_dsl-0.1.0.tar.gz.
File metadata
- Download URL: docker_dsl-0.1.0.tar.gz
- Upload date:
- Size: 56.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec9053b2e6abb8a712f38ad57928dbc86cb446e49b3ad11bf0c80b3c83a7cdce
|
|
| MD5 |
da1c46ebbf71cba633d3f5243994b04b
|
|
| BLAKE2b-256 |
e620af14d2ff7df62ac7c0186283687a9eaf2d4992f535368c3cfc8c1753b2c3
|
Provenance
The following attestation bundles were made for docker_dsl-0.1.0.tar.gz:
Publisher:
release-pypi.yml on yasyf/docker-dsl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docker_dsl-0.1.0.tar.gz -
Subject digest:
ec9053b2e6abb8a712f38ad57928dbc86cb446e49b3ad11bf0c80b3c83a7cdce - Sigstore transparency entry: 1779248441
- Sigstore integration time:
-
Permalink:
yasyf/docker-dsl@a3fb74aab06ac8924c45843921eb250f12f89a80 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/yasyf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@a3fb74aab06ac8924c45843921eb250f12f89a80 -
Trigger Event:
push
-
Statement type:
File details
Details for the file docker_dsl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: docker_dsl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 59.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9203ee622fdd78c4e6d285aa9ba39d5fc229e95dffa4e1addd79045784f8d425
|
|
| MD5 |
9ed69315de27178634b20d82bdb8011b
|
|
| BLAKE2b-256 |
26198d4c3ada08e08a0b5a5534b89f7c9977cceef77e98a57cb2800fc4722fcc
|
Provenance
The following attestation bundles were made for docker_dsl-0.1.0-py3-none-any.whl:
Publisher:
release-pypi.yml on yasyf/docker-dsl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docker_dsl-0.1.0-py3-none-any.whl -
Subject digest:
9203ee622fdd78c4e6d285aa9ba39d5fc229e95dffa4e1addd79045784f8d425 - Sigstore transparency entry: 1779248605
- Sigstore integration time:
-
Permalink:
yasyf/docker-dsl@a3fb74aab06ac8924c45843921eb250f12f89a80 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/yasyf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@a3fb74aab06ac8924c45843921eb250f12f89a80 -
Trigger Event:
push
-
Statement type: