The mkosi definitions of the images on hub.nspawn.org, the registry that nspawn pulls from. One OCI image per distribution, built by GitHub Actions from this repository, signed and pushed to the hub.
| Image | Tags | Definition |
|---|---|---|
archlinux |
rolling, latest |
mkosi.conf.d/arch/ |
debian |
13, trixie, latest; 12, bookworm; sid, unstable |
mkosi.conf.d/debian/ |
ubuntu |
26.04, resolute, latest; 24.04, noble; 22.04, jammy |
mkosi.conf.d/ubuntu/ |
fedora |
44, latest; 43; rawhide |
mkosi.conf.d/fedora/ |
centos |
10, latest; 9 |
mkosi.conf.d/el/ plus el-centos.conf |
almalinux |
10, latest; 9 |
mkosi.conf.d/el/ plus el-alma.conf |
rockylinux |
10, latest; 9 |
mkosi.conf.d/el/ plus el-rocky.conf |
opensuse |
tumbleweed, latest; 16.0, leap |
mkosi.conf.d/opensuse/ |
kali |
rolling, latest |
mkosi.conf.d/kali/ |
Services, each a profile on the Debian image, tagged with the version of the package
Debian ships (nginx:1.26.3, nginx:1.26, nginx:latest):
| Image | Package | Serves | Definition |
|---|---|---|---|
nginx |
nginx |
port 80, /var/www/html, /etc/nginx |
mkosi.profiles/nginx/ |
apache |
apache2 |
port 80, /var/www/html, /etc/apache2 |
mkosi.profiles/apache/ |
caddy |
caddy |
port 80, /usr/share/caddy, /etc/caddy |
mkosi.profiles/caddy/ |
postgresql |
postgresql (17) |
port 5432, /var/lib/postgresql, /etc/postgresql |
mkosi.profiles/postgresql/ |
mariadb |
mariadb-server |
port 3306, /var/lib/mysql, /etc/mysql |
mkosi.profiles/mariadb/ |
valkey |
valkey-server |
port 6379, /var/lib/valkey, /etc/valkey |
mkosi.profiles/valkey/ |
redis |
redis-server |
port 6379, /var/lib/redis, /etc/redis |
mkosi.profiles/redis/ |
memcached |
memcached |
port 11211, /etc/memcached.conf |
mkosi.profiles/memcached/ |
rabbitmq |
rabbitmq-server |
ports 5672 and 15672 (management), /var/lib/rabbitmq, /etc/rabbitmq |
mkosi.profiles/rabbitmq/ |
mosquitto |
mosquitto |
port 1883, /etc/mosquitto |
mkosi.profiles/mosquitto/ |
haproxy |
haproxy |
whatever /etc/haproxy/haproxy.cfg says |
mkosi.profiles/haproxy/ |
unbound |
unbound |
port 53, /etc/unbound/unbound.conf.d |
mkosi.profiles/unbound/ |
dnsmasq |
dnsmasq |
port 53, /etc/dnsmasq.d |
mkosi.profiles/dnsmasq/ |
bind9 |
bind9 |
port 53, /etc/bind |
mkosi.profiles/bind9/ |
samba |
samba |
port 445, share data on /srv/samba/data, /etc/samba/smb.conf |
mkosi.profiles/samba/ |
openssh |
openssh-server |
port 22, /root/.ssh/authorized_keys |
mkosi.profiles/openssh/ |
prometheus |
prometheus |
port 9090, /var/lib/prometheus, /etc/prometheus |
mkosi.profiles/prometheus/ |
node-exporter |
prometheus-node-exporter |
port 9100 | mkosi.profiles/node-exporter/ |
grafana |
grafana (Grafana Labs' repository) |
port 3000, /var/lib/grafana, /etc/grafana |
mkosi.profiles/grafana/ |
wordpress |
wordpress with nginx, PHP-FPM and MariaDB |
port 80, /var/lib/wordpress, /var/lib/mysql, /etc/wordpress |
mkosi.profiles/wordpress/ |
syncthing |
syncthing |
ports 8384 (GUI) and 22000, /var/lib/syncthing |
mkosi.profiles/syncthing/ |
forgejo |
the release binary from Codeberg | port 3000, /var/lib/forgejo, /etc/forgejo |
mkosi.profiles/forgejo/ |
They are full Debian machines with the service installed and enabled: nspawn shell
gets a root shell, systemctl status nginx inside says what it is doing, the paths above
are what to mount with -v to keep data and configuration on the host, and -p publishes
the port. Security updates come from Debian; the weekly rebuild picks them up.
Where Debian's default is to listen on localhost only, the images listen on every address
of the machine instead, as container images do: the machine is the boundary, and nothing
reaches it unless a port is published. Authentication stays as Debian ships it, so set a
password before publishing a port to the outside: PostgreSQL takes password logins from
the network once ALTER USER postgres PASSWORD '...' has run, MariaDB's root logs in
through the socket and network users are created from there, Valkey has no password until
requirepass is set, Mosquitto allows anonymous clients until a password file is added,
RabbitMQ's guest account works from localhost only, Grafana starts with admin/admin,
WordPress and Forgejo finish their installation in the browser on the first visit,
Syncthing's GUI has no password until one is set there, Samba's
data share is open to guests, and the OpenSSH image only lets root in with a key (mount
your authorized_keys at /root/.ssh/authorized_keys) and makes its host keys on the first
boot, so machines do not share them. The DNS images (unbound, dnsmasq, bind9) turn off
systemd-resolved's stub listener so that they own port 53.
Machines to work in: a toolbox per distribution, and one per language on Debian, all with
compilers, git and the usual tools. Mount your sources with -v ~/src:/root/src and they
stay a machine you come back to, not a build step.
| Image | Tags | Brings | Definition |
|---|---|---|---|
debian-devel |
13, trixie, latest |
build-essential, gdb, cmake, git, tmux | mkosi.profiles/devel/ |
ubuntu-devel |
26.04, resolute, latest |
the same on Ubuntu | mkosi.profiles/devel/ |
fedora-devel |
44, latest |
gcc, gdb, cmake, git, tmux | mkosi.profiles/devel/ |
archlinux-devel |
rolling, latest |
base-devel, gdb, cmake, git, tmux | mkosi.profiles/devel/ |
python |
the interpreter's version | Debian's Python with pip and venv | mkosi.profiles/python/ |
nodejs |
the runtime's version | Node.js 24 from NodeSource, with npm | mkosi.profiles/nodejs/ |
golang |
the toolchain's version | the current Go from go.dev in /usr/local/go |
mkosi.profiles/golang/ |
rust |
the toolchain's version | the current stable through rustup, under /usr/local |
mkosi.profiles/rust/ |
openjdk |
the JDK's version | Debian's OpenJDK 21 with Maven | mkosi.profiles/openjdk/ |
Node.js, Go and Rust come from upstream because Debian's are one or more releases behind;
each build verifies what it downloads (the NodeSource repository is signed, go.dev
publishes the sha256 of its archives, rustup's is next to the installer) and apt upgrade,
rustup update and the like keep working inside the machine.
Every build also gets a dated tag (fedora:44-20260922) to go back to: the first build of
a day owns that tag, a later one the same day moves the other tags but leaves it alone, so a
dated tag always names the same image. The hub keeps the last ten per repository. The images boot
systemd, log in as root with password root on the console, and carry systemd-networkd
and systemd-resolved, which is how they get their address and DNS on the nspawn bridge.
sudo nspawn pull fedora:44
sudo nspawn start fedora-44Every image the workflow pushes is signed with cosign
in keyless mode: no key exists to keep or to leak. GitHub's OIDC token identifies the
workflow run, Fulcio issues a short-lived certificate for that identity, the signature is
recorded in the Rekor transparency log and stored on the hub next to the image, as an OCI
referrer of its digest, so it covers every tag that points to that digest. What to trust is
therefore the identity of this workflow on master:
cosign verify \
--certificate-identity https://github.com/nspawn/mkosi-definitions/.github/workflows/mkosi.yml@refs/heads/master \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
hub.nspawn.org/fedora:44Each image carries a second signature, made with the project's key, whose public half is
cosign.pub in this repository: the hub's registry (zot) only verifies signatures against
public keys uploaded to it, and this is what makes it show the images as signed. It
verifies with cosign verify --key cosign.pub hub.nspawn.org/fedora:44. The keyless one
is the stronger claim, since it names the workflow that built the image rather than a key
that anyone able to run the workflow can use.
nspawn 1.5.0 and later verify one of the two before
downloading anything: cosign.pub and the identity above are built into it, so a pull
from the hub needs no configuration, pull says who signed the image, and an image without
a valid signature is refused unless --no-verify is given (see Signed
images in its
usage documentation). Two things follow for this repository:
- Images pushed to the hub by hand are not signed by this workflow, so nspawn refuses
them unless pulled with
--no-verify. - The key and the identity are what nspawn trusts, and the identity is the path of the
workflow file on
master. Since either signature satisfies it, the key can be rotated, or the workflow moved, one at a time without breaking pulls with the nspawn already installed; changing both before a matching nspawn release would stop every pull from the hub.
mkosi 27 or newer. The tools tree (ToolsTree=default) brings the package managers, so
the host only needs mkosi itself:
mkosi -B -d fedora -r 44 # mkosi.output/fedora_<date>_x86-64/, an OCI layout
mkosi -B -d debian -r trixie
mkosi -B --profile=disk -d fedora -r 44 vm # a bootable disk image, booted in a VM-B (--auto-bump) runs mkosi.bump, which sets the image version to today's date and
keeps it in mkosi.version; without it the output has no version in its name. Without
-r each distribution builds its default release (the one in its mkosi.conf).
mkosi.local.conf keeps a choice around, as in every mkosi project:
[Distribution]
Distribution=fedora
Release=44Test the result with nspawn on the same machine:
skopeo copy oci:mkosi.output/fedora_20260922_x86-64 docker://localhost:5000/fedora:testor push it to any registry nspawn pull can reach.
mkosi.conf output format, annotations and what every image shares
images.json the images the hub publishes: distribution, release, profile, tags
mkosi.conf.d/<distro>/ [Match] Distribution=..., default release, packages, postinst
mkosi.profiles/disk/ the bootable disk variant (kernel per distribution)
mkosi.profiles/<service>/ a service on the Debian image: its packages, its units, its text
mkosi.repart/ partitions of the disk variant
mkosi.bump the image version: the build date
cosign.pub the public half of the key the images are also signed with
.github/select-images.py which of them a change affects
.github/workflows/mkosi.yml one job per image to build
Edit the packages or the mkosi.postinst.chroot of the distribution and open a pull
request: the workflow builds the images your change affects, and keeps the package
manifest of each as an artifact, without pushing anything. A profile means the images
built with it, a distribution means the images of that distribution (its services
included), an entry added to or edited in images.json means that image, and the shared
configuration means all of them. Prose and the files under .github/ build nothing, so a
change to how the workflow builds (the mkosi version, for one) asks for a manual run.
.github/select-images.py decides, and the select job of the run prints the list. Once merged to master the
same images are built again, pushed to the hub and signed. The weekly run on Sunday rebuilds
everything, which is what picks up package updates. A manual run builds everything too,
unless given the images it should build (repo:release, separated by spaces):
gh workflow run mkosi.yml -f images="kali:kali-rolling fedora:44"To add a service, add a profile under mkosi.profiles/ (packages, a mkosi.postinst.chroot
that enables the units, the image id and the text) and an entry in images.json with the
package whose version becomes the tag. Something that Debian does not package, like Forgejo, is
installed by a mkosi.prepare script, the one mkosi runs with network access, that verifies
what it downloads and writes the version it installed into the image, to a file the matrix
entry names with versionfile.
To add a distribution or release, add its directory under mkosi.conf.d/ (and a kernel
entry under mkosi.profiles/disk/mkosi.conf.d/ if it should have a disk variant) and an
entry in the matrix of the workflow with the tags the hub should show.