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

# Docker

> Running Comis in a Docker container for production

Docker packages Comis and all its dependencies into a single container, making
deployment consistent across any server. This page covers the production setup --
for a quick start, see the [Installation Docker page](/installation/install-docker).

<Info>
  The [Installation Docker page](/installation/install-docker) covers basic container
  setup using the `docker-setup.sh` script. This page focuses on production hardening:
  multi-stage builds, health checks, security, volume management, and the full
  service architecture.
</Info>

## Production Dockerfile

The repository includes a 4-stage `Dockerfile` that produces a minimal production
image. Source code, build tools, and development dependencies are discarded after
compilation.

### Build architecture

**Stage 1 -- build:** Starts from `node:22-bookworm` with corepack/pnpm enabled.
Copies dependency manifests first for layer caching, installs dependencies with
a BuildKit cache mount on the pnpm store, compiles all packages with `pnpm build`,
then prunes devDependencies and removes `.d.ts`, `.map`, and `.tsbuildinfo` files.

**Stage 2 -- runtime-assets:** Copies the built output from Stage 1 and strips
remaining source files (`src/`, `test/`, `tsconfig*.json`, `vitest.config.*`)
so only `dist/` and `node_modules` remain.

**Stage 3 -- base variant selection:** Two base image options are defined:

* `base-default` -- full `node:22-bookworm` (includes debugging tools)
* `base-slim` -- minimal `node:22-bookworm-slim`

The `COMIS_VARIANT` build arg selects which base to use (default: `slim`).

**Stage 4 -- final:** Installs runtime dependencies (`procps`, `curl`,
`ca-certificates`, `dumb-init`, `git`), creates a non-root `comis` user
(UID/GID 1000), copies from runtime-assets, creates a `comis` CLI symlink at
`/usr/local/bin/comis`, and configures dumb-init as PID 1. Exposes port 4766
with a health check (60s interval, 10s timeout, 30s start period, 3 retries).

### Build args

```dockerfile theme={null}
ARG COMIS_NODE_BOOKWORM_IMAGE="node:22-bookworm"
ARG COMIS_NODE_BOOKWORM_SLIM_IMAGE="node:22-bookworm-slim"
ARG COMIS_VARIANT="slim"
ARG COMIS_DOCKER_APT_PACKAGES=""
ARG COMIS_WITH_BROWSER=0
ARG COMIS_WITH_XVFB=0
ARG COMIS_WITH_CLOAKBROWSER=0
```

* **Digest pinning** -- the base image ARGs accept `@sha256:` digests for
  reproducible builds. Override at build time with values resolved from
  `docker inspect --format='{{.RepoDigests}}' node:22-bookworm`, e.g.
  `docker build --build-arg COMIS_NODE_BOOKWORM_IMAGE=node:22-bookworm@sha256:<digest> ...`.
* **Variant selection** -- set `COMIS_VARIANT=default` for the full image with
  debugging tools, or leave as `slim` for production.
* **Custom packages** -- set `COMIS_DOCKER_APT_PACKAGES` to install additional
  system packages into the final image.
