Skip to main content

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.

note

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:

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.

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 forWhat
Every rungit, and the repositories reachable with the credentials the container has
Running the workclaude or codex, installed and signed in as the same user the runner runs as
The Docker runtimeA Docker socket the container can reach. Without one, set runtime: process in ~/.norn/runner.yaml
PreviewsOutbound 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.