CodeHerderSearch⌘KRequest access →

Running the device server as a service

How to run ch device-server as a persistent background service with systemd (Linux), launchd (macOS), or Docker so it survives terminal logout and reboot.

Running ch device-server in a terminal works for a quick test, but the device goes Offline as soon as that terminal closes. Configure it as a background service and the device server starts automatically at login, restarts on crash, and persists across reboots — so agents can always pick up work without any manual action. If you’d rather the device clean up and take itself out of the fleet once it runs out of work, see Devices that clean up after themselves instead.

Before you start

One thing must be in place before setting up the service: a CLI credential. ch device-server authenticates to CodeHerder at startup using either CH_TOKEN (an API key) or a named profile. Mint an API key from Set up → Members or with ch member keys create — see Minting API keys. See Credentials and profiles for how to store it in a profile.

Register the device interactively once, before installing the service. Run ch device-server yourself from a terminal first and accept the one-time registration prompt — see How do I add a device? for what that prompt looks like and what it does. Skip this and install the service directly instead: the service starts and keeps running, but with no terminal to answer the prompt it never registers, so the device never appears in your workspace and the service just idles indefinitely with no error to flag the problem. Once ch device list confirms the device is Online, the service setup below can take over — every subsequent start just reads the saved token and reconnects, prompt-free. See Quickstart for the full walkthrough from a fresh account.

A ch start registration counts too. If you’ve already run ch start on this machine, it registered a device — as a personal one, disabled by default (see Personal devices). Installing the service on that same registration brings it Online, but the engine still won’t hand it other people’s tasks until you promote it:

ch device enable <deviceId>

What the device server needs at startup

ch device-server requires two things before it can connect:

1. A CLI credential — used by the metrics reporter. Without it the server exits immediately with an error. Supply it via the CH_TOKEN environment variable or the CH_PROFILE variable pointing to a named credential profile.

2. The device token file — ~/.codeherder/device.token by default. This file is written the first time you run ch device-server interactively and accept the registration prompt (see How do I add a device?) — a non-interactive run never writes it, which is why registering first, before installing the service, matters (see above). The server must run as the user who owns this file so the ~ path resolves correctly. If you need to run the server as a different user, pass --token-file <absolute-path> to point at the file explicitly. See Device tokens for what this token is and how to rotate it if it’s ever compromised.

Linux: systemd user service

A systemd user service runs as your account, starts at login, and automatically restarts the server if it exits unexpectedly.

1. Create the unit file

mkdir -p ~/.config/systemd/user

Write the following to ~/.config/systemd/user/codeherder-device-server.service. Replace the ExecStart path with the location of your ch binary (run which ch to find it) and fill in your credentials:

[Unit]
Description=CodeHerder device server
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=%h/.local/bin/ch device-server
Restart=on-failure
RestartSec=5
Environment="CH_TOKEN=<your-api-key>"
Environment="CH_WORKSPACE_ID=<your-workspace-id>"

[Install]
WantedBy=default.target

%h is a systemd specifier that expands to your home directory. Adjust the binary path if you installed ch elsewhere.

Using a credential profile instead: To keep credentials out of the unit file, create a profile at ~/.codeherder/main.env (see Credentials and profiles) and replace the two Environment= lines above with:

Environment="CH_PROFILE=main"

2. Enable and start the service

systemctl --user daemon-reload
systemctl --user enable --now codeherder-device-server.service

enable registers the service for future logins; --now starts it immediately.

3. Start at boot (not just at login)

By default, user services start only when you log in interactively. To start the device server at boot — before any login — enable lingering for your account:

loginctl enable-linger $USER

This keeps your user session alive at boot so your user services start as soon as the machine is ready.

Useful commands

# Check service status
systemctl --user status codeherder-device-server

# Follow live logs
journalctl --user -u codeherder-device-server -f

# Restart after a configuration change
systemctl --user restart codeherder-device-server

# Stop the service
systemctl --user stop codeherder-device-server

macOS: launchd LaunchAgent

A launchd LaunchAgent runs as your account, starts at login, and automatically restarts the server if it exits.

1. Create the plist

Write the following to ~/Library/LaunchAgents/com.codeherder.device-server.plist. Replace the binary path with the absolute path to your ch binary (run which ch to find it) and fill in your credentials:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.codeherder.device-server</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/youruser/.local/bin/ch</string>
        <string>device-server</string>
    </array>

    <key>EnvironmentVariables</key>
    <dict>
        <key>CH_TOKEN</key>
        <string>your-api-key</string>
        <key>CH_WORKSPACE_ID</key>
        <string>your-workspace-id</string>
    </dict>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/tmp/codeherder-device-server.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/codeherder-device-server.log</string>
