Day-0 installer container that commissions a dmfdeploy platform cluster on a node you bring, through a localhost-only web UI.
Part of dmfdeploy, an independent open prototype that explores the ideas of the EBU Dynamic Media Facility on commodity infrastructure. Status: prototype, single-node sandbox.
New to the project vocabulary? See the DMF Glossary for project-coined terms (appliance, checkpoint, age key, answers-file, …).
A single container puts a friendly, localhost-only web UI on the existing
dmf-env env-creation and bin/
bootstrap toolchain. It drives a cluster from zero to verified. At each
checkpoint it offers a passphrase-wrapped backup as a browser download. You
can commission a cluster, save the backup wherever you choose, and then delete
the container with nothing left behind.
Reached as http://localhost, which is a browser secure context (clipboard
- WebAuthn work) with no certificate warning; HTTPS is opt-in for non-localhost access.
To see what a commissioned cluster does next, an 81-second recording of one media workload being provisioned through the console, with a plain account of what it does and does not show, is on the organization page.
What you get (the deliverable): a commissioned, verified cluster plus one or more passphrase-wrapped encrypted backups (browser downloads). The container is disposable / run-once; delete it when you're done and the backups + your passphrase are all that remain. It does not build or emit a deployable image; it commissions a cluster.
Architecture orientation: docs/ARCHITECTURE.md.
| Mode | Purpose |
|---|---|
| Create new | Collect inputs, render the env bundle, seal a downloadable passphrase-wrapped backup (checkpoint #1), then orchestrate the full sandbox bootstrap (pre-seed → seed → post-seed → configure → verify) with live logs and per-checkpoint downloads. |
| Manage | Restore an env by uploading a previously downloaded encrypted backup (passphrase-unwrapped), then re-run playbooks, upgrade, or tear down, offering a fresh downloadable backup on any change. |
- Stateless. All env state lives in a tmpfs scratch root; nothing durable is written to the host. After commissioning, the downloaded encrypted backups and the operator passphrase are the only artifacts that matter.
- Passphrase wraps the age key. The age private key rides inside the backup; the backup is encrypted by an operator passphrase (the single human-held secret). The operator keeps copies of the download for redundancy; the passphrase = confidentiality.
- Wraps, does not reimplement. The container drives
dmf-env'sinit-wizard.sh --non-interactiveandbin/scripts; the bash remains the single source of truth and a first-class CLI path. - Sandbox profile first (ADR-0031). Cloud (Hetzner) is a later phase.
To get started, the dmf-init launcher wraps Docker for you; it
starts the container with the correct flags, waits until it's ready, extracts the
one-time launch link, and opens your browser. No clone required:
From v0.4.0 on, each release publishes the launcher together with a SHA-256 checksum file, after the release's container image is built and pushed. Earlier releases have neither asset. The command below downloads both for one tagged release, checks the launcher against the checksum, and runs it only if the check passes. If a download fails, or the checksum file is missing, malformed, or doesn't match, the command stops before it runs anything.
The command does the following, in order:
- Creates a new empty temporary directory and changes to it, so a file that already exists in your current directory can never run or be overwritten. If the directory can't be created, the command stops before it downloads anything.
- Downloads
dmf-initandSHA256SUMSwith onecurlcall each, so a failed download stops the command. - Requires
SHA256SUMSto hold exactly one line: a 64-character lowercase hex digest, two spaces, and the namedmf-init. - Computes the digest of
dmf-initwithsha256sum(Linux) orshasum(macOS), and compares it with the digest fromSHA256SUMS. - Runs
bash dmf-init uponly if the digests are identical.
version=v0.4.0
base="https://github.com/dmfdeploy/dmf-init/releases/download/${version}"
dir="$(mktemp -d)" &&
[ -n "${dir}" ] &&
[ -d "${dir}" ] &&
cd "${dir}" &&
curl -fsSL -o dmf-init "${base}/dmf-init" &&
curl -fsSL -o SHA256SUMS "${base}/SHA256SUMS" &&
[ "$(grep -c '' SHA256SUMS)" -eq 1 ] &&
expected="$(sed -n -E 's/^([0-9a-f]{64}) dmf-init$/\1/p' SHA256SUMS)" &&
[ -n "${expected}" ] &&
actual="$({ sha256sum dmf-init 2>/dev/null || shasum -a 256 dmf-init; } | cut -d ' ' -f 1)" &&
[ "${actual}" = "${expected}" ] &&
bash dmf-init upThe command saves dmf-init and SHA256SUMS in the new directory and leaves
your shell there. The published launcher starts the container image that was
built for the same release, pinned by digest. To use a different image, pass
--image.
To choose a version, set version to a tag from the
releases page that lists both
dmf-init and SHA256SUMS as assets. Use a tag, never main. A checksum from
the same release catches a corrupted or partial download. It doesn't prove who
published the release, so read the launcher first if you want to review it. To
read it before you run it, replace the last line with less dmf-init, then run
bash dmf-init up yourself.
From a clone, run bin/dmf-init:
bin/dmf-init up # start + open the browser (prints the link too)
bin/dmf-init link # re-mint the single-use link and reopen it
bin/dmf-init status # is it running? show the current link
bin/dmf-init down # stop and remove the containerOptions for up: --port N (default 8000), --tls (HTTPS), --image REF,
--repo-base-url URL (private mirror). Run bin/dmf-init --help for details. The
launcher never weakens the security posture; it always publishes on 127.0.0.1
only and mounts the tmpfs data root. The explicit docker run invocations below
remain the advanced/reference path (and are what the launcher runs under the hood).
A BuildKit-capable Docker is mandatory; the Dockerfile pins
# syntax=docker/dockerfile:1.7 and bin/build-bundle.sh forces
DOCKER_BUILDKIT=1, so classic builders will fail. Docker 23.0+ is a
sensible minimum.
Pick based on whether the host has runtime network access:
| Networked host | Air-gapped host | |
|---|---|---|
| When | Host can reach the internet at runtime | Host has no runtime network |
| Image | ghcr.io/dmfdeploy/dmf-init:latest |
dmf-init:bundle |
| Repos | Fetched at startup | Baked into the tarball |
Networked (canonical ghcr image: simplest path):
docker run --rm -p 127.0.0.1:8000:8000 \
--tmpfs /tmp/dmf-init-data:exec \
ghcr.io/dmfdeploy/dmf-init:latest
# open the http://localhost:8000/?token=... printed in the logs
--tmpfs /tmp/dmf-init-data:execis required (ADR-0044): env secrets (age key, OpenBao keys) must stay in RAM and never touch host disk. The:execmount option is needed because the data root holds and runs the fetched wizard toolchain; without it Docker's defaultnoexecmount flag blocks script execution (rc 126). The mount is still tmpfs, so ADR-0044 (RAM-only secrets) holds. dmf-init refuses to start if its data root isn't tmpfs-backed or is mountednoexec. For a throwaway dev run on a non-tmpfs root, override with-e DMF_ALLOW_NON_TMPFS_DATA_ROOT=true(dev/test only).docker rmstays safe because nothing durable is written to host disk.
Repos are fetched from the public org https://github.com/dmfdeploy by default;
no extra env needed. To pull from a private mirror, override the base URL (and
optionally supply credentials):
docker run --rm -p 127.0.0.1:8000:8000 \
--tmpfs /tmp/dmf-init-data:exec \
-e DMF_REPO_BASE_URL=https://git.example.com/dmf \
ghcr.io/dmfdeploy/dmf-init:latestMulti-arch (amd64 + arm64), published by
.github/workflows/publish-image.yml.
Air-gapped (bundle: repos baked in via bin/build-bundle.sh):
# on a machine with Docker + the umbrella checked out:
bin/build-bundle.sh # → dmf-init-bundle-<version>.tar.gz
# copy that tarball to the clean host, then there:
docker load -i dmf-init-bundle-<version>.tar.gz
docker run --rm -p 127.0.0.1:8000:8000 --tmpfs /tmp/dmf-init-data:exec dmf-init:bundle
# open the http://localhost:8000/?token=... printed in the logsLoopback safety is the -p 127.0.0.1:… publish (never publish to a non-loopback
host interface). HTTPS is opt-in (-e DMF_TLS_ENABLED=true) for when you reach
it by a non-localhost address.
The setup-request gate is off by default. When you turn it on
(-e DMF_SETUP_REQUEST_GATE=true), the installer pauses for your first passkey and a
setup request ID from the DMF Console before it runs configure. Leave it off until
the dmf-infra support for setup requests lands, because the console can't issue an ID
before then. A run that is waiting at one of these pauses doesn't survive a
container restart, so start a new run if the container restarts. A run started with
one gate setting can't be retried after the setting changes; start a new run. For more information, see
dmfdeploy/dmfdeploy#844.
Lost your session / launch link? The session refreshes while you're active
and lasts well beyond a single run idle, but if it lapses (or you closed the tab)
the one-time launch link is spent. You don't need to restart; re-mint a fresh
link on the running container (preserving the in-progress run) by sending it
SIGHUP, then open the new link printed in the logs:
docker kill --signal=HUP <container> # e.g. the name from `docker ps`
docker logs --tail 3 <container> # copy the fresh open http://localhost:8000/?token=... lineCanonical spec:
DMF Init Bootstrap Container Plan 2026-06-02.
The installer provisions a node you bring: an SSH-reachable Debian, ARM64
host (a cloud VM, a spare box, a Raspberry Pi). The installer does not check the
Debian version itself. The running v0.1 sandbox is a Debian 13 (Trixie) ARM64
node with 4 vCPU, 16 GB RAM and a 60 GB disk, and the full stack uses about 7 GB
RAM and 30 GB disk at idle. Treat that as the minimum. The
dmf-infra baseline playbook also
handles Debian 12 (Bookworm). Other distros and architectures are untested for
now. Your own workstation OS doesn't matter; the installer runs in a browser. On the wizard's Target node step you supply:
- Node IP: an address reachable from this installer container. Do not use
localhostor127.0.0.1, which resolve to the container itself. - Ansible user: the SSH login on that node (
root, or the image's default user such asdebian). - Interface: the node's primary network interface (
eth0,ens3, …).
These fields are intentionally blank by default; fill them for your node.
macOS convenience (not the paved path):
dmf-env/bin/recreate-sandbox-vm.shspins up a local Lima VM (whose user / interface arelima/lima0). It requires macOS + Lima; the bring-a-Debian-node path above works from any workstation.
dmf-env: env tooling (init-wizard.sh,bin/scripts) the container wraps.dmf-infra: generic bootstrap playbooks/roles.dmf-runbooks: catalog launcher (post-seed).
AGENTS.md, CLAUDE.md and QWEN.md hold instructions for AI coding agents that help maintain this repository. Human contributors can skip them and start with CONTRIBUTING.md.
Apache License, Version 2.0. See LICENSE. Third-party components are listed in NOTICE.