Skip to main content
systemd is the standard way to run background services on Linux. Setting up Comis as a systemd service means it starts automatically when your server boots, restarts if it crashes, and integrates with standard Linux monitoring tools.
systemd is Linux only. If you are on macOS, use pm2 instead.

Two paths

You can get a systemd-managed Comis daemon two ways: Recommended: the one-line installer. The script at https://comis.ai/install.sh creates the comis system user, lays out /etc/comis, writes a managed comis.service unit, sets up sudoers rules so the service user can start/stop/restart its own daemon without root, and starts everything. This is the path the official VPS install guide uses.
The installed unit file is checksum-tagged with # managed-by: comis-installer. If you edit it by hand, the installer will refuse to overwrite it on upgrade — so manual edits stick around. Manual: the steps below. Use this when you need a unit file under your own control (different paths, different hardening, different user). The rest of this page walks through that.

Prerequisites

Before setting up the systemd service, make sure you have:
  • Node.js 22 or newer installed on your server
  • Comis built — run pnpm build in the Comis directory
  • A dedicated system user for running Comis (created in step 1 below)

Setup

1

Create a system user

Create a dedicated comis user that has no login shell. This is a security best practice — the daemon runs under its own user with limited permissions.
The -r flag creates a system user (no home directory, no login). The -s /sbin/nologin flag prevents anyone from logging in as this user.
2

Install Comis

Copy your built Comis files to a system directory and create the data directory:
  • /opt/comis/ — where the application code lives (read-only at runtime)
  • /var/lib/comis/ — where the database, logs, and runtime data are stored (read-write)
3

Create the configuration

Create a directory for the config file and environment variables:
Create the environment file at /etc/comis/env with your API keys and config path:
Keep API keys in the environment file rather than in config.yaml. The environment file has strict permissions (readable only by the comis user) and is not tracked in version control.
4

Install the service file

Create the systemd unit file at /etc/systemd/system/comis.service:
--permission also disables fd-based fs APIs (fsync, fchmod, fchown) at the daemon-process level. Credential file writes are best-effort durability (no fsync) and file permissions are best-effort. This is guarded at all call sites — the daemon will not crash — but the tradeoff is documented in Node Permissions — Production fd-API Disablement.
Then reload systemd to pick up the new file:
5

Start the service

Enable the service (so it starts on boot) and start it immediately:
Expected output:
6

Verify it is running

Check the service status:
You should see output similar to:
The key indicators are:
  • Active: active (running) — the daemon is running
  • Status: “Comis daemon started” — the daemon completed its startup sequence

Service file explained

Here is what each important section of the unit file does:

Type=exec

systemd considers the service started once execve() returns. In-process liveness is observed by Comis’s ProcessMonitor (event loop delay tracking) and surfaced on the /health HTTP endpoint; crash recovery is handled by Restart=on-failure below. Comis does not participate in the systemd liveness-ping protocol — operators who require kernel-level watchdog integration can add it via a systemd drop-in.

Restart=on-failure + RestartSec=5s

If the daemon crashes (exits with a non-zero code), systemd waits 5 seconds and then starts it again automatically. This covers unexpected errors, out-of-memory kills, and unhandled exceptions. Normal stops (via systemctl stop) do not trigger a restart.

KillMode=process

On stop, systemd signals only the main daemon process, not the entire control group (the default KillMode=control-group would SIGKILL every process in the cgroup). This is required for durable terminal drives (drive.durable): a durable session runs its child inside a detached tmux server that, although reparented to init, stays a member of the daemon’s cgroup. Under the default control-group kill, every systemctl restart would destroy that tmux server and the session could never survive a restart. With KillMode=process, the detached tmux server is left running and a restarted daemon re-attaches to it by name. Non-durable sessions are still cleaned up on stop: graceful shutdown runs the daemon’s own teardown, and the Terminal Worker exits as soon as its stdin closes (its sandboxed children carry --die-with-parent). The trade-off is that after a hard daemon crash (no graceful shutdown), other long-lived children such as MCP servers or the browser may linger for a few seconds until Restart=on-failure respawns the daemon.

MemoryMax=2G + TasksMax=100

Resource limits prevent the daemon from consuming too many system resources. If memory exceeds 2 GB, systemd kills the process (which then triggers a restart). TasksMax limits the number of threads and processes the daemon can create.