</dict>
</plist>

RunAtLoad starts the server as soon as the plist is loaded (and at every subsequent login). KeepAlive restarts it automatically if it exits.

Using a credential profile instead: Replace the CH_TOKEN and CH_WORKSPACE_ID entries in EnvironmentVariables with a single CH_PROFILE entry pointing to your named profile:

    <key>CH_PROFILE</key>
    <string>main</string>

The profile file at ~/.codeherder/main.env will be read at startup. See Credentials and profiles.

2. Load the agent

launchctl load ~/Library/LaunchAgents/com.codeherder.device-server.plist

The agent starts immediately and loads at every subsequent login.

Useful commands

# Stop and unload the agent
launchctl unload ~/Library/LaunchAgents/com.codeherder.device-server.plist

# Reload after editing the plist
launchctl unload ~/Library/LaunchAgents/com.codeherder.device-server.plist
launchctl load ~/Library/LaunchAgents/com.codeherder.device-server.plist

# Follow live logs
tail -f /tmp/codeherder-device-server.log

Docker container (trusted mode)

Running ch device-server --docker starts the device server inside a Docker container instead of directly on your machine. The container image is maintained for you and kept in sync with the CLI version automatically. Your home directory, SSH keys, and other machine credentials are not accessible from inside the container — only what CodeHerder needs is forwarded in.

--docker is the trusted mode. The container is privileged, and the agent has passwordless sudo and a world-writable Docker socket inside it. Every session shares one user, the device token, and the git credential pool. It is not a boundary against a hostile agent or a malicious repository. Set CH_TRUSTED_EXECUTION=1 to consent to this mode. Without it, the device server exits at start. To run agents you do not trust, use --docker-executor instead (see Isolating agent runs on a device).

This mode wraps the whole device server and every stage it runs, as a single unit. If you’d rather keep running the device server directly on the host but isolate each stage into its own disposable container instead, see Isolating agent runs on a device — its --docker-executor mode is a different feature from --docker here, and the two cannot be combined.

Prerequisites

  • Docker installed and running on the machine.
  • Register on the host first. Run ch device-server directly — without --docker — from a terminal once, and accept the registration prompt (see How do I add a device?). With that host token in place, --docker seeds it into the container automatically, so the container reuses that same device identity. Skip this and run --docker from a terminal anyway, and the container prints the same registration prompt from inside itself instead — it still registers, but the token then lives only in the container’s own storage, not at ~/.codeherder/device.token on the host. Run it non-interactively (as a service) with no host token, though, and you get the same idle case as the non-Docker path: no prompt, no registration, no device.
  • CH_TOKEN set to your CodeHerder API key (same as the non-Docker device server).
  • CH_TRUSTED_EXECUTION=1 set, to consent to the trusted mode. Without it the device server exits at start with a message that names this variable.
  • gh or glab logged in on the host. The launcher reads your host git credentials and forwards them into the container, so the agent can push code without you configuring git auth inside the container separately.

Run it

CH_TRUSTED_EXECUTION=1 ch device-server --docker

Or set the equivalent environment variable — useful in scripts and service files:

CH_TRUSTED_EXECUTION=1 CH_DOCKER=1 ch device-server

On start, the device server logs a warning that it runs in the trusted mode.

CodeHerder launches a container named ch-device-server by default and starts the device server inside it. A named volume keeps the device identity and any in-flight work across container restarts, so stopping and restarting the container does not lose your device registration or disrupt running sessions.

When the container exits it is removed automatically, so a stopped ch device-server --docker leaves no leftover container to clean up.

Run in Docker under systemd

Add CH_DOCKER=1 and CH_TRUSTED_EXECUTION=1 to your systemd unit’s environment. Using the unit file from the Linux: systemd user service section above, the relevant block becomes:

[Service]
ExecStart=%h/.local/bin/ch device-server
Restart=on-failure
RestartSec=5
Environment="CH_TOKEN=<your-api-key>"
Environment="CH_WORKSPACE_ID=<your-workspace-id>"
Environment="CH_DOCKER=1"
Environment="CH_TRUSTED_EXECUTION=1"

