CodeHerderSearch⌘KRequest access →

← All docs

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.

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. Both are available on the Install CLI page in your workspace sidebar. See Credentials and profiles for how to obtain and store them.

There is no separate device-registration step: run ch device-server on the machine you want to run and it self-registers on first run, writing the device token to ~/.codeherder/device.token. See How do I add a device? for the focused first-run steps, or Quickstart for the full walkthrough from a fresh account.

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 ch device-server runs (self-registration). 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 (host-isolated)

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. The main advantage is host isolation: your home directory, SSH keys, and other machine credentials are not accessible from inside the container — only what CodeHerder needs is forwarded in.

This isolates 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.
  • CH_TOKEN set to your CodeHerder API key (same as the non-Docker device server). If this is the machine’s first run, ch device-server --docker self-registers the device and writes the token to ~/.codeherder/device.token, which is seeded into the container automatically.
  • 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 device-server --docker

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

CH_DOCKER=1 ch device-server

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 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"

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.

Run in Docker under launchd

Add CH_DOCKER 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>
</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 device-server --docker --instance-name eu-worker

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

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 in the systemd unit’s environment to give that instance its own name:

Environment="CH_DOCKER=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. 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.

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, and it’s the only way to raise the cap under --docker (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 is managed entirely by the container. The --session-root, --max-session-worktrees, and --session-disk-budget-mb flags do not take effect in Docker mode. Use the environment variable below to tune the worktree cap instead:

Variable Default What it controls
CH_MAX_SESSION_WORKTREES 16 Maximum simultaneous session worktrees inside the container.

There is no disk-budget knob in Docker mode — only the worktree-count cap applies.

This is the same CH_MAX_SESSION_WORKTREES variable described above — it isn’t Docker-specific, Docker mode just has no flag to set it with directly. 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 of the server starting. If it stays Offline, check the service logs for errors — the most common causes are a missing or invalid CH_TOKEN and a missing device token file. See the Troubleshooting section of Managing your devices for next steps.

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch what's happening, see what's stuck, and know what it's costing you, live.

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

↑↓ move · ↵ open · esc close