> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-skill-archive-import.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# systemd

> Running Comis as a systemd service on Linux

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.

<Warning>
  systemd is **Linux only**. If you are on macOS, use [pm2](/operations/pm2) instead.
</Warning>

## 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](/installation/install-vps) uses.

```bash theme={}
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

<Steps>
  <Step title="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.

    ```bash theme={}
    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.
  </Step>

  <Step title="Install Comis">
    Copy your built Comis files to a system directory and create the data directory:

    ```bash theme={}
    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)
  </Step>

  <Step title="Create the configuration">
    Create a directory for the config file and environment variables:

    ```bash theme={}
    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:

    ```bash theme={}
    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
    ```

    <Tip>
      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.
    </Tip>
  </Step>

  <Step title="Install the service file">
    Create the systemd unit file at `/etc/systemd/system/comis.service`:

    ```ini theme={}
    [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

    # On stop, signal only the main daemon process (not the whole cgroup) so a
    # durable terminal session's detached tmux server survives a restart (see below).
    KillMode=process

    # 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 (the real
    # installer also adds @mount + setns for bubblewrap's exec sandbox). When the
    # browser tool is provisioned, Chrome needs a few more or the kernel
    # SIGSYS-kills it before it opens its CDP socket, so the installer appends:
    #   SystemCallFilter=pkey_alloc pkey_free pkey_mprotect \
    #                    landlock_create_ruleset landlock_add_rule landlock_restrict_self \
    #                    ptrace seccomp
    # (pkey_* = V8 memory-protection keys; landlock_*/seccomp = Chrome's own
    # restriction-only self-sandbox; ptrace is same-uid-limited under
    # NoNewPrivileges + empty CapabilityBoundingSet). Omitted for --without-browser.
    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
    ```

    <Note>
      `--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](/reference/node-permissions#production-fd-api-disablement-linux).
    </Note>

    Then reload systemd to pick up the new file:

    ```bash theme={}
    sudo systemctl daemon-reload
    ```
  </Step>

  <Step title="Start the service">
    Enable the service (so it starts on boot) and start it immediately:

    ```bash theme={}
    sudo systemctl enable --now comis
    ```

    Expected output:

    ```
    Created symlink /etc/systemd/system/multi-user.target.wants/comis.service -> /etc/systemd/system/comis.service.
    ```
  </Step>

  <Step title="Verify it is running">
    Check the service status:

    ```bash theme={}
    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
  </Step>
</Steps>

## 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`](/agent-tools/terminal-driver)): 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.

<Note>
  See [Node Permissions — Production fd-API Disablement](/reference/node-permissions#production-fd-api-disablement-linux) for the impact of `--permission` on daemon-process fd-based APIs (`fsync`, `fchmod`, `fchown`), and [Keyless local STT (whisper) engine](/reference/node-permissions#keyless-local-stt-whisper-engine) for why local STT needs no new flag.
</Note>

<Warning>
  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`](https://github.com/comisai/comis/blob/main/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.
</Warning>

### 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:

| Directive                          | What it does                                                          |
| ---------------------------------- | --------------------------------------------------------------------- |
| `ProtectSystem=strict`             | Makes the entire filesystem read-only except explicitly allowed paths |
| `ProtectHome=yes`                  | Hides all home directories from the service                           |
| `NoNewPrivileges=yes`              | Prevents the daemon from gaining elevated permissions                 |
| `MemoryDenyWriteExecute=yes`       | Blocks creating memory that is both writable and executable           |
| `SystemCallFilter=@system-service` | Only allows system calls needed for normal services                   |
| `PrivateDevices=yes`               | Hides 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 flag                             | `ReadWritePaths` additions                                                          | `--allow-fs-write` additions                                                                     |
| ---------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `--with-browser` (stock Chrome)          | `~/.config/google-chrome`, `~/.local/share/applications`, `~/.config/comis/browser` | Same set; mirrored on the Node permission model                                                  |
| `--with-cloakbrowser` (stealth Chromium) | `~/.cloakbrowser`, `~/.config/chromium`, `~/.config/comis/browser`                  | Same; 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. The companion unit also binds a shared host dir onto its `/tmp/.X11-unix` so the socket it creates is reachable from the daemon:

```
BindPaths=/run/comis-x11:/tmp/.X11-unix
```

The main `comis.service` unit picks up the display with:

```
After=network-online.target comis-xvfb.service
Wants=network-online.target comis-xvfb.service
Environment=DISPLAY=:99
BindReadOnlyPaths=/run/comis-x11:/tmp/.X11-unix
```

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:

```bash theme={}
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

| Command                        | What it does                                                   |
| ------------------------------ | -------------------------------------------------------------- |
| `sudo systemctl start comis`   | Start the daemon                                               |
| `sudo systemctl stop comis`    | Stop the daemon (waits up to 45 seconds for graceful shutdown) |
| `sudo systemctl restart comis` | Stop and start the daemon                                      |
| `sudo systemctl status comis`  | Show current status, PID, memory, and recent log lines         |
| `sudo systemctl enable comis`  | Start automatically on boot                                    |
| `sudo systemctl disable comis` | Do not start on boot                                           |

## Viewing logs

systemd sends all daemon output to the journal. Use `journalctl` to view logs:

**Live logs** (follow mode):

```bash theme={}
journalctl -u comis -f
```

**Recent logs** (last hour):

```bash theme={}
journalctl -u comis --since "1 hour ago"
```

**Logs since last boot:**

```bash theme={}
journalctl -u comis -b
```

**Only errors and warnings:**

```bash theme={}
journalctl -u comis -p err
```

<Info>
  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](/operations/logging) for details on configuring log
  levels and rotation.
</Info>

## Related pages

<CardGroup cols={2}>
  <Card title="Daemon" icon="server" href="/operations/daemon">
    How the daemon starts, runs, and shuts down.
  </Card>

  <Card title="pm2" icon="rotate" href="/operations/pm2">
    Alternative process manager for macOS and Linux.
  </Card>

  <Card title="Docker" icon="docker" href="/operations/docker">
    Run Comis in a Docker container.
  </Card>

  <Card title="Logging" icon="file-lines" href="/operations/logging">
    Configure log levels, rotation, and structured output.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/operations/troubleshooting">
    Solutions to common issues.
  </Card>
</CardGroup>
