6.8 KiB
Guix-in-Docker build environment
Guix has no native macOS build, so this runs a Debian container with Guix
installed (official binary tarball) as the build machine, and uses
guix system docker-image to turn metznet/machines/*.scm into Docker
images you can docker load/docker run directly in Docker Desktop. No
QEMU/KVM required, which is why it works on macOS where scripts/qemu.sh
(KVM-only) does not.
The builder container runs as linux/amd64 (via Rosetta on Apple
Silicon), matching real deployment hardware — nonguix substitutes are
x86_64-focused, so building on aarch64 forces a lot of expensive
from-source builds (and occasionally hits GCC/architecture edge cases)
that just don't happen on amd64.
Why this works without a bootloader/kernel/real disks
guix system docker-image (and guix system container) transform the
operating-system value before building: the real kernel, bootloader,
and file-systems fields are all replaced/ignored (see
containerized-operating-system in Guix's gnu/system/linux-container.scm),
along with services that make no sense in a container (mingetty,
agetty, static networking). Nothing in metznet/machines/*.scm needs to
change for this to work.
What isn't swapped out: services like dhcpcd-service-type and
openvpn-client-service-type (in %metznet-server-services) will still
try to run and will likely fail/hang inside a container (no real DHCP
server or VPN endpoint reachable) — that's expected. Pass -N/--network
(the image subcommand below always does) so a dummy networking
shepherd service is provided up front; other services that just
(requirement '(networking ...)) (openssh, slapd, kdc) come up fine
regardless of whether the DHCP/VPN services themselves succeed.
Container internals (why the Dockerfile/entrypoint look the way they do)
netbaseis installed explicitly:debian:bookworm-slimships without/etc/services, and Guile'sgetaddrinfoneeds it to resolvehttps/httpas service names. Without it, every network fetch inside Guix (substitutes, source downloads, even channel git clones from the guix pull subprocess) fails with a crypticIn procedure getaddrinfo: Servname not supported for ai_socktype— nothing to do with DNS or certs despite how it reads.entrypoint.shstartsguix-daemon --disable-chroot. Sandboxed builds normally isolate via Linux namespaces (unshare/clone), which Docker blocks by default even for root (clone: Operation not permitted). We're already isolated by the outer container, so build isolation is traded away here rather than running the whole thing--privileged. One side effect: guix-daemon's store-path grafting (security-patch rewriting) hits a permission error creating output files under--disable-chroot, soscripts/guix-docker.sh imagealways passes--no-grafts(uses un-grafted originals instead — fine for testing).- It also always kills and restarts
guix-daemonrather than checking if its socket already exists:/var/guixis a persistent volume, so a stale socket file from a previous (already-exited) container can still be sitting there even though nothing is listening on it. ps(and possibly other procps tools) crash reliably under Rosetta withassertion failed [true_path_length_self >= 0]— a Rosetta bug, not a Guix issue. Doesn't affect the actual build; just don't rely ondocker exec <container> psto check progress.du -sh /gnu/storeor watching the log works fine.
One-time setup
scripts/guix-docker.sh build # builds the builder image (~1-2 min)
scripts/guix-docker.sh pull # guix pull -C channels.scm
pull needs network access to gitlab.com (nonguix, nonguix-games),
git.metznet.ca, and Savannah/git.guix.gnu.org. Re-run it whenever
channels.scm/channels.d/*.scm change. The main guix channel alone
is ~14.8k commits to authenticate the first time, which is genuinely slow
(several minutes of CPU) — after that it's cached (see below) and
re-pulls are fast.
Guix's store (/gnu), daemon state (/var/guix), and git-checkout/
authentication cache (/root/.cache, notably ~/.cache/guix/checkouts
and ~/.cache/guix/authentication) all live in named Docker volumes, so
none of that expensive work repeats across runs. docker compose down -v
wipes everything if you ever want a clean slate (necessary if you ever
switch the platform: in docker-compose.yml — a store built for one
architecture isn't usable from another).
Building a system as a docker image
scripts/guix-docker.sh image metznet/machines/ldap.scm
docker load < build/ldap.tar.gz
docker run --rm -it <image-id-or-name> /run/current-system/profile/bin/bash --login
or do build+load+run in one step:
scripts/guix-docker.sh load-run metznet/machines/ldap.scm
image/load-run pass -L /workspace (this checkout, bind-mounted
live), so uncommitted local edits to any metznet/** module are picked
up directly — no commit/pull round-trip needed, unlike a real guix pull on a deployed machine.
DOMAIN_CAPS/DOMAIN_NAME default to METZNET.CA/metznet.ca in
docker-compose.yml, matching scripts/qemu.sh; override by exporting
them before calling guix-docker.sh, or in a .env file next to
docker-compose.yml.
Once running, herd status inside the container shows shepherd service
state — useful for checking whether e.g. slapd actually came up.
Known gotchas
channels.d/metznet.scmhas nointroduction, soguix pulldoesn't authenticate it (unlike nonguix/games, which do).metznet/machines/otto.scm(Noah's laptop) hasfirefoxand the ARM embedded toolchain (make-arm-none-eabi-toolchain-7-2018-q2-update) removed from its package list — both require building an entire LLVM/Clang toolchain from scratch (no substitute currently available, even on x86_64), which is impractical for routine testing. Add them back with(make-arm-none-eabi-toolchain-7-2018-q2-update)(a zero-arg constructor from(gnu packages embedded), not a bare package name — that changed upstream) andfirefox(from(nongnu packages mozilla)) if you need them for a real build/deploy.- Package-level quirks fixed in this channel to build at all against
current Guix (all in
metznet/packages/, not upstream Guix changes):metznet-sssddisables tests (one fails only under Docker's emulated filesystem/Rosetta, not a real regression) and strips the--install-layout=debflag setuptools no longer accepts;metznet-kittydisables tests for the same reason (O_TMPFILEatomicity isn't honored by Docker Desktop's virtualized filesystem on macOS);openldap-slapdforces-std=gnu17plus several-Wno-*flags because GCC 14 turned some of its 25-year-old autoconf checks into hard errors, and addslibxcryptas an input since glibc droppedcrypt().