Skip to main content

Run Ethos as a daemon

Looking for the full production setup? If you want both the gateway (Telegram + Slack + Discord + Email) and the web dashboard up under one supervisor, with reboot survival on a mini-PC / VPS / home server, jump to Deploy in production — it uses ethos run-all and PM2 and is the shorter path. This page is the building-block reference for daemonising a single ethos command (the gateway by itself, or cron, or serve).

Task

Run ethos gateway start (or another long-running ethos subcommand) as a persistent background process under systemd, launchd, or pm2, surviving logout and restarting on crash. The gateway is the long-running process that routes platform messages into the agent loop.

Result

The gateway answers your bot on Telegram, Slack, Discord, WhatsApp, or email without a terminal open, restarts on failure, and starts again on boot.

Prereqs

  • ethos installed; ethos --version returns a version string.
  • A provider configured via ethos setup (Configure an LLM provider).
  • For gateway use, at least one platform token in ~/.ethos/config.yaml (telegramToken, slackBotToken, etc.).
  • which ethos returns an absolute path. If you installed via nvm, that path is under ~/.nvm/versions/node/... — service managers cannot resolve a bare ethos without your shell.

What can run as a daemon

Four ethos subcommands are long-running. Everything else is one-shot or REPL.

CommandPurpose
ethos gateway startMulti-platform message gateway (Telegram, Slack, Discord, WhatsApp, email).
ethos cron runScheduled-job worker.
ethos serveWeb UI plus HTTP API.
ethos acpAgent Control Protocol server for mesh coordination.

The examples below use ethos gateway start. Substitute any of the others — the unit-file shape is the same.

Steps

1. Foreground-test first

Daemons fail silently. Confirm the command works under your shell before wrapping it in a service manager.

ethos gateway start

Send your bot a test message from the target platform; confirm a reply. Press Ctrl+C to stop. If foreground does not work, the daemon will not either — fix the config first.

Note the absolute path to the binary:

which ethos

You'll paste it into the unit file in the next step.

2A. macOS — launchd

launchd ships with macOS. Unit files (plists) live in ~/Library/LaunchAgents/. Write ~/Library/LaunchAgents/ai.ethosagent.gateway.plist:

<?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>ai.ethosagent.gateway</string>

<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/ethos</string>
<string>gateway</string>
<string>start</string>
</array>

<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/YOUR_USERNAME</string>
</dict>

<key>WorkingDirectory</key>
<string>/Users/YOUR_USERNAME</string>

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

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

<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/.ethos/logs/gateway.out.log</string>

<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/.ethos/logs/gateway.err.log</string>
</dict>
</plist>

Replace YOUR_USERNAME with the output of whoami and the binary path with which ethos. Then load and start:

launchctl load ~/Library/LaunchAgents/ai.ethosagent.gateway.plist
launchctl start ai.ethosagent.gateway
launchctl list | grep ethosagent
tail -f ~/.ethos/logs/gateway.out.log

Stop, unload, or reload:

launchctl stop ai.ethosagent.gateway
launchctl unload ~/Library/LaunchAgents/ai.ethosagent.gateway.plist

RunAtLoad plus the ~/Library/LaunchAgents/ location starts the agent at login. KeepAlive restarts it on crash.

2B. Linux — systemd user unit

User units live in ~/.config/systemd/user/ and run as your login user. Write ~/.config/systemd/user/ethos-gateway.service:

[Unit]
Description=Ethos gateway
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/ethos gateway start
Restart=on-failure
RestartSec=5
StandardOutput=append:%h/.ethos/logs/gateway.out.log
StandardError=append:%h/.ethos/logs/gateway.err.log
Environment=NODE_ENV=production

[Install]
WantedBy=default.target

Alternatively, generate the unit file automatically:

ethos systemd-unit ethos-gateway > ~/.config/systemd/user/ethos-gateway.service

This generates a production-ready unit with managed mode (ETHOS_MANAGED=1) and EnvironmentFile for secrets. See CLI reference: systemd-unit for placeholders and customisation.

Replace /usr/bin/ethos with the output of which ethos if writing the unit manually. Enable, start, and inspect:

systemctl --user daemon-reload
systemctl --user enable --now ethos-gateway.service
systemctl --user status ethos-gateway
journalctl --user -u ethos-gateway -f

To survive logout on a headless server:

sudo loginctl enable-linger $USER

Restart, stop, or disable:

systemctl --user restart ethos-gateway
systemctl --user stop ethos-gateway
systemctl --user disable ethos-gateway

2C. Cross-platform — pm2

