# 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 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 /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()`.