docker-dsl
Delete your Dockerfile.gpu.
Delete your Dockerfile.gpu. docker-dsl renders both GPU and CPU Dockerfiles from one Python recipe, where a flag flips the variant and run() collapses each chain into a single RUN.
Get started
Write a recipe module, minimal.py:
from docker_dsl import Stage
from docker_dsl import context as ctx
ctx.register("tag", str)
with Stage("ubuntu:24.04") as s:
s.arg("APP_TAG", ctx.tag or "latest", env=True)
s.workdir("/app")
with s.run() as r:
r.echo("hello from docker-dsl") > "/app/greeting.txt"
s.cmd("cat", "/app/greeting.txt")Render it:
uvx docker-dsl minimal --tag=v1.0.0 --out DockerfileDriving with an agent? Paste this:
Install docker-dsl in this repo with `uv add docker-dsl`.
Port my existing Dockerfile to a recipe module: one Stage per build stage, run() blocks for RUN chains, cache mounts on the apt/pip steps.
Render it with `uvx docker-dsl <module.path> --out Dockerfile` and diff against the original.
Docs: https://yasyf.github.io/docker-dsl/
Use cases
Ship GPU and CPU images from one recipe
Your Dockerfile.gpu started life as a copy of your Dockerfile, and the two have drifted ever since. Register a bool and branch in Python instead:
ctx.register("gpu", bool)
base = "nvidia/cuda:12.4.1-runtime-ubuntu22.04" if ctx.gpu else "ubuntu:24.04"uvx docker-dsl train --gpu=true --out Dockerfile.gpu
uvx docker-dsl train --gpu=false --out DockerfileBoth files render from the same recipe: the GPU variant gets the CUDA base image and pip install torch, the CPU variant gets ubuntu:24.04 and the CPU wheel index, and the diff between them is a Python if you can read.
Turn a 10-command build into one correct RUN
A long RUN chain means a && and a trailing backslash on every line, and a cd that silently leaks into the rest of the chain. Write the commands as method calls — r.cd() scopes the directory change:
uvx docker-dsl run_builder --ref=v2.0.0
The nine calls in run_builder.py emit one RUN
RUN git clone https://github.com/example/widget.git . \
&& git checkout v2.0.0 \
&& cd build \
&& cmake .. --build-type Release \
&& make -j$(nproc) \
&& make install \
&& cd - \
&& echo "widget built" >> /var/log/build.txt \
&& echo "build complete" > /src/STATUS \
&& rm -rf /src/buildKeep secrets and caches scoped to the RUNs that need them
A BuildKit --mount flag lives on one RUN instruction, so refactoring the chain means re-plumbing every mount by hand. In a recipe, cache, secret, and bind are context managers — every run() inside the block picks them up, and nothing leaks past it:
uvx docker-dsl mounts --private=trueRUN --mount=type=secret,id=pypi,target=/root/.netrc --mount=type=cache,target=/root/.cache/pip,sharing=shared \
pip install --requirement requirements-private.txtRender with --private=false and the secret-mounted RUN disappears entirely — the .netrc never touches the public variant.
More in the docs
- Multi-stage builds — build in one stage,
copy(..., stage=builder)the artifact into a slim final image — guide - Smart apt — apt_install inserts
apt-get updateexactly where package lists change, PPAs and third-party repos included — guide - The run builder — any shell command as a method call, with redirects and directory scoping — guide
- Reusable helpers — factor repeated setup into plain Python context managers — guide
- Render from the CLI or from Python — every recipe gets typed
--flags for free — guide
Status: alpha — the DSL surface may still shift before 1.0.


