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-alland PM2 and is the shorter path. This page is the building-block reference for daemonising a singleethoscommand (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
ethosinstalled;ethos --versionreturns 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 ethosreturns an absolute path. If you installed vianvm, that path is under~/.nvm/versions/node/...— service managers cannot resolve a bareethoswithout your shell.
What can run as a daemon
Four ethos subcommands are long-running. Everything else is one-shot or REPL.
| Command | Purpose |
|---|---|
ethos gateway start | Multi-platform message gateway (Telegram, Slack, Discord, WhatsApp, email). |
ethos cron run | Scheduled-job worker. |
ethos serve | Web UI plus HTTP API. |
ethos acp | Agent 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.