pm2 is a Node process manager. Same commands on macOS, Linux, and Windows; bundled log rotation; pm2 startup wires into the OS service manager.

npm install -g pm2
pm2 start ethos --name ethos-gateway -- gateway start
pm2 list
pm2 logs ethos-gateway

The -- separates pm2's own flags from the args passed to ethos. Survive reboots:

pm2 startup # prints a command — run it as root
pm2 save

Common operations:

pm2 restart ethos-gateway
pm2 stop ethos-gateway
pm2 delete ethos-gateway
pm2 monit
pm2 logs ethos-gateway --lines 200

Running gateway + serve together as one supervised unit? Don't list them as separate PM2 apps — use Deploy in production, which wraps ethos run-all as a single PM2 process that spawns and watches both children. That gives you in-process restart-on-crash AND PM2's reboot survival without the double-supervision footgun.

The pm2-app-per-ethos-command shape is still right when you want to daemonise just one long-running command (e.g. only ethos cron run for a scheduled-job worker without the gateway), which is what this page is about.

3. Update the daemon after ethos upgrade

The running process keeps the old binary in memory. Always restart after upgrading:

ethos upgrade
launchctl stop ai.ethosagent.gateway && launchctl start ai.ethosagent.gateway # macOS
systemctl --user restart ethos-gateway # Linux
pm2 restart ethos-gateway # pm2

Verify

The bot replies to a fresh message within ten seconds, and the appropriate liveness check passes:

launchctl list | grep ai.ethosagent # macOS — non-empty line
systemctl --user is-active ethos-gateway # Linux — prints "active", exit 0
pm2 jlist | jq '.[] | .name' # pm2 — includes "ethos-gateway"

Tail the structured logs Ethos writes alongside whatever stdout your service manager captures:

tail -f ~/.ethos/logs/gateway.out.log

Operator concerns

Linger (headless Linux)

systemd user units are tied to login sessions. When the last session for a user closes — including SSH disconnects — systemd kills all user services by default. On a headless server with no GUI session, ethos stops as soon as you close SSH.

The fix is a one-time command:

sudo loginctl enable-linger $USER

This tells systemd to keep the user's service manager alive even with zero sessions. The user's services start at boot and survive logout.

To undo (e.g. when decommissioning):

sudo loginctl disable-linger $USER

Check the current state:

loginctl show-user $USER --property=Linger

Detached child processes

The bash and process tools can spawn long-running background processes with detached: true. These processes intentionally survive when ethos itself stops — they are children of PID 1, not of the Ethos process tree. ethos stop, systemctl --user stop ethos-gateway, and SIGTERM do not reap them.

This is by design: a user might ask the agent to start a dev server or a build watcher that should keep running. But it means decommissioning ethos does not automatically clean up everything it started.

To see what is still running:

ethos process list

To stop a specific detached process:

ethos process stop <pid>

To stop all tracked detached processes:

ethos process stop --all

If ethos process list shows nothing but you suspect orphans, check for processes whose cwd is under ~/.ethos/:

lsof +D ~/.ethos/ 2>/dev/null | grep -v ethos

Troubleshoot

Daemon starts but the bot does not respond. — Run ethos gateway start in your shell with the same ~/.ethos/config.yaml. If foreground works and daemon does not, it's almost always a stripped PATH or HOME. Hardcode the absolute path to ethos in ProgramArguments / ExecStart and set HOME explicitly (launchd).

ethos: command not found in the service log. — Service managers do not source your shell rc. If you installed via nvm, the binary lives at ~/.nvm/versions/node/v24.x.x/bin/ethos. Paste that absolute path into the unit file.

Run ethos setup first on boot.HOME does not point at your user account. systemd user units inherit it correctly; launchd sometimes does not. Set HOME in the plist EnvironmentVariables block as shown above.

Telegram returns HTTP 429. — Two gateway processes are polling the same bot token. Check for a duplicate launchd plist, a stale pm2 entry, or a forgotten tmux session. One process per bot token.

Daemon stops on logout (Linux). — Run sudo loginctl enable-linger $USER once. Without it, systemd tears down user units when the last login session ends.

Memory grows unbounded. — Check pm2 monit or top -p $(pgrep -f "ethos gateway"). If the resident set climbs steadily over hours, it's likely a leak — file an issue with a node --inspect heap snapshot. Short-term: pm2 supports --max-memory-restart 500M to recycle the process at a threshold.

Logs missing on Linux.StandardOutput=append: requires systemd 240+; on older systems, drop those lines and use journalctl --user -u ethos-gateway instead.