Node.js —permission flags

The --permission flag enables the Node.js permission model, which restricts what the daemon can access:
  • --allow-fs-read=/opt/comis — can read application code only from /opt/comis
  • --allow-fs-write=/var/lib/comis — can write data only to /var/lib/comis
  • --allow-child-process — can spawn child processes (needed for some tools)
This acts as a second layer of security, even if a vulnerability is exploited. The keyless in-process local STT engine caches its whisper model under the data dir at <data-dir>/models/whisper/ (e.g. /var/lib/comis/models/whisper/), so the existing --allow-fs-write=/var/lib/comis and ReadWritePaths=/var/lib/comis already cover it — no additional --allow-fs-write or ReadWritePaths entry is needed for local STT.
See Node Permissions — Production fd-API Disablement for the impact of --permission on daemon-process fd-based APIs (fsync, fchmod, fchown), and Keyless local STT (whisper) engine for why local STT needs no new flag.
The reference unit above sets --jitless and MemoryDenyWriteExecute=yes, and does not pass --allow-addons. The keyless in-process whisper engine needs the opposite: --allow-addons (its ONNX Runtime is a native addon) and JIT / writable-exec memory for the WebAssembly ONNX fallback. The bundled install.sh unit therefore omits --jitless/MemoryDenyWriteExecute and includes --allow-addons for exactly this class of dependency (see website/public/install.sh). If you harden a hand-written unit with --jitless/MemoryDenyWriteExecute and want local STT, either add --allow-addons and drop those two hardening flags, or keep the hardening and use a transcription.local.baseUrl whisper server instead (an out-of-process server is unaffected by the daemon’s addon/JIT flags). Either way the model-cache write path is already in scope.

EnvironmentFile=-/etc/comis/env

Loads environment variables (like API keys and COMIS_CONFIG_PATHS) from the specified file. The - prefix means systemd will not fail if the file is missing — it simply skips loading it.

Security hardening directives

The bottom section of the unit file locks down the service:

Browser tool wiring (when installed with --with-browser / --with-xvfb / --with-cloakbrowser)

The browser-tool flags don’t change the security posture — they widen specific write paths just enough for the chosen browser binary to launch: Every additional write path is named, scoped to the daemon’s home, and matches what the binary actually writes. The Chrome variant needs ~/.local/share/applications for mimeapps.list (Chrome’s default-browser registration; no flag disables it). The cloak variant doesn’t.

comis-xvfb.service companion (when installed with --with-xvfb)

A second managed unit at /etc/systemd/system/comis-xvfb.service runs Xvfb on display :99 as the same comis user:
-nolisten tcp keeps the X server on a Unix socket only; -ac is safe because the socket is owned by the comis user. The companion unit also binds a shared host dir onto its /tmp/.X11-unix so the socket it creates is reachable from the daemon:
The main comis.service unit picks up the display with:
The shared-bind pair is load-bearing — without it, PrivateTmp=yes on each unit gives the daemon its own /tmp and the X11 socket at /tmp/.X11-unix/X99 is unreachable. (JoinsNamespaceOf= was tried first but does not share the PrivateTmp /tmp content on systemd 255 — the daemon’s namespace gets an empty /tmp/.X11-unix.) /run/comis-x11 is created with mode 1777 by /etc/tmpfiles.d/comis-x11.conf so it survives reboot before the units mount it. Manage the companion unit the same way as the main one:
The companion is uninstalled together with the main service by bash install.sh --uninstall (no separate command needed).

Common commands

Viewing logs

systemd sends all daemon output to the journal. Use journalctl to view logs: Live logs (follow mode):
Recent logs (last hour):
Logs since last boot:
Only errors and warnings:
The daemon also writes logs to ~/.comis/logs/daemon.log (resolved against the service user’s home — /var/lib/comis/.comis/logs/daemon.log for an installer-managed service). See Logging for details on configuring log levels and rotation.

Daemon

How the daemon starts, runs, and shuts down.

pm2

Alternative process manager for macOS and Linux.

Docker

Run Comis in a Docker container.

Logging

Configure log levels, rotation, and structured output.

Troubleshooting

Solutions to common issues.