Skip to content

Running as a Service

William Koch edited this page Sep 30, 2026 · 11 revisions

For regular use, install Keryx as a background service. uv run keryx serve is fine for local testing, but it stops with the terminal and doesn't come back after a reboot. Without a service manager, it also can't restart itself.

Both installers render a template from ops/ into your own service directory, and keryx setup offers to run the right installer for you. It's safe to run an installer again to apply template changes.

The installer takes two things from the shell you run it in, so run it from the shell whose tools the tasks should have:

  • PATH. A service manager starts its units with a bare PATH, so the installer writes its own PATH into them. Tasks then find what a terminal finds (nvm, Homebrew, conda, cargo, ~/.local/bin…). Run the installer again after your PATH changes.
  • Where things are. The installer asks keryx config path --shell, so it sees the directories exactly as the service will. It writes KERYX_HOME and the four XDG_*_HOME variables into the units. The units log to STATE_DIR/logs/ (~/.local/state/keryx/logs/ by default), where keryx restart reads them back. Run the installer again after you move any of those directories.

CLAUDE_CONFIG_DIR isn't one of them. Keryx honors it wherever it reads the Claude CLI's files: the stored login, CLAUDE.md, the skills, and the Slack MCP server's config. But the units don't include it, so the service and its tasks use ~/.claude unless the service's own environment sets it.

Linux: systemd

scripts/install-systemd.sh              # render + start both units
scripts/install-systemd.sh --uninstall  # stop + remove them

This renders two units into ~/.config/systemd/user/:

  • ops/systemd/keryx.service, which runs uv run --project <repo> keryx serve;
  • ops/systemd/cloudflared.service, the tunnel to localhost:$PORT.

It enables both, and turns on lingering with loginctl enable-linger, so they keep running when nobody is logged in and come back after a reboot. Both restart on failure. The units' stdout and stderr go to STATE_DIR/logs/{keryx,cloudflared}.{out,err}.log, and to journalctl --user -u keryx. Keryx's own log is STATE_DIR/logs/keryx.log (10 MB × 5 rotated files).

If the named tunnel doesn't exist yet, the installer stops early and prints the three cloudflared commands that create it. CLOUDFLARE_TUNNEL names the tunnel (default keryx).

macOS: launchd

scripts/install-launchd.sh              # render + load both agents
scripts/install-launchd.sh --uninstall  # unload + remove them

This renders two agents into ~/Library/LaunchAgents/ and hands them to launchctl bootstrap:

  • ops/launchd/dev.keryx.agent.plist, which runs uv run --project <repo> keryx serve;
  • ops/launchd/dev.keryx.tunnel.plist, which runs ngrok http --domain=<PUBLIC_HOST> <PORT>.

Both have RunAtLoad and KeepAlive, so they start at login and restart if they die. launchd's own stdout and stderr go to STATE_DIR/logs/{keryx,ngrok}.{out,err}.log. Keryx's own log is STATE_DIR/logs/keryx.log (10 MB × 5 rotated files).

On macOS the tunnel uses ngrok, where Linux uses cloudflared. A reserved ngrok domain doesn't need a DNS zone, which makes setup simpler on a laptop. The installer needs ngrok on PATH. To set it up once:

  1. Install ngrok and add your token from the ngrok dashboard:

    brew install ngrok
    ngrok config add-authtoken <your-token>
  2. Reserve a domain in the ngrok dashboard.

  3. Set it with uv run keryx config set PUBLIC_HOST <that-domain>, or in the phone section of keryx setup.

CLOUDFLARE_TUNNEL isn't used on macOS. scripts/dev.sh still runs cloudflared on either platform. keryx doctor accepts whichever tunnel program it finds.

If you rename the agent label, set SERVICE_UNIT. It defaults to dev.keryx.agent.

Which unit a restart talks to

SERVICE_MANAGER picks the service manager: auto, systemd, launchd, or none. SERVICE_UNIT names the unit or agent label (by default keryx.service or dev.keryx.agent). If you set systemd or launchd, Keryx uses it as given.

With auto, Keryx checks whether this process actually runs as the unit, not just whether systemctl or launchctl is on PATH. Under systemd, it checks the process's own cgroup. Under launchd, it checks whether XPC_SERVICE_NAME equals the label.

A keryx serve you started in a terminal isn't supervised, even on a machine where the service is installed too. So if you ask it on a call to restart itself, it refuses. Otherwise it would restart the installed copy in its place, or a unit that isn't there.

keryx restart and keryx doctor always run in a terminal, never inside the unit. For them, the question is whether the unit is installed, and keryx restart restarts that unit. If there's nothing to restart, that's the same as none. That's a supported mode with one consequence, described in Assumptions and Deployments.

Related

  • Restarting Keryx — what happens when the unit is asked to restart, and why Keryx phones you back.
  • Troubleshooting — where the logs are, and what the common failures look like.

Clone this wiki locally