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

# Install with Docker

> Run Comis in a Docker container with Docker Compose

This guide sets up Comis in a Docker container. You do not need Node.js installed
on your system -- Docker handles everything.

<Info>
  You don't need to understand the technical details to use this feature. The configuration examples below are copy-paste ready.
</Info>

<Warning>
  You need **Docker Engine 24+** and **Docker Compose v2+** installed. Check with:

  ```bash theme={}
  docker --version
  docker compose version
  ```
</Warning>

<Warning>
  **Production = Linux host only.** macOS and Windows Docker Desktop run containers
  inside a `linuxkit` VM whose kernel does not allow nested PID-namespace + procfs
  mounts. This breaks the bubblewrap exec sandbox.

  On Docker Desktop the daemon detects this at startup and **automatically
  disables the exec sandbox** so the agent is functional for development. The
  trade-off:

  * ✅ **Dev/testing UX on Mac and Windows:** the agent runs end-to-end. You
    can drive `exec` commands, run scripts in the workspace, and exercise
    every feature that depends on shell execution.
  * ❌ **The documented exec-isolation boundary is absent on Docker
    Desktop.** With the sandbox off, agent-issued shell commands may read
    `/home/comis/.comis/.env`, the encrypted `secrets.db`, `/etc/comis/config.yaml`,
    channel tokens, the `SECRETS_MASTER_KEY`, and anything else the daemon
    user owns. Treat prompt-driven shell execution as host-equivalent authority
    within the container account.

  This is fine for **development and local testing only**. For production,
  deploy to a real Linux host (bare metal, VPS, or a Linux cloud VM) and verify
  that bubblewrap is active after startup. Some Linux hosts restrict unprivileged
  user namespaces through AppArmor and require the host configuration described in
  [Operations: Docker → Per-command exec sandbox](/operations/docker#per-command-exec-sandbox-bubblewrap).

  See [Operations: Docker → Platform Support](/operations/docker#platform-support)
  for the full explanation, including local Linux-VM alternatives
  (Lima / Colima `vz` driver / OrbStack / UTM) that get you a working
  sandbox on a Mac.
</Warning>

## Automated setup

The repository includes a setup script that creates directories, generates a
gateway token, builds the Docker image, and starts the daemon. This is the
recommended way to get started.

<Steps>
  <Step title="Clone the repository">
    ```bash theme={}
    git clone https://github.com/comisai/comis.git
    cd comis
    ```
  </Step>

  <Step title="Configure Compose and credentials">
    Copy the included Compose settings template:

    ```bash theme={}
    cp .env.docker.example .env
    ```

    Keep persistent credentials in the mounted daemon environment file (the
    default `COMIS_ENV_FILE` is `~/.comis/.env`):

    ```bash theme={}
    mkdir -p ~/.comis
    chmod 700 ~/.comis
    touch ~/.comis/.env
    chmod 600 ~/.comis/.env
    ${EDITOR:-vi} ~/.comis/.env
    ```

    Add the credentials required by your chosen provider, for example
    `ANTHROPIC_API_KEY=...` or `OPENAI_API_KEY=...`. OAuth and local keyless
    providers do not need a static provider key. If you change `COMIS_ENV_FILE`
    in the project-root `.env`, edit that target instead.

    <Warning>
      Never store API keys, tokens, or passwords directly in `config.yaml`. Use the `.env` file or [Secret Manager](/security/secrets) for credential management.
    </Warning>

    <Tip>
      `.env.docker.example` contains image, path, and network settings plus a
      small set of optional explicit credential overrides. Blank overrides are
      removed by the container entrypoint, so they cannot hide non-empty values
      from `COMIS_ENV_FILE`.
    </Tip>
  </Step>

  <Step title="Run the setup script">
    ```bash theme={}
    ./docker-setup.sh
    ```

    The script performs these steps automatically:

    1. Creates the data and config directories (both `~/.comis` by default)
    2. Generates a gateway authentication token (or reuses an existing one)
    3. Writes token and network settings to `COMIS_ENV_FILE`
    4. Creates a schema-valid `config.yaml` with authenticated gateway, logging, and memory paths
    5. Builds the Docker image from the included `Dockerfile`
    6. Prepares data and config ownership for the container's non-root user (UID 1000)
    7. Starts the `comis-daemon` service and waits for its health check to pass

    The script prints the gateway URL and credential-file path only after the
    daemon is healthy; it does not echo the token. On timeout it exits nonzero
    and prints the logs command to run.
  </Step>

  <Step title="Verify">
    Confirm the daemon is healthy:

    ```bash theme={}
    curl http://localhost:4766/health
    ```

    Expected response:

    ```json theme={}
    { "status": "ok" }
    ```
  </Step>
</Steps>

## Browser tool (optional)

The published image is browser-free. To use the agent browser tool, rebuild from the repo with one of three build args mirroring the bare-VPS install flags. Image-size impact in parentheses.

```bash theme={}
# Stock Chrome (+400 MB)
docker build \
  --build-arg COMIS_WITH_BROWSER=1 \
  -t comisai/comis:browser .

# Headed via Xvfb — for workflows that require a display server (+15 MB on top)
docker build \
  --build-arg COMIS_WITH_XVFB=1 \
  -t comisai/comis:xvfb .

# Optional CloakBrowser runtime (+500 MB)
docker build \
  --build-arg COMIS_WITH_CLOAKBROWSER=1 \
  -t comisai/comis:cloak .

# Full stack
docker build \
  --build-arg COMIS_WITH_CLOAKBROWSER=1 \
  --build-arg COMIS_WITH_XVFB=1 \
  -t comisai/comis:cloak-xvfb .
```

The entrypoint shim (`/usr/local/bin/comis-entrypoint.sh`) starts Xvfb on `:99` automatically when the image was built with `COMIS_WITH_XVFB=1`. It verifies the X11 socket before starting the daemon and exits if the virtual display cannot start. The daemon's `findChrome()` probes `~/.cloakbrowser/chromium-*/chrome` first, so when the cloak binary is baked in it wins over stock Chrome with no config change.

Run the CloakBrowser image identically to the no-browser one — same volumes, same ports, same environment variables:

```bash theme={}
docker run -d \
  --name comis \
  --restart unless-stopped \
  -p 127.0.0.1:4766:4766 \
  -v comis-data:/home/comis/.comis \
  -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
  comisai/comis:cloak
```

### Alternative: installer-based image

The repo also ships `Dockerfile.install`. It packs the local workspace and runs
the installer's non-interactive, non-root package-install path inside Ubuntu
24.04. This exercises package installation and selected browser-provisioning
branches, but skips interactive setup, initialization, service management,
dedicated-user creation, and the public-registry download path.

```bash theme={}
docker build -f Dockerfile.install \
  --build-arg COMIS_WITH_CLOAKBROWSER=1 \
  -t comis-installed:cloak .
```

The main `Dockerfile` is the primary multi-stage image path.
`Dockerfile.install` is a focused installer validation image, not a parity image
for a managed host.

<Warning>
  CloakBrowser does not include a proxy, change IP reputation, or guarantee access
  to any site. Review the target site's terms and the
  [binary license](https://github.com/CloakHQ/CloakBrowser/blob/main/BINARY-LICENSE.md)
  before enabling it.
</Warning>

See [agent-tools/browser](/agent-tools/browser) for the browser-tool security model.

## Optional services

The daemon is the only service that starts by default. The web dashboard and
CLI are available as Docker Compose profiles.

### Web dashboard

Start the Nginx-served web UI on port 8080:

```bash theme={}
docker compose --profile web up -d
```

Then open `http://localhost:8080` in your browser. The dashboard proxies API
calls and WebSocket connections to the daemon automatically.

### CLI

Run one-shot CLI commands against the running daemon:

```bash theme={}
docker compose --profile cli run --rm comis-cli status
```

The CLI container shares the daemon's network namespace, so it reaches the
gateway at `127.0.0.1:4766` without extra configuration.

## Stopping and updating

| Action             | Command                                    |
| ------------------ | ------------------------------------------ |
| Stop daemon        | `docker compose down`                      |
| Stop web dashboard | `docker compose --profile web down`        |
| Restart daemon     | `docker compose restart comis-daemon`      |
| Update             | `git pull && docker compose up -d --build` |
| View logs          | `docker compose logs -f comis-daemon`      |

## Deploying on a VPS

The supplied Compose file and quick-start examples bind the host port to
`127.0.0.1` by default. If you omit the host address (`-p 4766:4766`) or set
the host-side bind to `0.0.0.0`, a VPS may expose the gateway to the public
internet. Choose an intentional posture below before changing the loopback
default.

<Warning>
  The wizard's "LAN mode" sets `gateway.host: 0.0.0.0` *inside the container*.
  That's correct — the daemon must bind all interfaces so Docker port-forwarding
  can reach it. The exposure decision is made on the **host side** by the `-p`
  mapping (or Compose `ports:`). Confusing the two is the #1 way to accidentally
  expose a daemon publicly.
</Warning>

### Recommended: loopback bind + reverse proxy

Bind the host port to loopback only and put nginx (or Caddy/Traefik) in front
to terminate TLS:

```bash theme={}
docker run -d \
  --name comis \
  --restart unless-stopped \
  -p 127.0.0.1:4766:4766 \
  -v comis-data:/home/comis/.comis \
  -e COMIS_CONFIG_PATHS=/home/comis/.comis/config.yaml \
  comisai/comis:latest-slim
```

Or in `docker-compose.yml`:

```yaml theme={}
ports:
  - "127.0.0.1:4766:4766"
```

Then point your reverse proxy at `http://127.0.0.1:4766` (HTTP) and
`http://127.0.0.1:4766/ws` (WebSocket). See
[Operations: Reverse Proxy](/operations/reverse-proxy) for full nginx /
Caddy snippets with WebSocket upgrade headers.

### Acceptable: direct exposure + token auth + firewall

If you don't want a reverse proxy, you can expose the gateway directly,
but you **must** combine three controls:

1. **Token auth on the gateway.** The init wizard enables this by default
   (`Authentication method → Token`). Verify your `~/.comis/config.yaml`
   contains a `gateway.tokens:` section and `~/.comis/.env` has a
   `COMIS_GATEWAY_TOKEN`.
2. **Firewall to trusted IPs only.** On Ubuntu/Debian:
   ```bash theme={}
   sudo ufw allow from <your-trusted-ip> to any port 4766
   sudo ufw enable
   ```
3. **TLS.** Without TLS, the bearer token crosses the wire in cleartext.
   Either run with `gateway.tls.cert` configured (see
   [HTTP gateway: mTLS](/reference/http-gateway#mtls) or front it
   with a reverse proxy that does TLS for you (in which case use the
   recommended posture above instead).

### `comis status` from your laptop against a remote VPS

The CLI's `comis status` defaults to reading `~/.comis/config.yaml` on the
machine where it runs. Copying the VPS's config to your laptop won't work
on its own — `gateway.host: 0.0.0.0` is a *bind* address that the CLI
remaps to local loopback, not the VPS's public IP.

Set `COMIS_GATEWAY_URL` (and the matching token) on your laptop instead:

```bash theme={}
export COMIS_GATEWAY_URL=wss://comis.example.com/ws
export COMIS_GATEWAY_TOKEN=<the-token-from-the-VPS>
comis status
```

Use `wss://` (TLS) when the gateway is reachable over the public internet.
Use `ws://` only over a trusted network (VPN, SSH tunnel).

## Common issues

<AccordionGroup>
  <Accordion title="Setup script fails">
    Common causes:

    * **Docker not running** -- start Docker Desktop or the Docker daemon
    * **Port 4766 in use** -- stop the other process or set `COMIS_GATEWAY_PORT` in `.env`
    * **Missing openssl** -- the script uses `openssl rand -hex 32` for token generation; install it with your system package manager
  </Accordion>

  <Accordion title="Cannot access dashboard from host">
    **Symptom:** `curl http://127.0.0.1:4766/health` returns "connection refused"
    or "connection reset by peer" even though the container is reported `healthy`.

    **Fix:** Two distinct binds are involved — get them in the right place:

    1. **Host-side bind (which interface the host listens on):** controlled by `COMIS_GATEWAY_HOST` in the project-root Compose `.env`. The default is `127.0.0.1`; use `0.0.0.0` only when you intentionally want LAN exposure and have configured authentication and transport security.
    2. **Container-side bind (which interface the daemon listens on inside the container):** the base Compose file sets this to `0.0.0.0` so port forwarding can reach the daemon. A mounted `config.yaml` has higher precedence; if it sets `gateway.host: 127.0.0.1`, change it to `0.0.0.0` or remove the field.

    Other things to check:

    * The container is running and healthy: `docker compose ps`
    * No firewall is blocking the port
    * Confirm no additional Compose file intentionally replaces the loopback host-port mapping.
  </Accordion>

  <Accordion title="Container exits immediately">
    **Symptom:** The container starts but stops within seconds.

    **Fix:** Check the logs for the error:

    ```bash theme={}
    docker compose logs comis-daemon
    ```

    Common causes:

    * **Invalid config.yaml** -- check YAML syntax and required fields. The daemon writes a recovery snapshot at `~/.comis/config.last-good.yaml`; run `cp ~/.comis/config.last-good.yaml ~/.comis/config.yaml` to restore the previous-known-good config, then `docker compose restart`.
    * **Missing API key** -- verify `COMIS_ENV_FILE` (default `~/.comis/.env`) has the correct variable name
    * **Port already in use** -- another process is using port 4766 on the host
  </Accordion>

  <Accordion title="Daemon can't write to .env (`Is a directory` error)">
    **Symptom:** Bringing up Compose without first running `docker-setup.sh`,
    the daemon fails to persist credentials and you see
    `cannot create /home/comis/.comis/.env: Is a directory` in the logs.

    **Cause:** Docker bind-mounts `${COMIS_ENV_FILE:-~/.comis/.env}` into the
    container. If the host path doesn't exist, Docker silently creates it
    as a **directory** instead of a file, then every write fails.

    **Fix:** create the file before `docker compose up`:

    ```bash theme={}
    mkdir -p ~/.comis
    touch ~/.comis/.env
    chmod 600 ~/.comis/.env
    ```

    If `.env` already exists as a directory, remove it first:
    `rmdir ~/.comis/.env && touch ~/.comis/.env`.

    The repo's `docker-setup.sh` handles this automatically — this only
    affects manual `docker compose up`.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Configuration Guide" icon="gear" href="/installation/configuration">
    Customize your agent settings, add channels, and configure advanced options.
  </Card>

  <Card title="Verify Installation" icon="circle-check" href="/installation/verify">
    Run diagnostic commands to confirm everything is working.
  </Card>

  <Card title="Operations: Docker" icon="docker" href="/operations/docker">
    Docker deployment boundaries, multi-stage builds, and security hardening.
  </Card>
</CardGroup>