* **Browser tool** -- three independent flags mirror the bare-VPS installer:
  `COMIS_WITH_BROWSER=1` installs Google Chrome + headless shared libs;
  `COMIS_WITH_XVFB=1` adds Xvfb (entrypoint shim starts it on `:99` before the
  daemon); `COMIS_WITH_CLOAKBROWSER=1` installs CloakBrowser (stealth Chromium)
  instead of stock Chrome. All implications and the datacenter-IP caveat are
  documented in [agent-tools/browser](/agent-tools/browser#stealth-mode-anti-bot-detection).

### Installer-built variant (`Dockerfile.install`)

The repo ships a second Dockerfile that runs `install.sh` end-to-end inside
the container instead of doing the multi-stage TypeScript build. Same build
args, same final image shape, but exercises the bare-VPS installer code path:

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

Use this for CI validation of `install.sh` changes, or when you want strict
parity with a bare-VPS deploy. The main `Dockerfile` remains the production
path (smaller, faster, no apt repo activity in the final layer).

<Tip>
  The multi-stage build keeps the final image small. Only compiled JavaScript,
  production dependencies, and minimal system tools are included. The full
  `Dockerfile` is in the repository root.
</Tip>

## Docker Compose

The `docker-compose.yml` in the repository root defines three services with
a profile-based activation model.

### Services

**comis-daemon** (always runs):

* Gateway port binding via `COMIS_GATEWAY_HOST` and `COMIS_GATEWAY_PORT` env vars
  (default: `127.0.0.1:4766`)
* Volumes for `/home/comis/.comis` (persistent data) and `/etc/comis` (config); both bind-mount the same host dir (`~/.comis` by default)
* Environment variable pass-through for secrets, provider keys, and channel tokens
* Health check: `curl -sf http://127.0.0.1:4766/health` (60s interval, 10s
  timeout, 30s start period, 3 retries)
* Resource limits: 2G memory / 2 CPU (limit), 256M / 0.5 CPU (reservation)
* JSON log rotation: 50MB max size, 5 files

**comis-web** (profile: `web`):

* Nginx-served Lit SPA dashboard on port 8080
* Built from `Dockerfile.web` (see [Web dashboard](#web-dashboard) below)
* Depends on a healthy `comis-daemon`
* Health check via `wget` on port 8080

**comis-cli** (profile: `cli`):

* Shares the daemon's network namespace (`network_mode: service:comis-daemon`)
  so it reaches the gateway at `127.0.0.1:4766`
* Interactive mode (stdin/tty enabled) for one-shot commands
* Security hardened: `no-new-privileges`, drops `NET_RAW` and `NET_ADMIN` capabilities

### Using profiles

```bash theme={null}
# Start only the daemon (default)
docker compose up -d

# Start daemon + web dashboard
docker compose --profile web up -d

# Run a CLI command against the running daemon
docker compose --profile cli run --rm comis-cli status
docker compose --profile cli run --rm comis-cli agent list
```

### Build and run

```bash theme={null}
# Build and start (first time or after code changes)
docker compose up -d --build

# Build web dashboard image (if using web profile)
docker compose --profile web build
docker compose --profile web up -d
```

### View logs

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

Look for the `"Comis daemon started"` message confirming the daemon is running.

### Stop services

```bash theme={null}
# Stop daemon
docker compose down

# Stop web dashboard
docker compose --profile web down
```

## Volume management

The daemon uses two mount points inside the container:

| Container path       | Purpose                                                                                                                                 | Default host path |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `/home/comis/.comis` | SQLite databases, logs, traces, secrets, `.env`                                                                                         | `~/.comis`        |
| `/etc/comis`         | Canonical Linux config path — same host dir as above; the daemon reads `config.yaml` here and writes `config.last-good.yaml` next to it | `~/.comis`        |

### Files in `/home/comis/.comis`

| File         | What it contains                                 |
| ------------ | ------------------------------------------------ |
| `memory.db`  | Agent conversations and memory (SQLite database) |
| `daemon.log` | Log files (if file logging is enabled)           |
| `secrets.db` | Encrypted secrets store                          |
| `traces/`    | Execution traces                                 |
| `.env`       | Runtime environment (token, network settings)    |

<Warning>
  If you do not mount a volume for `/home/comis/.comis`, you will **lose all agent memory and
  configuration** when the container restarts. The Compose file mounts
  `COMIS_DATA_DIR` (default `~/.comis`) to `/home/comis/.comis` to ensure persistence.
</Warning>

### Backup

```bash theme={null}
docker compose stop comis-daemon
docker cp comis-daemon:/home/comis/.comis ./comis-backup
docker compose start comis-daemon
```

## Health checks

The `HEALTHCHECK` directive in the Dockerfile sends a request to the
`GET /health` endpoint. Configuration:

| Parameter    | Value |
| ------------ | ----- |
| Interval     | 60s   |
| Timeout      | 10s   |
| Start period | 30s   |
| Retries      | 3     |

The endpoint returns:

```json theme={null}
{ "status": "ok", "timestamp": "2026-03-15T10:00:00.000Z" }
```

Docker uses the health check to determine container status:

| Status      | What it means                                         |
| ----------- | ----------------------------------------------------- |
| `healthy`   | The daemon is running and the gateway is responding   |
| `unhealthy` | Three consecutive health checks failed                |
| `starting`  | Within the 30s start period; failures are not counted |

You can see the health status in `docker ps` output:

```bash theme={null}
docker ps
```

```
CONTAINER ID   IMAGE        STATUS                   PORTS
abc123         comis:local  Up 5 minutes (healthy)   127.0.0.1:4766->4766/tcp
```

With `restart: unless-stopped` in Compose, Docker automatically restarts the
container if it exits.

## Security

The production setup includes several hardening measures:

* **Non-root user** -- the container runs as the `comis` user (UID/GID 1000),
  not as root
* **dumb-init** -- proper PID 1 signal handling prevents zombie processes and
  ensures clean shutdowns
* **Digest-pinned base images** -- ARGs at the top of the Dockerfile accept
  `@sha256:` digests for reproducible, tamper-evident builds
* **UID/GID 1000** -- predictable file ownership on host-mounted volumes
* **Writable state mounts** -- both `/home/comis/.comis` (data, including
  `.env`) and `/etc/comis` (config) are mounted read-write so the daemon
  can persist new credentials (via the agent's `gateway` env\_set action,
  `comis configure`, or the `env.set` RPC) and write its
  `config.last-good.yaml` snapshot next to `config.yaml` for the
  bootstrap-failure recovery path. Prompt-injected exec is contained
  separately by the bwrap sandbox, which excludes `/home/comis/.comis`
  and `/etc/comis` from the per-command mount set entirely (see
  [Per-command exec sandbox](#per-command-exec-sandbox-bubblewrap) below).
* **Minimal base image** -- `node:22-bookworm-slim` has fewer packages and a
  smaller attack surface than the full image
* **No unnecessary ports** -- only port 4766 is exposed
* **Resource limits** -- memory and CPU limits prevent runaway processes (2G/2CPU
  limit, 256M/0.5CPU reservation)
* **JSON log rotation** -- daemon logs rotate at 50MB with 5 files retained,
  preventing disk exhaustion
* **CLI hardening** -- the CLI service sets `security_opt: no-new-privileges`
  and drops `NET_RAW` and `NET_ADMIN` capabilities

### Platform Support

**Production deployments must run on a Linux host** -- bare metal, a Linux VPS,
or any cloud Linux VM. macOS and Windows Docker Desktop are supported for
development and testing only.

#### Why

Docker Desktop runs containers inside a `linuxkit` VM. Its kernel does not allow
a nested PID namespace to mount a fresh `procfs` -- the operation EPERMs even
when the container has `apparmor=unconfined` and `seccomp=unconfined`. The
bubblewrap exec sandbox always uses `--unshare-pid` paired with `--proc /proc`,
so on Docker Desktop every `bwrap` invocation aborts at the proc-mount step
before forking the inner shell.

To keep the agent functional for local testing despite this kernel limitation,
the daemon detects the failure mode at startup (via a one-shot smoke test) and
**automatically disables the exec sandbox when running inside a container with
a non-functional bwrap**. Behaviour by environment:

| Environment                               | Sandbox state         | `exec` works?        | Trust boundary                                     |
| ----------------------------------------- | --------------------- | -------------------- | -------------------------------------------------- |
| Linux host (bare metal, VPS, cloud VM)    | bwrap active          | yes, sandboxed       | per-command bwrap mount-set isolation              |
| Docker Desktop on macOS / Windows         | **auto-disabled**     | yes, **unsandboxed** | the container itself (effectively none, see below) |
| Bare-metal Linux with broken bwrap (rare) | bwrap returned anyway | no, fails at runtime | n/a -- operator must fix kernel/userns config      |

Inside Docker Desktop with the sandbox auto-disabled, agent-issued shell
commands run as the `comis` user with full read access to everything the
daemon owns inside the container -- `/home/comis/.comis/.env`, the encrypted `secrets.db`,
`/etc/comis/config.yaml`, channel tokens, the `SECRETS_MASTER_KEY`. A single
prompt-injected `cat /home/comis/.comis/.env` exfiltrates them all. **This is why
Docker Desktop is dev/testing only.** On a real Linux host the sandbox
runs unrestricted and these paths are not mounted into the bwrap'd
process at all.

You can confirm the active mode in the daemon's startup logs:

```bash theme={null}
docker compose logs comis-daemon | grep -E "Exec sandbox|smoke test"
```

| Log line                                                                                 | Meaning                                                                  |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `Exec sandbox provider detected` `provider=bwrap`                                        | Sandbox is active.                                                       |
| `Exec sandbox DISABLED ... shell commands will run UNSANDBOXED`                          | Auto-disabled fallback. **Do not use this configuration in production.** |
| `bwrap installed but smoke test failed -- exec sandbox is non-functional on this kernel` | Bare-metal kernel rejected bwrap. Investigate and fix; do not deploy.    |

#### Recommendations for Mac / Windows users

| Use case                                                    | Recommendation                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Local development / quick try-out                           | Docker Desktop is the fastest path. Sandbox is auto-disabled -- treat the container's data dir as untrusted, never put real production secrets in `~/.comis-docker/.env`.                                                                                                                                                                   |
| Local development that exercises `exec` with a real sandbox | Use a Linux VM on your Mac instead of Docker Desktop: [Lima](https://lima-vm.io/), [Colima with the `vz` VM type](https://github.com/abiosoft/colima), [OrbStack](https://orbstack.dev/) with a Linux distro, or [UTM](https://mac.getutm.app/). The container kernel inside those is a real Linux kernel and bubblewrap runs unrestricted. |
| Production / multi-user deployment                          | Deploy to a real Linux host: any major cloud VM, a self-hosted Linux server, or a single-board computer running Linux. The same image and compose files work without changes.                                                                                                                                                               |

### Per-command exec sandbox (bubblewrap)

The Docker image ships **bubblewrap (`bwrap`)** and the daemon uses it as a
per-command sandbox for every shell command issued by the agent's `exec`
skill. This is **not** redundant with Docker's container isolation -- the
two boundaries protect different things:

| Boundary   | What it isolates                                                                                                                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Docker** | The container from the host kernel and other containers                                                                                                                                                            |
| **bwrap**  | Each agent-issued shell command from the daemon's own data inside the container -- `/home/comis/.comis/.env`, `/home/comis/.comis/secrets.db`, `/etc/comis/config.yaml`, channel tokens, the master encryption key |

#### Why this matters

Comis processes user input (chat messages, channel events) through an LLM
that has tool-use authority. A prompt-injection-style payload can drive the
`exec` skill to run arbitrary shell commands. Without bwrap, those commands
run as the `comis` user inside the container -- the same user that owns
`/home/comis/.comis` -- so a single `cat /home/comis/.comis/.env` would exfiltrate `SECRETS_MASTER_KEY`,
which decrypts every credential ever stored in `secrets.db`. That makes the
"container is the trust boundary" model unsafe for any deployment that
accepts untrusted user input *and* stores secrets *and* enables `exec`.

bwrap closes this gap by running each exec'd command under
`--unshare-all` (new mount/pid/ipc/uts/user namespaces, only network is
shared) and only mounting the agent's workspace, system binaries
(read-only), and a fresh `tmpfs` for `/tmp`. `/home/comis/.comis`, `/etc/comis`, and
`/home/comis` are not mounted into the sandbox -- they don't exist from
the perspective of an exec'd command.

#### Required compose security\_opt

`docker-compose.yml` configures the daemon service with:

```yaml theme={null}
security_opt:
  - apparmor=unconfined
  - seccomp=unconfined
  - systempaths=unconfined
```

These opt-outs are required because bwrap needs to create a user + pid
namespace **and** mount a private `/proc` inside it, and Docker's defaults
block all three layers:

* **AppArmor** -- the default `docker-default` AppArmor profile denies
  unprivileged user-namespace creation by binaries inside the container.
  Without `apparmor=unconfined`, bwrap fails with `setting up uid map:
  Permission denied`.
* **seccomp** -- Docker's default seccomp profile filters several `unshare`
  and `clone` flags that bwrap needs. Without `seccomp=unconfined`, the
  same userns setup is blocked at the syscall layer instead of AppArmor.
* **systempaths** -- Docker masks and read-only-binds parts of `/proc` by
  default, which makes bwrap's `--proc /proc` mount in the new namespace fail
  with `Can't mount proc on /newroot/proc: Operation not permitted`. Without
  `systempaths=unconfined`, userns succeeds but the sandbox aborts at the
  proc-mount step.

<Warning>
  **Ubuntu 23.10+ / 24.04 hosts need a host-level change too.** These hosts set
  `kernel.apparmor_restrict_unprivileged_userns=1`, which denies user namespaces
  to the now-**unconfined** container `bwrap` — so even with all three
  `security_opt` entries, bwrap fails at `setting up uid map: Permission denied`.
  On such hosts also run, **on the host**:

  ```bash theme={null}
  sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0   # persist in /etc/sysctl.d
  ```

  (Alternatively, load an AppArmor profile that grants `userns` to
  `/usr/bin/bwrap`.) This is a host setting; `docker-compose.yml` cannot set it.
</Warning>

When bwrap fails, the daemon does not abort -- it logs a WARN and runs
agent-issued shell commands unsandboxed. Always check the startup logs for
`Exec sandbox provider detected` (provider name `bwrap`) to confirm the
sandbox is active — if you see the sandbox disabled, the broker's network
containment is not enforced.

#### Verifying the sandbox

After `docker compose up -d`, confirm bwrap is active:

```bash theme={null}
# Should print "bwrap"
docker compose logs comis-daemon | grep "Exec sandbox provider detected"

# Should print "/usr/bin/bwrap"
docker compose exec comis-daemon which bwrap

# Should fail with "Permission denied" or "No such file or directory"
# (bwrap's mount set excludes /home/comis/.comis)
docker compose exec comis-daemon bwrap \
  --unshare-all --share-net \
  --ro-bind /usr /usr --ro-bind /lib /lib --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin --ro-bind /etc/passwd /etc/passwd \
  --tmpfs /tmp \
  /bin/sh -c 'cat /home/comis/.comis/.env'
```

#### Opting out (not recommended)

If you have a deployment where the agent processes only trusted input and
you want a smaller image or different isolation strategy, you can rebuild
without bubblewrap:

```yaml theme={null}
# docker-compose.override.yml
services:
  comis-daemon:
    build:
      args:
        # Override the apt list — note the daemon will log a startup INFO
        # noting the sandbox is absent, and `exec` runs unsandboxed.
        COMIS_DOCKER_APT_PACKAGES: ""
    security_opt: !override
      - no-new-privileges:true
```

You also need to disable the `exec` skill in your agent config, or accept
that prompt-injected commands can read all daemon state inside the
container. There is no middle ground.

### Secrets management

Comis supports three credential-storage modes, selected via `security.storage`
in `config.yaml`. **The encrypted `secrets.db` is the default** — the master key
is auto-generated on first boot and written to `~/.comis/.env` (mode 0600). To
opt out, set `security.storage: env` (or `file`).

| Mode (`security.storage`)    | When to use                                                                                                                                                              | How it works                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`encrypted` (default)**    | All deployments. The encrypted store is active immediately on first boot — no manual key generation required.                                                            | On first boot with no `SECRETS_MASTER_KEY` present, the daemon auto-generates a 32-byte master key, writes it to `<data dir>/.env` (mode 0600), and opens `secrets.db` on the same boot. **Back up `~/.comis/.env`** — losing `SECRETS_MASTER_KEY` makes `secrets.db` permanently unreadable (AES-256-GCM, no key escrow). All three credential families — named secrets, OAuth profiles, and **MCP OAuth tokens** — live in `secrets.db` (the `secrets`, `oauth_profiles`, and `mcp_credentials` tables); no plaintext `mcp-tokens/` directory is written. |
| **`env` / `file` (opt-out)** | Single-tenant deployments where you want to manage credentials exclusively via env vars (`env`) or plaintext-at-0600 files (`file`) and do not need the encrypted store. | Set `security.storage: env` (or `file`). The daemon boots in that mode and emits a startup WARN. Provider keys and channel tokens come from compose's `environment:` pass-through and from `<data dir>/.env`. In `file` mode MCP OAuth tokens are written to the plaintext `mcp-tokens/` directory (0600), matching `secrets.json` / `auth-profiles.json`. In `env` mode the daemon does **not** create `secrets.db`, and MCP OAuth login is unavailable (no writable token store — `comis mcp login` fails with an actionable storage-mode error).         |

#### Important caveat about `secrets.db` inside Docker

Storing both the encrypted database (`/home/comis/.comis/secrets.db`) and the master
key (`SECRETS_MASTER_KEY` env var) inside the same container provides
**no additional protection** against an in-container compromise -- anyone
who can read `/proc/<daemon-pid>/environ` or your compose config can
recover the key and decrypt the DB. The encryption pays off only when:

* You inject `SECRETS_MASTER_KEY` from a host-level secret manager
  (HashiCorp Vault, AWS Secrets Manager, Docker secrets backed by a
  tmpfs mount) so it never lives on disk in the container, AND
* You can rely on the bwrap exec sandbox (see above) to keep the key
  out of agent-issued processes.

For most Docker deployments, the flat `.env` mode is both simpler and
no less secure than running `secrets.db` with a co-located key.

#### Adding a credential

```bash theme={null}
# Envfile-only mode: add to your repo .env (for compose pass-through)
# OR to the data dir's .env file (read by the daemon at startup)
echo "TELEGRAM_BOT_TOKEN=12345:abc..." >> ~/.comis/.env

# Then restart the daemon to reload the file
docker compose restart comis-daemon
```

The daemon prefers values already in `process.env` (set by compose) over
values from `<data dir>/.env`, so per-deployment overrides via compose's
`environment:` block always win.

### Environment file

Copy `.env.docker.example` from the repository root as your starting point:

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

This template includes all configurable variables: image selection, data/config
paths, network binding, gateway token, secrets master key, LLM provider keys,
and channel tokens. See the comments in `.env.docker.example` for details.

<Tip>
  Pass API keys via environment variables (using `.env` or docker compose environment
  section), not in the config file mounted into the container. Environment variables
  are not written to the image layers.
</Tip>

## Development overrides

The repository includes a `docker-compose.override.yml` that Docker Compose
activates automatically when the file exists alongside `docker-compose.yml`.

Development overrides change the following:

| Setting         | Production               | Development                                    |
| --------------- | ------------------------ | ---------------------------------------------- |
| Image variant   | `slim` (bookworm-slim)   | `default` (full bookworm with debugging tools) |
| Port binding    | `127.0.0.1:4766`         | `0.0.0.0:4766` (all interfaces)                |
| NODE\_ENV       | `production`             | `development`                                  |
| Resource limits | 2G / 2 CPU               | 4G / 4 CPU                                     |
| Web dashboard   | Requires `--profile web` | Starts automatically (no profile needed)       |

To use production settings without overrides, either rename or remove
`docker-compose.override.yml`:

```bash theme={null}
mv docker-compose.override.yml docker-compose.override.yml.bak
```

## Web dashboard

The web dashboard is built from `Dockerfile.web` -- a 2-stage build:

1. **Build stage** -- `node:22-bookworm-slim`, installs the `@comis/web` package
   and its dependencies, runs `pnpm build` to produce the Lit SPA
2. **Serve stage** -- `nginx:alpine`, copies the built SPA into the Nginx html
   directory with a custom config from `docker/nginx.conf`

### Nginx configuration

The `docker/nginx.conf` provides:

* **SPA fallback** -- all routes serve `index.html` via `try_files`
* **API reverse proxy** -- `/api/` requests are proxied to `comis-daemon:4766`
  with HTTP/1.1 upgrade support
* **WebSocket proxy** -- `/ws` is proxied to the daemon's WebSocket endpoint
  with an 86400s (24h) read timeout
* **Static asset caching** -- JS, CSS, images, and fonts get 30-day cache
  headers with `Cache-Control: public, immutable`

The dashboard runs on port 8080 with a health check via `wget`.

## CI/CD

The repository ships two automated Docker workflows:

| Workflow                | Registry                              | Trigger                    |
| ----------------------- | ------------------------------------- | -------------------------- |
| `docker-release.yml`    | GitHub Container Registry (`ghcr.io`) | Push to `main` or `v*` tag |
| `dockerhub-release.yml` | Docker Hub (`comisai/comis`)          | `v*` tag only              |

Both workflows build multi-arch manifests (`linux/amd64` + `linux/arm64`) and
produce both `default` and `slim` variants. The Docker Hub workflow version tags
are derived directly from the git tag — pushing `v1.0.26` produces
`comisai/comis:1.0.26`, `comisai/comis:1.0`, and `comisai/comis:latest`
(plus the matching `-slim` variants).

For full details on image tags, required secrets, and manual publishing, see the
[Docker Hub Publishing](/operations/docker-hub) page.

## Updating

To update Comis to a new version:

```bash theme={null}
# Pull latest code
git pull

# Rebuild and restart
docker compose up -d --build
```

If using the web dashboard:

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

The mounted `/home/comis/.comis` volume preserves your data across rebuilds. Only the
application code is replaced.

## Related pages

<CardGroup cols={2}>
  <Card title="Install with Docker" icon="docker" href="/installation/install-docker">
    Quick-start Docker setup for getting started.
  </Card>

  <Card title="Reverse Proxy" icon="shield-halved" href="/operations/reverse-proxy">
    Put Comis behind Nginx or Caddy for TLS and custom domains.
  </Card>

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

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