assert _instr('RUN', 'echo hello') == 'RUN echo hello'core
Dockerfile Instructions
instr creates a Dockerfile instruction string from a keyword and value. Factory functions (from_, run_, cmd_, etc.) wrap instr for each Dockerfile keyword, handling formatting details like tag joining, JSON exec form, and multi-command chaining.
Instruction factory functions
Each function maps to a Dockerfile keyword with a trailing _ to avoid clashing with Python builtins.
assert _from('python', '3.11') == 'FROM python:3.11'
assert _from('ubuntu', as_='builder') == 'FROM ubuntu AS builder'
assert _from('alpine') == 'FROM alpine'assert _run('apt-get update') == 'RUN apt-get update'
r = _run(['apt-get update', 'apt-get install -y curl'])
assert 'apt-get update && ' in r
assert 'apt-get install -y curl' in rassert _apt_install('curl', 'wget', y=True) == 'RUN apt-get update && apt-get install -y curl wget'
assert _apt_install('git') == 'RUN apt-get update && apt-get install git'assert _cmd(['python', 'app.py']) == 'CMD ["python", "app.py"]'
assert _cmd('echo hello') == 'CMD echo hello'assert _copy('.', '/app') == 'COPY . /app'
assert _copy('/build/out', '/app', from_='builder') == 'COPY --from=builder /build/out /app'
assert _copy('app/', '.', link=True) == 'COPY --link app/ .'
assert _copy('/app', '/app', from_='builder', link=True) == 'COPY --from=builder --link /app /app'assert _workdir('/app') == 'WORKDIR /app'assert _env('PATH', '/usr/local/bin') == 'ENV PATH=/usr/local/bin'
assert _env('DEBIAN_FRONTEND=noninteractive') == 'ENV DEBIAN_FRONTEND=noninteractive'assert _expose(8080) == 'EXPOSE 8080'assert _entrypoint(['python', '-m', 'flask']) == 'ENTRYPOINT ["python", "-m", "flask"]'assert _arg('VERSION', '1.0') == 'ARG VERSION=1.0'
assert _arg('VERSION') == 'ARG VERSION'assert _label(version='1.0', maintainer='me') == 'LABEL version="1.0" maintainer="me"'assert _volume('/data') == 'VOLUME /data'
assert _volume(['/data', '/logs']) == 'VOLUME ["/data", "/logs"]'assert _shell(['/bin/bash', '-c']) == 'SHELL ["/bin/bash", "-c"]'assert 'CMD curl' in _healthcheck('curl -f http://localhost/', i='30s')
assert _healthcheck('curl localhost', i='30s', t='10s') == 'HEALTHCHECK --interval=30s --timeout=10s CMD curl localhost'
assert _healthcheck('curl localhost') == 'HEALTHCHECK CMD curl localhost'assert _on_build(_run('echo triggered')) == 'ONBUILD RUN echo triggered'Dockerfile Builder
The Dockerfile class provides a fluent interface for building Dockerfiles. Start with a base image, chain instruction methods, then render or save.
Each method is one line – it creates an instruction and appends it, returning self for chaining.
parsed = _parse("# comment\nFROM python:3.11\nRUN apt-get update && \\\n apt-get install -y curl\nCOPY . /app")
print(parsed)
assert len(parsed) == 3
assert parsed[0] == 'FROM python:3.11'
assert 'apt-get install -y curl' in parsed[1]Dockerfile
def Dockerfile(
items:NoneType=None, rest:VAR_POSITIONAL, use_list:bool=False, match:NoneType=None
):
Fluent builder for Dockerfiles
df = (Dockerfile().from_('python:3.11-slim')
.run('pip install flask')
.copy('.', '/app')
.workdir('/app')
.expose(5000)
.cmd(['python', 'app.py']))
expected = """FROM python:3.11-slim
RUN pip install flask
COPY . /app
WORKDIR /app
EXPOSE 5000
CMD [\"python\", \"app.py\"]"""
assert str(df) == expected
print(df)# run_mount: cache mounts for fast rebuilds
df = (Dockerfile().from_('python:3.12-slim')
.run_mount('pip install -r requirements.txt', target='/root/.cache/pip')
.run_mount('uv sync --frozen', target='/root/.cache/uv')
.run_mount('apt-get install -y curl', type='cache', target='/var/cache/apt'))
s = str(df)
assert "RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt" in s
assert "RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen" in s
print(df)Multi-stage builds work naturally:
df = (Dockerfile().from_('golang:1.21', as_='builder')
.workdir('/src')
.copy('.', '.')
.run('go build -o /app')
.from_('alpine')
.copy('/app', '/app', from_='builder')
.cmd(['/app']))
assert 'FROM golang:1.21 AS builder' in str(df)
assert 'COPY --from=builder /app /app' in str(df)
print(df)Multi-command RUN chains with &&:
df = (Dockerfile().from_('ubuntu:22.04').run(['apt-get update', 'apt-get install -y python3', 'rm -rf /var/lib/apt/lists/*']))
print(df)Loading from an existing Dockerfile
Use Dockerfile.load() to read an existing Dockerfile. save() returns the Path it wrote to.
import tempfile
tmp = tempfile.mkdtemp()
Path(f'{tmp}/Dockerfile').write_text("# My app\nFROM python:3.11-slim\nRUN apt-get update && \\\n apt-get install -y curl\nCOPY . /app\nCMD [\"python\", \"app.py\"]")
# Load existing Dockerfile
df = Dockerfile.load(f'{tmp}/Dockerfile')
assert len(df) == 4
assert df[0] == 'FROM python:3.11-slim'
# save returns the path and writes the file
p = df.save(f'{tmp}/Dockerfile')
assert Path(p).exists()
# chain after loading
df2 = df.run('echo hi')
assert len(df2) == 5
print(df)FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl
COPY . /app
CMD ["python", "app.py"]
Build, Run, Test
These top-level functions wrap the Docker CLI for the common workflow: build an image from a Dockerfile, run a container, and test that a command succeeds inside an image.
Requires Docker daemon
The functions below need a running Docker daemon.
Cli
def Cli(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
*Base: call builds flags → _run(), getattr dispatches subcommands*
Docker
def Docker(
no_creds:bool=False
):
Wrap docker CLI: getattr dispatches subcommands, kwargs become flags
calldocker
def calldocker(
args:VAR_POSITIONAL, no_creds:bool=False
):
Run a docker CLI command, return stdout. Respects DOCKR_RUNTIME env var (default: docker).
Dockerfile.build
def build(
df:Dockerfile, tag:str=None, path:str='.', rm:bool=True, no_creds:bool=False
):
Build image from Dockerfile. path is the build context directory.
test
def test(
img_or_tag:str, cmd
):
Run cmd in image, return True if exit code 0
run
def run(
img_or_tag:str, detach:bool=False, ports:NoneType=None, name:NoneType=None, remove:bool=False,
command:NoneType=None
):
Run a container, return container ID (detached) or output
Convenience functions
containers
def containers(
all:bool=False
):
List running containers (names)
images
def images(
):
List image tags
stop
def stop(
name_or_id:str
):
Stop a container by name or ID
logs
def logs(
name_or_id:str, n:int=10
):
Tail logs of a container
rm
def rm(
name_or_id:str, force:bool=False
):
Remove a container by name or ID
rmi
def rmi(
image:str, force:bool=False
):
Remove an image by name or ID
Example: FastHTML app with uv
A realistic Dockerfile for a FastHTML app that uses uv for dependency management, installs system packages, and is designed to run with a mounted volume for persistent data.
df = (Dockerfile().from_('python', '3.12-slim')
.apt_install('curl', 'sqlite3', y=True)
.run('pip install uv')
.workdir('/app')
.copy('pyproject.toml', '.')
.run('uv export --no-hashes -o requirements.txt && pip install -r requirements.txt')
.copy('.', '.')
.volume('/app/data')
.expose(5001)
.cmd(['python', 'main.py']))
print(df)import tempfile
tmp = tempfile.mkdtemp()
df = Dockerfile().from_('alpine').run('echo hello > /greeting.txt').cmd(['cat', '/greeting.txt'])
try:
tag = df.build(tag='fastops-test:hello', path=tmp, no_creds=True)
print(f'Built: {tag}')
out = run(tag, remove=True)
print(f'Output: {out}')
rmi(tag)
print('Cleaned up.')
except Exception as e: print(f'Docker not available: {e}')Docker not available: name 'os' is not defined
End-to-end: FastHTML + FastLite todo app
import tempfile, os
app_dir = Path(tempfile.mkdtemp()) / 'fasthtml-todo'
app_dir.mkdir()
# --- main.py: FastHTML + FastLite todo app ---
(app_dir / 'main.py').write_text('''import json as jsonlib
from fasthtml.common import *
db = database('data/todos.db')
todos = db.t.todos
if todos not in db.t: todos.create(id=int, title=str, done=bool, pk='id')
Todo = todos.dataclass()
app, rt = fast_app(live=False)
@rt('/')
def get():
items = [Li(f"{'✓' if t.done else '○'} {t.title}", id=f'todo-{t.id}') for t in todos()]
return Titled('Todos',
Ul(*items),
Form(Input(name='title', placeholder='New todo...'), Button('Add'), action='/add', method='post'))
@rt('/add', methods=['post'])
def post(title: str):
todos.insert(Todo(title=title, done=False))
return Redirect('/')
@rt('/api/todos')
def api():
data = [dict(id=t.id, title=t.title, done=t.done) for t in todos()]
return Response(jsonlib.dumps(data), media_type='application/json')
serve(host='0.0.0.0', port=5001)
''')
# --- requirements.txt ---
(app_dir / 'requirements.txt').write_text('python-fasthtml\n')
print(f'App dir: {app_dir}')
print('Files:', os.listdir(app_dir))App dir: /tmp/tmpatb77qy_/fasthtml-todo
Files: ['requirements.txt', 'main.py']
df = (Dockerfile()
.from_('python', '3.12-slim')
.workdir('/app')
.copy('requirements.txt', '.')
.run('pip install --no-cache-dir -r requirements.txt')
.copy('.', '.')
.volume('/app/data')
.expose(5001)
.cmd(['python', 'main.py']))
print(df)import time
from fastcore.net import urlread, urljson
tag = 'fastops-fasthtml:latest'
name = 'fastops-fasthtml-demo'
try:
df.build(tag=tag, path=str(app_dir), no_creds=True)
print(f'Built: {tag}')
try: rm(name, force=True)
except: pass
cid = run(tag, detach=True, ports={'5001/tcp': 5001}, name=name)
print(f'Container: {cid[:12]}')
time.sleep(3)
# Add some todos via POST
url = 'http://localhost:5001'
for t in ['Buy milk', 'Write docs', 'Ship fastops']: urlread(f'{url}/add', title=t)
# Fetch the JSON API
for t in urljson(f'{url}/api/todos'): print(f" {'✓' if t['done'] else '○'} {t['title']}")
print(f'\nLogs:')
print(logs(name, n=3))
except IOError as e: print(f'Docker not available: {e}')
finally:
try: rm(name, force=True)
except: pass
try: rmi(tag)
except: pass
print('Cleaned up.')Built: fastops-fasthtml:latest
Container: aed89d887421
Docker not available: <urlopen error [Errno 111] Connection refused>
Cleaned up.