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-stageDockerfile 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 fromnode: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— fullnode:22-bookwormDebian basebase-slim— smallernode:22-bookworm-slimDebian base
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 fromdocker 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=defaultfor the full image with debugging tools, or leave asslimfor production. - Custom packages — set
COMIS_DOCKER_APT_PACKAGESto install additional system packages into the final image. - Browser tool — three independent flags mirror the bare-VPS installer:
COMIS_WITH_BROWSER=1installs Google Chrome + headless shared libs;COMIS_WITH_XVFB=1adds Xvfb (the entrypoint verifies its:99socket before starting the daemon and exits if the display fails);COMIS_WITH_CLOAKBROWSER=1installs 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:
Dockerfile
remains the primary multi-stage image path.
Docker Compose
Thedocker-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_HOSTandCOMIS_GATEWAY_PORTenv 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 (~/.comisby default) - A mounted
COMIS_ENV_FILEfor 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
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
wgeton port 8080
cli):
- Shares the daemon’s network namespace (
network_mode: service:comis-daemon) so it reaches the gateway at127.0.0.1:4766 - Interactive mode (stdin/tty enabled) for one-shot commands
- Security hardened:
no-new-privileges, dropsNET_RAWandNET_ADMINcapabilities
Using profiles
Build and run
View logs
"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
Backup
Health checks
TheHEALTHCHECK directive in the Dockerfile sends a request to the
GET /health endpoint. Configuration:
The endpoint returns:
You can see the health status in
docker ps output:
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
comisuser (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’sgatewayenv_set action,comis configure, or theenv.setRPC) and write itsconfig.last-good.yamlsnapshot next toconfig.yamlfor the bootstrap-failure recovery path. Prompt-injected exec is contained separately by the bwrap sandbox, which excludes/home/comis/.comisand/etc/comisfrom the per-command mount set entirely (see Per-command exec sandbox below). - Minimal base image —
node:22-bookworm-slimhas 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-privilegesand dropsNET_RAWandNET_ADMINcapabilities
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 alinuxkit 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 theexec 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:
/proc inside it, and Docker’s defaults
block all three layers:
- AppArmor — the default
docker-defaultAppArmor profile denies unprivileged user-namespace creation by binaries inside the container. Withoutapparmor=unconfined, bwrap fails withsetting up uid map: Permission denied. - seccomp — Docker’s default seccomp profile filters several
unshareandcloneflags that bwrap needs. Withoutseccomp=unconfined, the same userns setup is blocked at the syscall layer instead of AppArmor. - systempaths — Docker masks and read-only-binds parts of
/procby default, which makes bwrap’s--proc /procmount in the new namespace fail withCan't mount proc on /newroot/proc: Operation not permitted. Withoutsystempaths=unconfined, userns succeeds but the sandbox aborts at the proc-mount step.
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
Afterdocker compose up -d, confirm bwrap is active:
Secrets management
Comis supports three credential-storage modes, selected viasecurity.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
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:
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.
Development overrides
The repository includesdocker-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:
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 fromDockerfile.web — a 2-stage build:
- Build stage —
node:22-bookworm-slim, installs the@comis/webpackage and its dependencies, runspnpm buildto produce the Lit SPA - Serve stage —
nginx:alpine, copies the built SPA into the Nginx html directory with a custom config fromdocker/nginx.conf
Nginx configuration
Thedocker/nginx.conf provides:
- SPA fallback — all routes serve
index.htmlviatry_files - API reverse proxy —
/api/requests are proxied tocomis-daemon:4766with HTTP/1.1 upgrade support - WebSocket proxy —
/wsis 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
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:/home/comis/.comis volume preserves your data across rebuilds. Only the
application code is replaced.
Related pages
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.
