# banger golden image — Debian bookworm sandbox for development + testing. # # Two sections: # 1. ESSENTIAL — what banger's lifecycle requires to boot the guest. # 2. OPINION — developer conveniences curated for banger sandboxes. # # Banger's guest agents (vsock agent, network bootstrap, first-boot unit) # are injected at `banger image pull` time, not baked here. Keeping them # out means this image stays portable enough to run in other contexts. FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 # -------- 1. ESSENTIAL -------- # Banger needs: an init (systemd + udev + dbus), sshd (the only # control channel), TLS roots + curl (first-boot installs + mise # installer), gnupg (build-time signing-key verification for the # Docker apt repo), iproute2 (debugging; `ip` is still useful even # when the kernel sets IP via cmdline). # # udev is a Recommends of the systemd package on Debian. With # --no-install-recommends it's skipped — and without it systemd never # activates device units, so fstab mounts of /dev/vdb (banger's work # disk) hang forever waiting for a device that is already enumerated # by the kernel but never "seen" by systemd. dbus gets the same # treatment for the same reason (system-bus-ness services wedge # without it). RUN apt-get update \ && apt-get install -y --no-install-recommends \ systemd systemd-sysv udev dbus \ openssh-server \ ca-certificates \ curl \ gnupg \ iproute2 \ && rm -rf /var/lib/apt/lists/* # -------- 2. OPINION -------- # Developer sandbox conveniences. Language runtimes are deliberately # absent — `mise` (below) handles per-repo `.mise.toml`/`.tool-versions` # on first `vm run`. # Core CLI + search/nav + build toolchain + lint/debug + editor/session. RUN apt-get update \ && apt-get install -y --no-install-recommends \ git jq less tree file unzip zip rsync \ ripgrep fd-find \ build-essential pkg-config make \ shellcheck sqlite3 \ iputils-ping dnsutils \ vim-tiny tmux htop \ && rm -rf /var/lib/apt/lists/* # Docker CE (with Compose v2 + buildx) from the official apt repo. # Nested-VM docker gives Compose workflows hostname/port isolation # per banger VM, which is a big part of the sandbox story. # # The apt key is verified against its published fingerprint before # we commit it to the signed-by keyring, so a tampered download (or # a TLS compromise against download.docker.com) cannot silently # swap in an attacker-controlled signing key. Fingerprint source: # https://docs.docker.com/engine/install/debian/#install-using-the-repository RUN set -eu; \ expected_fpr=9DC858229FC7DD38854AE2D88D81803C0EBFCD88; \ install -m 0755 -d /etc/apt/keyrings; \ curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.asc; \ got="$(gpg --with-colons --show-keys --fingerprint /tmp/docker.asc | awk -F: '/^fpr:/ {print $10; exit}')"; \ if [ "$got" != "$expected_fpr" ]; then \ echo "docker apt key fingerprint mismatch: got $got, want $expected_fpr" >&2; \ exit 1; \ fi; \ mv /tmp/docker.asc /etc/apt/keyrings/docker.asc; \ chmod a+r /etc/apt/keyrings/docker.asc; \ printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n' \ "$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list; \ apt-get update; \ apt-get install -y --no-install-recommends \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin; \ rm -rf /var/lib/apt/lists/* # mise — per-repo version manager. Installed from a pinned GitHub # release asset rather than `curl https://mise.run | sh` so a compromise # of the installer endpoint can't silently push arbitrary code into # the golden image. # # Update protocol: bump MISE_VERSION + MISE_SHA256 together. Source # for the hash is the `digest` field on the release asset from # `gh release view --repo jdx/mise --json assets`, or compute from # the downloaded file and cross-reference against SHASUMS256.txt on # the release page. ARG MISE_VERSION=v2026.4.18 ARG MISE_SHA256_AMD64=6ae2d5f0f23a2f2149bc5d9bf264fe0922a1da843f1903e453516c462b23cc1f RUN set -eux; \ arch="$(dpkg --print-architecture)"; \ if [ "$arch" != "amd64" ]; then \ echo "mise pin only tracks amd64; add a ${arch} hash to refresh" >&2; \ exit 1; \ fi; \ curl -fsSL -o /tmp/mise "https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-x64"; \ echo "${MISE_SHA256_AMD64} /tmp/mise" | sha256sum -c -; \ install -m 0755 /tmp/mise /usr/local/bin/mise; \ rm /tmp/mise; \ install -d /etc/profile.d; \ printf '%s\n' 'if [ -x /usr/local/bin/mise ]; then eval "$(/usr/local/bin/mise activate bash)"; fi' \ > /etc/profile.d/mise.sh; \ chmod 0644 /etc/profile.d/mise.sh # Default branch for any git init inside the sandbox. RUN git config --system init.defaultBranch main # `fd-find` installs as `fdfind` on Debian to avoid a long-standing name # clash. Expose the ergonomic name for interactive use. RUN ln -s /usr/bin/fdfind /usr/local/bin/fd # Strip per-image identity so every banger VM gets its own. # - /etc/machine-id: systemd-firstboot regenerates at boot when empty. # - SSH host keys: removed here; a ssh.service drop-in (below) runs # `ssh-keygen -A` before sshd so the VM's first boot generates a # unique set. # - /run/sshd tmpfiles entry: Debian's openssh-server package doesn't # ship one, and ssh.service's own `RuntimeDirectory=sshd` fires too # late for the ExecStartPre config test, so sshd -t blows up with # "Missing privilege separation directory: /run/sshd" before the # daemon ever starts. Creating the dir via tmpfiles.d runs early in # systemd-tmpfiles-setup, well before ssh.service kicks off. RUN : > /etc/machine-id \ && rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub \ && install -d /etc/systemd/system/ssh.service.d \ && printf '%s\n' \ '[Service]' \ '# Reset main unit ExecStartPre list: Debian ships `sshd -t` as' \ '# the first ExecStartPre, which fails on missing host keys and' \ '# short-circuits the service before ours gets a chance to run.' \ 'ExecStartPre=' \ 'ExecStartPre=/usr/bin/mkdir -p /run/sshd' \ 'ExecStartPre=/usr/bin/ssh-keygen -A' \ 'ExecStartPre=/usr/sbin/sshd -t' \ 'StandardOutput=journal+console' \ 'StandardError=journal+console' \ > /etc/systemd/system/ssh.service.d/banger.conf \ && rm -f /etc/systemd/system/ssh.service.d/regen-host-keys.conf \ && printf 'd /run/sshd 0755 root root -\n' > /usr/lib/tmpfiles.d/sshd.conf # No CMD / ENTRYPOINT: banger boots this via systemd as PID 1 after # first-boot, not via `docker run`.