Everything else in the unit file stays the same. systemd treats the container exit as a normal process exit, so Restart=on-failure brings the device server back up if the container stops unexpectedly. If you leave out CH_TRUSTED_EXECUTION=1, the device server logs a consent warning on every start; check journalctl --user -u <unit>. A later release will exit at once instead, and systemd will then restart it in a loop. Set the variable now to avoid that.

Run in Docker under launchd

Add CH_DOCKER and CH_TRUSTED_EXECUTION to the EnvironmentVariables block of your launchd plist. Using the plist from the macOS: launchd LaunchAgent section above, the EnvironmentVariables dict becomes:

<key>EnvironmentVariables</key>
<dict>
    <key>CH_TOKEN</key>
    <string>your-api-key</string>
    <key>CH_WORKSPACE_ID</key>
    <string>your-workspace-id</string>
    <key>CH_DOCKER</key>
    <string>1</string>
    <key>CH_TRUSTED_EXECUTION</key>
    <string>1</string>
</dict>

Reload the agent after saving the plist for the change to take effect:

launchctl unload ~/Library/LaunchAgents/com.codeherder.device-server.plist
launchctl load ~/Library/LaunchAgents/com.codeherder.device-server.plist

Running more than one on the same host

Each --docker device server is a singleton by default: it always launches a container named ch-device-server and reuses the same supporting Docker networks and volume. Starting a second default instance removes the first instance’s container to make room, because the launcher clears out any existing container of that name before it starts. If you want more than one Docker device server running on the same machine, give each one a distinct instance name.

Pass --instance-name <name>:

CH_TRUSTED_EXECUTION=1 ch device-server --docker --instance-name eu-worker

Or set the equivalent environment variable — useful in scripts and service files:

CH_TRUSTED_EXECUTION=1 CH_DOCKER_INSTANCE=eu-worker ch device-server --docker

If both are set, the CH_DOCKER_INSTANCE environment variable wins.

The instance name is appended as a suffix to the container and to its supporting Docker networks and volume, so each instance stays fully independent — for example, --instance-name eu-worker produces a container named ch-device-server-eu-worker. It must be 1–20 characters long and contain only letters, digits, and hyphens, and it must start and end with a letter or digit.

Each concurrent device server is still a separate device from CodeHerder’s point of view, so it needs its own device registration and token — see What the device server needs at startup above for how to point a server at a specific token file with --token-file.

Add CH_DOCKER_INSTANCE alongside CH_DOCKER and CH_TRUSTED_EXECUTION in the systemd unit’s environment to give that instance its own name:

Environment="CH_DOCKER=1"
Environment="CH_TRUSTED_EXECUTION=1"
Environment="CH_DOCKER_INSTANCE=eu-worker"

The launchd equivalent adds the same variable to the plist’s EnvironmentVariables dict, alongside CH_DOCKER:

<key>CH_DOCKER_INSTANCE</key>
<string>eu-worker</string>

Auto-update under a service manager

Auto-update is on by default when a release source is configured (CH_CLI_MANIFEST_URL, or the manifest URL stamped into the build; with neither it stays off). Roughly once an hour, ch device-server checks whether a newer ch release is available. When it finds one, it downloads and verifies the binary, then re-execs itself in place: the running process replaces itself with the updated binary without terminating.

By default, in-flight agent sessions are not interrupted. Before re-executing, the server detaches any running agent processes. The new binary starts up and re-adopts those sessions automatically — tasks continue without any action from you. See Updating the CLI for the opt-in flag that makes an update stop running agents instead.

Because re-exec replaces the process image rather than exiting, neither systemd nor launchd sees a restart event. The service manager continues supervising normally throughout the update.

To control auto-update behaviour, set these environment variables in your service file:

Variable Effect
CH_DS_AUTO_UPDATE=0 Disable automatic updates.
CH_DS_AUTO_UPDATE_INTERVAL=6h Check every six hours instead of every hour.

See Updating the CLI for the full set of auto-update options including the equivalent flags.

Tuning session capacity and storage

By default, the device server stores session worktrees in ~/codeherder-sessions and caps the total at 8 simultaneous worktrees. Three flags let you tune this on a self-managed device server:

