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.
curl -fsSL https://comis.ai/install.sh | bash
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.
sudo useradd -r -s /sbin/nologin comis
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:
sudo mkdir -p /opt/comis
sudo cp -r . /opt/comis/
sudo mkdir -p /var/lib/comis
sudo chown -R comis:comis /opt/comis /var/lib/comis
  • /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:
sudo mkdir -p /etc/comis
sudo cp ~/.comis/config.yaml /etc/comis/config.yaml
sudo chown comis:comis /etc/comis/config.yaml
sudo chmod 600 /etc/comis/config.yaml
Create the environment file at /etc/comis/env with your API keys and config path:
sudo tee /etc/comis/env > /dev/null << 'EOF'
COMIS_CONFIG_PATHS=/etc/comis/config.yaml
ANTHROPIC_API_KEY=your-api-key-here
EOF
sudo chmod 600 /etc/comis/env
sudo chown comis:comis /etc/comis/env
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:
[Unit]
Description=Comis AI Agent Daemon
Documentation=https://github.com/comis/comis
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=comis
Group=comis
WorkingDirectory=/opt/comis

# --jitless: Disable JIT to reduce attack surface (no W^X pages)
# --permission: Enable Node.js permission model for filesystem restrictions
ExecStart=/usr/bin/node --jitless --permission \
  --allow-fs-read=/opt/comis \
  --allow-fs-write=/var/lib/comis \
  --allow-child-process dist/daemon.js

# Restart policy
Restart=on-failure
RestartSec=5s

# Shutdown: allow 45s for graceful shutdown (app timeout is 30s)
TimeoutStopSec=45

# Resource limits
MemoryMax=2G
TasksMax=100

# Standard output/error to journald for structured log collection
StandardOutput=journal
StandardError=journal
SyslogIdentifier=comis

# Environment
Environment=NODE_ENV=production
EnvironmentFile=-/etc/comis/env

# --- Security Hardening ---

# Filesystem protection
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/comis /var/log/comis

# Memory protection
MemoryDenyWriteExecute=yes

# Privilege escalation prevention
NoNewPrivileges=yes

# Drop all capabilities
CapabilityBoundingSet=

# System call filtering: allow only standard service calls
SystemCallFilter=@system-service
SystemCallArchitectures=native

# Kernel and device protection
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target
--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:
sudo systemctl daemon-reload
5

Start the service

Enable the service (so it starts on boot) and start it immediately:
sudo systemctl enable --now comis
Expected output:
Created symlink /etc/systemd/system/multi-user.target.wants/comis.service -> /etc/systemd/system/comis.service.
6

Verify it is running

Check the service status:
sudo systemctl status comis
You should see output similar to:
● comis.service - Comis AI Agent Daemon
     Loaded: loaded (/etc/systemd/system/comis.service; enabled)
     Active: active (running) since ...
   Main PID: 12345 (node)
     Status: "Comis daemon started"
      Tasks: 12 (limit: 100)
     Memory: 150.0M (max: 2.0G)
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.

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.
See Node Permissions — Production fd-API Disablement for the impact of --permission on daemon-process fd-based APIs (fsync, fchmod, fchown).

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:
DirectiveWhat it does
ProtectSystem=strictMakes the entire filesystem read-only except explicitly allowed paths
ProtectHome=yesHides all home directories from the service
NoNewPrivileges=yesPrevents the daemon from gaining elevated permissions
MemoryDenyWriteExecute=yesBlocks creating memory that is both writable and executable
SystemCallFilter=@system-serviceOnly allows system calls needed for normal services
PrivateDevices=yesHides physical devices from 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:
Install flagReadWritePaths additions--allow-fs-write additions
--with-browser (stock Chrome)~/.config/google-chrome, ~/.local/share/applications, ~/.config/comis/browserSame set; mirrored on the Node permission model
--with-cloakbrowser (stealth Chromium)~/.cloakbrowser, ~/.config/chromium, ~/.config/comis/browserSame; tighter than the Chrome variant (no mimeapps write needed — CloakBrowser patches that out)
--with-xvfb(no extra paths — see companion unit below)(no extra paths)
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:
[Service]
ExecStart=/usr/bin/Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp
-nolisten tcp keeps the X server on a Unix socket only; -ac is safe because the socket is owned by the comis user and inaccessible from outside that namespace. The main comis.service unit picks up the display by way of three additional directives:
After=network-online.target comis-xvfb.service
Wants=network-online.target comis-xvfb.service
Environment=DISPLAY=:99
JoinsNamespaceOf=comis-xvfb.service
JoinsNamespaceOf= is load-bearing — without it, PrivateTmp=yes on the main unit gives the daemon its own /tmp namespace and the X11 socket at /tmp/.X11-unix/X99 is unreachable. The pair share /tmp for exactly this purpose. Manage the companion unit the same way as the main one:
sudo systemctl status comis-xvfb         # check it's running
sudo systemctl restart comis-xvfb        # bounces both this and the daemon
journalctl -u comis-xvfb --since "-1h"   # Xvfb startup logs
The companion is uninstalled together with the main service by bash install.sh --uninstall (no separate command needed).

Common commands

CommandWhat it does
sudo systemctl start comisStart the daemon
sudo systemctl stop comisStop the daemon (waits up to 45 seconds for graceful shutdown)
sudo systemctl restart comisStop and start the daemon
sudo systemctl status comisShow current status, PID, memory, and recent log lines
sudo systemctl enable comisStart automatically on boot
sudo systemctl disable comisDo not start on boot

Viewing logs

systemd sends all daemon output to the journal. Use journalctl to view logs: Live logs (follow mode):
journalctl -u comis -f
Recent logs (last hour):
journalctl -u comis --since "1 hour ago"
Logs since last boot:
journalctl -u comis -b
Only errors and warnings:
journalctl -u comis -p err
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.