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 twoEnvironment=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_TOKENandCH_WORKSPACE_IDentries inEnvironmentVariableswith a singleCH_PROFILEentry pointing to your named profile:<key>CH_PROFILE</key> <string>main</string>The profile file at
~/.codeherder/main.envwill 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_TOKENset to your CodeHerder API key (same as the non-Docker device server). If this is the machine’s first run,ch device-server --dockerself-registers the device and writes the token to~/.codeherder/device.token, which is seeded into the container automatically.ghorglablogged 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.
Related guides
- How do I add a device? — the focused first-run steps to connect a machine
- Managing your devices — day-to-day device health, capacity, and troubleshooting
- Monitoring your agents — watch fleet health once the service is running
- Updating the CLI — how the service keeps its own binary current
