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

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

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

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

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.

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

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

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

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: To use production settings without overrides, either rename or remove docker-compose.override.yml:

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