Flag Default What it controls
--session-root <abs-dir> ~/codeherder-sessions Parent directory for per-session git worktrees. Must be an absolute path. The Session root and Disk headroom readiness checks (visible in Managing your devices) report on this filesystem.
--max-session-worktrees <n> 8 Device-local cap on simultaneous session worktrees. The device runs at most the smallest of this value, its Max concurrent sessions (set in the web app), and the platform-wide ceiling — so raise this only after you have also raised the server-side limits and want more parallelism on a beefier machine. The Session capacity readiness check reports how close the device is to this cap.
--session-disk-budget-mb <mb> 0 (count cap only) Optional cap on total session-worktree disk in MB. When set to 0 (the default), only the worktree-count cap applies; when set, Session capacity also reports disk usage against this budget.

See Managing your devices for how the per-device limit and platform-wide ceiling combine to set the running-session cap.

For the other directories that grow on a device, how to measure them, and the disk alert level, see Self-hosted device disk growth.

The CH_MAX_SESSION_WORKTREES environment variable is an alternative to the --max-session-worktrees flag above — set it to a positive number and it overrides the flag, on every launch mode (terminal, systemd, launchd, or --docker). It’s handy in a service file where adding an environment entry is easier than editing the command line; under --docker it still unconditionally overrides whatever --max-session-worktrees value the container parses (see below).

Applying these flags under a service manager

Linux (systemd) — append the flags to the ExecStart= line:

ExecStart=%h/.local/bin/ch device-server --session-root /data/codeherder-sessions --max-session-worktrees 4

Reload and restart the service after editing the unit file:

systemctl --user daemon-reload
systemctl --user restart codeherder-device-server

macOS (launchd) — add each flag as a separate <string> entry in the ProgramArguments array:

<key>ProgramArguments</key>
<array>
    <string>/Users/youruser/.local/bin/ch</string>
    <string>device-server</string>
    <string>--session-root</string>
    <string>/Users/youruser/codeherder-sessions</string>
    <string>--max-session-worktrees</string>
    <string>4</string>
</array>

Reload the plist after saving:

launchctl unload ~/Library/LaunchAgents/com.codeherder.device-server.plist
launchctl load ~/Library/LaunchAgents/com.codeherder.device-server.plist

Docker (ch device-server --docker) — in Docker mode, session storage lives on a named Docker volume the launcher manages, not the host filesystem, so --session-root has no effect: the launcher strips it from the flags it forwards into the container and instead pins the in-container session-root to the volume mount via CH_SESSION_ROOT. --token-file is host-side-only for the same class of reason — the launcher reads it on the host to seed the container’s device token, so it’s stripped too. --max-session-worktrees and --session-disk-budget-mb, by contrast, DO take effect under --docker — the launcher forwards them straight through to the in-container device-server. Use the environment variable below if you’d rather tune the worktree cap without editing the command line:

Variable Default What it controls
CH_MAX_SESSION_WORKTREES 16 Maximum simultaneous session worktrees inside the container. When set, it unconditionally overrides --max-session-worktrees (flag or no flag) — the same override precedence as every other launch mode.

--session-disk-budget-mb has no environment-variable equivalent; set it as a flag on ch device-server --docker <flags> if you want a disk budget enforced inside the container.

CH_MAX_SESSION_WORKTREES is the same variable described above — it isn’t Docker-specific, and under --docker it’s just handier than editing the command line. To apply it under a service manager, add it to the environment block alongside CH_DOCKER, the same way environment variables are set in the Linux: systemd and macOS: launchd sections above.

Confirm the device is Online

After the service starts, check that the device is connected:

ch device list

The device should show Online within a few seconds. If it doesn’t, the symptom tells you which of two problems you have:

  • The service keeps restarting, or the process won’t stay up. This is the auth gate: no CH_TOKEN is set and CH_PROFILE doesn’t name a profile it can load, so the server exits immediately every time it starts. Fix the credential in your unit file or plist, then restart the service. (A CH_TOKEN that’s set but wrong or revoked passes this check — the server starts and only fails later, once it tries to actually talk to CodeHerder.)
  • The service stays running, but the device never shows up in ch device list. The device token file was never written — the service started before you ever registered interactively, so there’s nothing for it to connect with, and it just idles. Register the device from a terminal first (see How do I add a device? and the prerequisite above), confirm it’s Online, then restart the service so it picks up the saved token.
  • The service runs and the device shows Online, but no work ever lands on it. This is a personal device, most likely one ch start registered on this machine earlier. Promote it with ch device enable <deviceId> — see Personal devices.

See the Troubleshooting section of Managing your devices for more on either case.

Last updated

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch the work move. Costs update as it happens.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close