Skip to main content
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.
The Installation Docker page 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.

Production Dockerfile

The repository includes a 4-stage Dockerfile that separates application compilation from the runtime image. Comis TypeScript source and development dependencies are removed after compilation; runtime language toolchains and agent utilities are installed deliberately in the final stage.

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 Debian base
  • base-slim — smaller node:22-bookworm-slim Debian base
The COMIS_VARIANT build arg selects which base to use (default: slim). Stage 4 — final: Installs the system, media, sandbox, and language tools used at runtime (including Git, Python/pipx, ffmpeg, bubblewrap, Go, DuckDB, uv, and Rust), creates a non-root comis user (UID/GID 1000), copies the compiled application, and creates the comis CLI symlink. dumb-init runs as PID 1. Port 4766 has a health check with a 60s interval, 10s timeout, 30s start period, and 3 retries.

Build args

  • 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 (the entrypoint verifies its :99 socket before starting the daemon and exits if the display fails); COMIS_WITH_CLOAKBROWSER=1 installs CloakBrowser as an alternative Chromium runtime instead of stock Chrome. Runtime limitations and licensing are documented in Browser runtime options.

Installer-built variant (Dockerfile.install)

The repo ships a second Dockerfile that packs the local workspace and invokes install.sh inside Ubuntu rather than copying the main multi-stage build output directly:
Use this to exercise the installer’s non-interactive package-install and browser-provisioning branches in CI. It skips interactive setup, initialization, service management, dedicated-user creation, and the public-registry download path, so it is not a parity image for a managed host. The main Dockerfile remains the primary multi-stage image path.
The multi-stage build removes Comis TypeScript source and development dependencies from the final image. The configured runtime tools and language toolchains remain available to agents. The full Dockerfile is in the repository root.

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)
  • A mounted COMIS_ENV_FILE for persistent credentials, plus explicit pass-through for a small set of optional non-empty provider/channel overrides
  • 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 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

Build and run

View logs

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

Stop services

Volume management

The daemon uses two mount points inside the container:

Files in /home/comis/.comis

Use an explicit volume or bind mount for /home/comis/.comis. The image declares this path as a volume, so Docker may create an anonymous volume when you omit -v; that state is easy to orphan when replacing the container. The Compose file binds COMIS_DATA_DIR (default ~/.comis) so the location and backup lifecycle are explicit.

Backup

Health checks

The HEALTHCHECK directive in the Dockerfile sends a request to the GET /health endpoint. Configuration: The endpoint returns:
Docker uses the health check to determine container status: You can see the health status in docker ps output:
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 below).
  • Minimal base imagenode: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: 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:

Recommendations for Mac / Windows users

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:

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

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

Important caveat about secrets.db inside Docker

The default Docker layout keeps secrets.db and its master key in the same mounted data directory. Encryption protects the database at rest when the key is separated from a copied database, but it does not protect against a process or account that can read both files. Keep the mount private, verify the exec sandbox is active, and back up secrets.db together with COMIS_ENV_FILE. Do not interpret this limitation as a reason to switch silently to plaintext file storage; that mode is an explicit operator choice.

Adding a credential

For the exact credential names projected by the base Compose file, its entrypoint removes blank values before Comis loads COMIS_ENV_FILE. Explicit non-empty container values retain precedence over the mounted file at load time. In encrypted mode, a value already stored in secrets.db is authoritative over both sources. After setup, prefer comis secrets set <NAME> for encrypted, live-applied updates.

Environment file

Copy .env.docker.example from the repository root as your starting point:
This template contains Compose image, path, and network settings plus blank explicit overrides for Anthropic, OpenAI, Telegram, Discord, Slack, and the gateway token. It deliberately does not define SECRETS_MASTER_KEY; encrypted first boot writes that key to COMIS_ENV_FILE. Put other persistent provider and channel credentials in COMIS_ENV_FILE as well.
Keep API keys out of config.yaml. Use the mounted COMIS_ENV_FILE for first-boot credentials, then the encrypted secret store for managed updates.

Development overrides

The repository includes docker-compose.dev.yml as an explicit development overlay. Compose does not load it automatically, so plain docker compose up keeps the base file’s loopback-only host bindings and production settings. Development overrides change the following: Opt in only when you want the broader development posture:
The development overlay binds the gateway to 0.0.0.0:4766, sets NODE_ENV=development, uses the full image variant, and starts the web dashboard without a profile. Do not use it unintentionally on an untrusted network. Its !override tags require Docker Compose 2.24.4 or newer.

Web dashboard

The web dashboard is built from Dockerfile.web — a 2-stage build:
  1. Build stagenode:22-bookworm-slim, installs the @comis/web package and its dependencies, runs pnpm build to produce the Lit SPA
  2. Serve stagenginx: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: 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 vX.Y.Z produces comisai/comis:X.Y.Z, comisai/comis:X.Y, 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 page.

Updating

To update Comis to a new version:
If using the web dashboard:
The mounted /home/comis/.comis volume preserves your data across rebuilds. Only the application code is replaced.

Install with Docker

Quick-start Docker setup for getting started.

Reverse Proxy

Put Comis behind Nginx or Caddy for TLS and custom domains.

Daemon

How the daemon starts, runs, and shuts down.

Troubleshooting

Solutions to common issues.