Run a machine in a container
The installer expects launchd or systemd to keep the runner alive, and the credential store expects either an OS keystore or a machine id. A container usually has none of them.
Three things break, in this order. Fix them in this order.
1. The service was not registered
registering the service systemctl is not on this system, so the unit file was written
but nothing was registered; run 'norn runner start' yourself
norn runner install wrote ~/.config/systemd/user/norn-runner.service and stopped there. The unit
is a user unit, so it is waiting for a systemd user session that a container does not have.
Do not install systemd. It has to run as PID 1, and in a container PID 1 is the entrypoint or
the workspace agent. apt-get install systemd puts the binaries on disk and then every call fails
with System has not been booted with systemd as init system (PID 1). Can't operate. Making it work
needs a privileged container with cgroup mounts, which is not what a dev environment should be.
The installer connects after it registers the service and waits for the daemon to answer. When registration is a no-op the wait fails and the script exits before the connect step, so the token you passed was never spent. You still have to connect by hand below.
2. There is no machine id
this machine has no usable OS keystore, so there is nowhere safe to keep the credential.
On a headless Linux box, pass --insecure-store …
this machine has no machine id, so an encrypted file store has nothing to key itself with.
With no keystore, --insecure-store writes the credential to ~/.norn/credentials.enc, encrypted
with a key derived from /etc/machine-id — or /var/lib/dbus/machine-id if the first is missing or
empty. Container images usually ship neither.
Generate one into your persistent home first, then copy it into place:
mkdir -p ~/.norn
[ -f ~/.norn/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > ~/.norn/machine-id
sudo cp ~/.norn/machine-id /etc/machine-id
That ordering is the whole recipe, and step 4 is why.
Writing to /etc needs root. If the container has no passwordless sudo, this has to come from
the image or the platform's template.
3. Connect and start it
NORN_TOKEN=nrn_… norn runner connect --name build-box --insecure-store
setsid nohup norn runner start >> ~/.norn/logs/runner.log 2>&1 < /dev/null &
norn runner status
Pass the token in the environment. --token puts it in the argument list, where anything that can
run ps sees it.
setsid is not decoration. A plain & leaves the process in your terminal's process group, and
both Docker and cloud workspaces kill that group when the session ends — the runner dies a few
minutes after you close the tab. setsid detaches it, and it reparents to PID 1.
4. The restart trap
The encryption key comes from the contents of /etc/machine-id. Your home is on a persistent
volume; /etc/machine-id is in the container's writable layer and is destroyed when the container
is recreated.
Generate it straight into /etc and the next restart gives you a different id,
~/.norn/credentials.enc can no longer be decrypted, and the runner fails in a way that reads like
corruption. You would have to connect again and revoke the orphaned machine in Norn.
Keeping the canonical copy in ~/.norn/machine-id and restoring it on every start is what avoids
that. Wire the same lines into whatever runs at container start:
- Cloud dev workspace
- Docker Compose
In the workspace template, as a script that runs on start:
resource "coder_script" "norn_runner" {
agent_id = coder_agent.main.id
display_name = "Norn Runner"
run_on_start = true
start_blocks_login = false
script = <<-EOT
mkdir -p ~/.norn/logs
[ -f ~/.norn/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > ~/.norn/machine-id
sudo cp ~/.norn/machine-id /etc/machine-id
setsid nohup ~/.local/bin/norn runner start >> ~/.norn/logs/runner.log 2>&1 < /dev/null &
EOT
}
start_blocks_login = false matters. Without it the workspace waits on a process that never exits.
Keep both the state directory and the machine id on a named volume, and make the entrypoint restore it:
services:
runner:
image: your-image
volumes:
- norn-state:/home/norn/.norn
entrypoint:
- /bin/sh
- -c
- |
mkdir -p /home/norn/.norn/logs
[ -f /home/norn/.norn/machine-id ] ||
tr -d - < /proc/sys/kernel/random/uuid > /home/norn/.norn/machine-id
cp /home/norn/.norn/machine-id /etc/machine-id
exec norn runner start
restart: unless-stopped
volumes:
norn-state:
exec in the foreground with restart: unless-stopped gives you supervision, so setsid is not
needed here — Docker is the service manager.
Do not bake a machine id into the image. Every container from that image would share one key, so anyone with the image and a copy of the volume could decrypt the credential.
Restart it if it falls over
Neither hook brings the runner back after a crash. Docker's restart: unless-stopped does. For the
workspace case, supervise it yourself:
cat > ~/.local/bin/norn-runner-keepalive <<'EOF'
#!/usr/bin/env bash
mkdir -p ~/.norn/logs
while true; do
~/.local/bin/norn runner start >> ~/.norn/logs/runner.log 2>&1
echo "runner exited $?, restarting in 5s" >> ~/.norn/logs/runner.log
sleep 5
done
EOF
chmod +x ~/.local/bin/norn-runner-keepalive
Run that from the start hook instead of norn runner start.
The runner binds ~/.norn/runner.sock, so a second copy refuses to start rather than racing the
first. Running the command twice by accident is safe.
What --insecure-store costs
The flag is named that on purpose. The credential is encrypted with a key derived from a file anybody with a shell in the container can read. Against somebody who already has your shell it is barely better than plaintext. What it protects is the volume being copied off the host on its own.
That is a reasonable trade for a dev container, and the blast radius is one machine — the credential is bound to this runner, and revoking it leaves the agent's other machines and its MCP access alone.
On a laptop, do not use the flag. Keychain and Secret Service are what the runner wants.
What else the container needs
| Needed for | What |
|---|---|
| Every run | git, and the repositories reachable with the credentials the container has |
| Running the work | claude or codex, installed and signed in as the same user the runner runs as |
| The Docker runtime | A Docker socket the container can reach. Without one, set runtime: process in ~/.norn/runner.yaml |
| Previews | Outbound HTTPS to the preview gateway. norn runner status says whether it is reachable |
Check it
norn runner status
The machine appears under Workspace settings → Runners within a few seconds of connecting.
Restart the container once and run norn runner status again — if it still reports the same
identity, the machine id is being restored properly.