Skip to main content

Imperative context-manager DSL for authoring multi-stage Dockerfiles.

Project description

docker-dsl

PyPI Python Docs License: MIT

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?

  • RUN chains glued with && are fragile. The run builder accumulates commands in Python — r.git("clone", url), r.make("-j$(nproc)") — and emits one correct RUN instruction, with cd scoping 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|false and 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(...), and s.bind(...) are scoped context managers — every RUN inside 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 recipe
  • multi_stage.py — builder + release pattern with COPY --from
  • apt_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:

  1. Runs bash -lic 'compgen -c' to enumerate commands
  2. Invokes bash completion functions to extract subcommands + flags
  3. Falls back to man <cmd> | col -b (parallelized across CPU cores) for commands without bash completions
  4. Parses builder.py via ast to preserve real method signatures
  5. Writes builder.pyi with @generated header

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

docker_dsl-0.1.0.tar.gz (56.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

docker_dsl-0.1.0-py3-none-any.whl (59.8 kB view details)

Uploaded Python 3

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

Hashes for docker_dsl-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ec9053b2e6abb8a712f38ad57928dbc86cb446e49b3ad11bf0c80b3c83a7cdce
MD5 da1c46ebbf71cba633d3f5243994b04b
BLAKE2b-256 e620af14d2ff7df62ac7c0186283687a9eaf2d4992f535368c3cfc8c1753b2c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for docker_dsl-0.1.0.tar.gz:

Publisher: release-pypi.yml on yasyf/docker-dsl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for docker_dsl-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9203ee622fdd78c4e6d285aa9ba39d5fc229e95dffa4e1addd79045784f8d425
MD5 9ed69315de27178634b20d82bdb8011b
BLAKE2b-256 26198d4c3ada08e08a0b5a5534b89f7c9977cceef77e98a57cb2800fc4722fcc

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page