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 produces a minimal production
image. Source code, build tools, and development dependencies are discarded after
compilation.
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-bookworm(includes debugging tools)base-slim— minimalnode:22-bookworm-slim
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 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 (entrypoint shim starts it on:99before the daemon);COMIS_WITH_CLOAKBROWSER=1installs 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:
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).
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) - 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
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:
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: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 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
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_KEYfrom 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.
.env mode is both simpler and
no less secure than running secrets.db with a co-located key.
Adding a credential
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:
.env.docker.example for details.
Development overrides
The repository includes adocker-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 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 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:/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.
