# The base Template: a Machine a developer or an agent can work in. # # This is an ordinary Dockerfile and `podman build` builds it, but the result is # not run as a container: the Template builder (internal/hostagent/template) # exports this image's filesystem onto a ZFS zvol as ext4 and Firecracker boots # it as PID 1 under systemd. Three things follow from that and none of them are # normal Docker practice: # # * systemd and systemd-sysv are installed, because /sbin/init has to exist # and has to be systemd. A container image has no init; a Machine is a whole # Linux userspace. # * Nothing is trimmed to keep the image small. There is no registry pull on # the critical path: the Template is a zvol on the Host and a Machine is a # `zfs clone` of it, so bytes here cost disk once and nothing per Machine. # * There is no CMD or ENTRYPOINT, and the builder ignores them if a Tenant's # Dockerfile has them. A Machine boots; it does not run one process. # # envd, sshd and the runin user come from the Host's platform drive at # boot (ADR 0014); the builder adds only a few files under /etc after the # export. This file is optional for a Tenant: any Linux image boots, and this # one is what gives a Machine systemd and the usual tools. FROM ubuntu:24.04 # One layer, and the apt lists are dropped at the end of it so they are not # carried into the rootfs. DEBIAN_FRONTEND keeps tzdata and friends from # blocking on a prompt no one can answer. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get update \ && apt-get install -y --no-install-recommends \ systemd \ systemd-sysv \ dbus \ udev \ util-linux \ openssh-server \ sudo \ ca-certificates \ curl \ git \ build-essential \ python3 \ python3-venv \ python3-pip \ iproute2 \ iputils-ping \ less \ nano \ vim-tiny \ jq \ unzip \ xz-utils \ tzdata \ locales \ && rm -rf /var/lib/apt/lists/* # The account a Tenant's work happens under, and the one the Front Door logs # into (ticket 110). The builder makes sure this user exists whatever a # Dockerfile did, but creating it here is what gives it a populated home # directory and a shell a person would want. Machines from Templates built # before the rename have forgesmith instead and keep it. RUN useradd --create-home --shell /bin/bash --user-group runin \ && printf 'runin ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/runin \ && chmod 0440 /etc/sudoers.d/runin # One ed25519 host key, so sshd can start on the very first boot, and an # sshd_config that names it and nothing else. # # It is the Template's key and never a Machine's: the Host mints a fresh # ed25519 key per Machine and ships it in the Identity, which envd writes over # this one before sshd is reachable (guest/envd/DIVERGENCES.md, D4 and D7). # # ed25519 alone, and no `ssh-keygen -A`, because -A also mints RSA 3072 and # ECDSA. Doing that inside a Machine was 1.3 seconds of a Golden Snapshot # restore and several times that on a Machine that had just resumed and was # short of CPU (ticket 020). Nothing in front of a Machine's sshd wants another # algorithm: ticket 040 terminates SSH at sshpiper, which is handed this key's # public half so the proxy-to-guest hop is verified rather than trusted on # first use. # # The drop-in is 20- so it sorts after anything an image ships itself. # Ordering does not matter for HostKey -- sshd accumulates them and using any # at all replaces the built-in defaults -- but it keeps the file out of the # way. # The removal comes first: openssh-server's postinst has already generated a # full set, and ssh-keygen asks "Overwrite (y/n)?" on a key that is there, which # in a build with no terminal is a build that never finishes. RUN rm -f /etc/ssh/ssh_host_* \ && ssh-keygen -q -t ed25519 -N '' -C runin-template -f /etc/ssh/ssh_host_ed25519_key \ && install -d -m 0755 /etc/ssh/sshd_config.d \ && printf '%s\n' \ '# Written by templates/base/Dockerfile (ticket 020).' \ '#' \ '# One algorithm. The Host generates this key per Machine and hands the' \ '# public half to the Front Door, so there is nothing for RSA or ECDSA' \ '# to be compatible with, and generating them cost seconds of every' \ '# Golden Snapshot restore.' \ 'HostKey /etc/ssh/ssh_host_ed25519_key' \ > /etc/ssh/sshd_config.d/20-runin-hostkeys.conf # An idle Machine should cost the Host nothing (ticket 019). Ubuntu's stock # timers are the opposite of that: apt-daily and apt-daily-upgrade wake the # Machine twice a day to talk to archive.ubuntu.com, which a Tenant's egress # allowlist will refuse anyway; motd-news fetches a news feed; dpkg-db-backup # and e2scrub_all walk the disk. None of them are how a Machine is kept up to # date -- that is a new Template version -- so all five are masked. # # A mask is a symlink to /dev/null in /etc/systemd/system, which is exactly # what `systemctl mask` writes. Enabling a timer only creates symlinks and # works in the build container without a running systemd. # # fstrim.timer trims free ext4 ranges weekly. With a discard-capable # Firecracker drive, ZFS can release blocks no Snapshot or Fork still retains. # Enable it explicitly below; leaving a unit unmasked does not enable it. RUN for timer in \ apt-daily.timer \ apt-daily-upgrade.timer \ motd-news.timer \ dpkg-db-backup.timer \ e2scrub_all.timer; do \ ln -sf /dev/null "/etc/systemd/system/$timer"; \ done \ && systemctl enable fstrim.timer # Nothing here reads these at build time; they are the environment a shell in a # Machine gets. ENV LANG=C.UTF-8 \ LC_ALL=C.UTF-8 \ DEBIAN_FRONTEND= WORKDIR /home/runin