Introduction
nix-hex-box is a nix-darwin module that configures an Apple Container based
aarch64-linux remote builder for Nix.
The module is designed for Darwin hosts that want to offload Linux derivations to a local virtualized builder while keeping the host configuration declarative.
Current design highlights:
- installs Apple
containerfrom the official signed release package - configures
nix.buildMachinesforssh-ng://container-builder - uses a pinned upstream
docker.io/nixos/nix:<version>builder image - manages durable state under
~/.local/state/hb - supports an optional bridge for the root
nix-daemon - wakes the builder on demand for user-side SSH access through
ProxyCommand - supports guest-side idle shutdown and recovery-oriented health checks
This book documents the module itself. If you use nix-hex-box from another
repo, that repo should only need a high-level integration guide.
Overview
nix-hex-box exports two identical module entry points:
darwinModules.defaultdarwinModules.container-builder
The main option namespace is services.container-builder.
When enabled, the module:
- installs the Apple Container runtime package when needed
- writes helper scripts and SSH configuration under
~/.local/state/hb - configures host-side SSH aliases for
nix-builderandcontainer-builder - configures
nix.buildMachinesso the host daemon can use the builder for Linux derivations - optionally loads a launch agent that exposes the localhost bridge used by the root daemon path
The helper entrypoint is hb, which provides status, repair, logs, and
inspection commands for the builder runtime.
Installation
Add the flake input and import the module into your Darwin host:
{
inputs.hexbox.url = "github:RobertDeRose/nix-hex-box";
outputs = inputs: {
darwinConfigurations.my-host = inputs.darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [
inputs.hexbox.darwinModules.default
];
};
};
}
The module is intended for nix-darwin hosts running Apple Container capable
macOS systems. The builder guest is always aarch64-linux.
Configuration
Minimal example:
services.container-builder = {
enable = true;
cpus = 4;
memory = "8G";
maxJobs = 4;
bridge.enable = true;
};
Common settings to review first:
hostAliasportlistenAddresscpusmemorymaxJobsbridge.enableprotocolidleShutdown.enableidleShutdown.timeoutSecondsdns.*imageRepositorynixVersion
The default image is the upstream pinned image:
docker.io/nixos/nix:2.34.6
The container guest writes a minimal nix.conf that uses
https://cache.nixos.org/ by default.
Current default behavior to keep in mind:
bridge.enable = trueprotocol = "ssh-ng"hostAlias = "container-builder"listenAddress = "127.0.0.1"port = 2222
Runtime Model
Durable state lives under:
~/.local/state/hb
The current runtime model is:
- the builder container is generation-aware
- helper scripts are rewritten declaratively on activation
- the user-side SSH path wakes the builder on demand with
ProxyCommand - the root
nix-daemonpath can use the localhost bridge when enabled - idle shutdown runs inside the guest and stops
sshdafter inactivity
The builder uses the image’s built-in /nix directly. Build outputs live in
the container’s writable layer, so recreating the container loses any guest
local store writes that were not already available from substituters.
That design keeps the module simpler than the older overlay-based approach, but it also means the builder should be treated as a cache-backed disposable guest, not as a durable Linux machine image.
Generated Files
Activation writes the operational helper files into ~/.local/state/hb.
Important files include:
bootstrap-keys.shhexbox-bridgeinit.shproxy.shstart-container.shstop-container.shssh-wrapper.shssh_configssh_config_roothexbox-readiness.loghexbox-idle.loginit-debug.loghb
These files are the practical runtime interface to the builder. They are generated from the active Nix configuration and should not be edited manually.
Network And Access Paths
The module uses two related access paths.
User-side SSH path
The user SSH config points at ~/.local/state/hb/proxy.sh as a ProxyCommand.
That proxy:
- starts the Apple container system if needed
- starts the builder on demand
- waits for guest
sshd - resolves the current container IP
- relays SSH directly into the guest
This path is used for helper access such as ssh nix-builder true.
Root daemon path
The root nix-daemon path uses the generated root SSH config for
${cfg.hostAlias}. There are two transport modes.
Bridge mode
This is the default with bridge.enable = true.
The bridge launch agent exposes the configured host socket:
127.0.0.1:2222
and forwards incoming connections into the wake-and-relay path.
Direct published-port mode
When bridge.enable = false, the module publishes the container SSH port
directly with container run -p <listenAddress>:<port>:<containerPort>.
That avoids the bridge agent entirely, but it is a different host transport shape and should be chosen deliberately.
This split exists because the direct user path and the daemon-driven path have different compatibility constraints on macOS.
Verification And Recovery
Main helper entrypoint:
hb status
Recovery-aware verification path:
hb repair
Useful checks after activation:
hb status
hb repair
ssh nix-builder true
nix store ping --store ssh-ng://container-builder
nix build --max-jobs 0 --rebuild nixpkgs#legacyPackages.aarch64-linux.hello
hb repair attempts to recover the Apple container system before retrying the
builder startup path. It also verifies:
- container system health
- bridge agent presence
- current builder container status
- SSH handshake success
- cache reachability inside the guest
- remote store reachability from the host side
Other useful helper commands:
hb resethb restarthb sshhb inspecthb gc
Logs And Diagnostics
Runtime logs live in ~/.local/state/hb.
Common log files:
hexbox-readiness.loghexbox-idle.loginit-debug.loghexbox-bridge.out.loghexbox-bridge.err.log
Use the helper to read the most important logs:
hb logs readiness
hb logs bridge
hb logs bridge-out
hb logs boot
hb logs idle
These logs are usually the fastest way to determine whether a failure is in:
- Apple
containerruntime startup - guest init/bootstrap
- SSH readiness
- bridge/proxy relay behavior
Troubleshooting
Builder does not become SSH-ready
Start with:
hb repair
hb logs readiness
hb logs boot
Look for guest init failures, SSH startup problems, or bridge/proxy timeouts.
Apple container runtime looks unhealthy
The Apple container runtime is still an external mutable subsystem. The
module can reconcile configuration and containers, but it cannot guarantee the
runtime substrate is always healthy.
hb repair attempts a recovery by starting the container system with kernel
install enabled before retrying the builder.
Cache resolution fails inside the guest
The guest writes a minimal nix.conf and depends on working DNS and network
reachability to cache.nixos.org. If substitute downloads fail, check:
- guest DNS settings
- upstream cache availability
- host networking state
Container recreation lost previous build outputs
This is expected in the current runtime model. The guest store is not preserved across recreation; outputs are expected to come back from substituters.
Direct port mode behaves differently from bridge mode
If bridge.enable = false, the host connects through the directly published
container port instead of the bridge agent. Troubleshooting should then focus on
the published host socket and container port mapping rather than the launchd
bridge path.
Options
The main option namespace is services.container-builder.
Important options:
enablehostAliassshUserlistenAddressportcontainerPortworkingDirectoryusercontainerBinaryinstaller.urlinstaller.hashinstaller.versioncontainerNameimageRepositorynixVersioncpusmemorydns.serversdns.searchdns.optionsdns.domaindns.disablesystemssupportedFeaturesmandatoryFeaturesmaxJobsspeedFactorprotocolautoStartreadiness.timeoutSecondsreadiness.intervalSecondsidleShutdown.enableidleShutdown.timeoutSecondsbridge.enable
See modules/container-builder.nix for the authoritative option defaults and
types.
Design Notes
nix-hex-box currently follows these design choices:
- upstream pinned
nixos/niximage instead of a custom prebuilt image - generation-aware container recreation
- on-demand user-side startup through
ProxyCommand - optional bridge for the root daemon path, with direct published-port mode available when disabled
- guest-side idle shutdown using a lightweight watchdog
- explicit DNS configuration support for builder cache resolution
Known constraints:
- Apple
containerremains an external mutable runtime - the daemon path still depends on the localhost bridge in current deployments
- recreating the container loses guest-local build outputs
- host and guest behavior still depend on the health of Apple’s virtualization and networking layers
Historical design notes from earlier overlay-based experiments should no longer be treated as current behavior.