metznet-channel/docker
noah metz 2721615497 Move channels.scm, scripts/, docker/ in from the separate system repo
That repo (system) is being deprecated in favor of keeping deploy
tooling alongside the channel it deploys -- cloning this repo alone is
now enough to either build docker images for local testing (docker/,
docker-compose.yml, scripts/guix-docker.sh) or reconfigure a real
machine (channels.scm registers this channel plus nonguix/games/guix;
scripts/qemu.sh and network.sh for the KVM-based workflow on real
Linux hosts).

docker-compose.yml and scripts/guix-docker.sh updated for the merge:
no more local-channel indirection (docker/channels.local.scm, a
file:// bind mount to a sibling repo) -- guix system docker-image now
passes -L /workspace directly against this checkout, so uncommitted
edits are picked up immediately without a commit+pull round-trip.
2026-07-28 20:10:51 -06:00
..
Dockerfile Move channels.scm, scripts/, docker/ in from the separate system repo 2026-07-28 20:10:51 -06:00
README.md Move channels.scm, scripts/, docker/ in from the separate system repo 2026-07-28 20:10:51 -06:00
entrypoint.sh Move channels.scm, scripts/, docker/ in from the separate system repo 2026-07-28 20:10:51 -06:00

README.md

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)

  • netbase is installed explicitly: debian:bookworm-slim ships without /etc/services, and Guile's getaddrinfo needs it to resolve https/ http as service names. Without it, every network fetch inside Guix (substitutes, source downloads, even channel git clones from the guix pull subprocess) fails with a cryptic In procedure getaddrinfo: Servname not supported for ai_socktype — nothing to do with DNS or certs despite how it reads.
  • entrypoint.sh starts guix-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, so scripts/guix-docker.sh image always passes --no-grafts (uses un-grafted originals instead — fine for testing).
  • It also always kills and restarts guix-daemon rather than checking if its socket already exists: /var/guix is 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 with assertion failed [true_path_length_self >= 0] — a Rosetta bug, not a Guix issue. Doesn't affect the actual build; just don't rely on docker exec <container> ps to check progress. du -sh /gnu/store or 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.scm has no introduction, so guix pull doesn't authenticate it (unlike nonguix/games, which do).
  • metznet/machines/otto.scm (Noah's laptop) has firefox and 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) and firefox (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-sssd disables tests (one fails only under Docker's emulated filesystem/Rosetta, not a real regression) and strips the --install-layout=deb flag setuptools no longer accepts; metznet-kitty disables tests for the same reason (O_TMPFILE atomicity isn't honored by Docker Desktop's virtualized filesystem on macOS); openldap-slapd forces -std=gnu17 plus several -Wno-* flags because GCC 14 turned some of its 25-year-old autoconf checks into hard errors, and adds libxcrypt as an input since glibc dropped crypt().