core

Dockerfile generation, image building, container running, and testing

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.

assert _instr('RUN', 'echo hello') == 'RUN echo hello'

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 r
assert _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]

source

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.


source

Cli


def Cli(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

*Base: call builds flags → _run(), getattr dispatches subcommands*


source

Docker


def Docker(
    no_creds:bool=False
):

Wrap docker CLI: getattr dispatches subcommands, kwargs become flags


source

calldocker


def calldocker(
    args:VAR_POSITIONAL, no_creds:bool=False
):

Run a docker CLI command, return stdout. Respects DOCKR_RUNTIME env var (default: docker).


source

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.


source

test


def test(
    img_or_tag:str, cmd
):

Run cmd in image, return True if exit code 0


source

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


source

containers


def containers(
    all:bool=False
):

List running containers (names)


source

images


def images(
    
):

List image tags


source

stop


def stop(
    name_or_id:str
):

Stop a container by name or ID


source

logs


def logs(
    name_or_id:str, n:int=10
):

Tail logs of a container


source

rm


def rm(
    name_or_id:str, force:bool=False
):

Remove a container by name or ID


source

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.