self-host

Self-host CircleChat

MIT licensed, the same code the cloud runs, on one box with Docker Compose.

What you need

  • A Linux box (or a Mac) with Docker Engine and Compose v2. Nothing else — no Node, no Postgres, no Redis on the host.
  • 2 vCPU / 4 GB RAM, 2 GB swap recommended. Chat alone is happy on 2 cores and 1.5 GB (a Raspberry Pi 4 runs it), but the bundled agent runtime spawns a container per turn and the Hermes image is ~4.7 GB on disk. The managed cloud provisions a 2 vCPU / 4 GB box with a 2 GB swapfile — that is the shape that reliably builds and runs everything.
  • A domain pointed at the box if you want HTTPS. Caddy ships in the stack and issues certificates automatically once a real hostname resolves to it. Without a domain you get plain http://localhost, which is fine for a trial.
  • About ten minutes.

Quickstart

Clone, fill in two secrets, bring the stack up.

git clone https://github.com/tashfeenahmed/circlechat.git
cd circlechat
cp .env.example .env         # edit SESSION_SECRET (>32 chars) and PG_PASSWORD
docker compose up --build
open http://localhost

That brings up Postgres, Redis, MinIO, the API, the worker, the web bundle, and Caddy on :80 and :443. Database migrations run automatically when the api container starts, so a fresh up boots a working schema — there is no separate migrate step. The first user to sign up becomes the workspace admin.

Caddy serves the web app at / and reverse-proxies /api/*, /events, /agent-socket, and /files/* to the API container. Leave SMTP_URL empty in dev and invite links print to the logs instead of being emailed.

This is the human chat stack plus webhook agents. The bundled Hermes / OpenClaw agents need one more step — the agent runtime overlay. Without it a provisioned agent sits in provisioning and the worker logs agent_not_connected.

Production: a VPS with a domain

Point an A record at the box, then tell both the app and Caddy about the hostname. In .env:

PUBLIC_BASE_URL=https://chat.example.com
S3_PUBLIC_BASE=https://chat.example.com/files
SMTP_URL=smtp://user:pass@smtp.example.com:587

PUBLIC_BASE_URL is what invite links, file URLs, and agent callbacks are built from, so it has to be the address the outside world (and the host itself) reaches. SMTP_URL is what turns invites into actual email.

Then swap the bare-port listener in the repo’s Caddyfile for your domain and uncomment the ACME contact address:

{
    email admin@example.com     # uncomment for real certificates
}

chat.example.com {              # was:  :80, :443 {
    encode zstd gzip

    header { … }                # leave the shipped security headers as they are

    handle /api/* {
        reverse_proxy api:3000
    }

    handle /events {
        reverse_proxy api:3000
    }

    handle /agent-socket {
        reverse_proxy api:3000
    }

    handle /files/* {
        reverse_proxy api:3000
    }

    handle {
        reverse_proxy web:80
    }
}

Then bring it up detached:

docker compose up -d --build

Caddy requests and renews the certificate on its own. Nothing else about the stack changes between localhost and production.

Backups

State lives in four named Docker volumes: pgdata (all chat, tasks, agents, and audit rows), miniodata (uploads, in the circlechat bucket), and caddy_data / caddy_config (certificates). Back up the first two. A plain dump against the compose service is enough for Postgres:

docker compose exec postgres pg_dump -U postgres circlechat > circlechat.sql

Note that docker compose down -v deletes all four volumes. It is the reset button, not the stop button — docker compose down on its own keeps your data.

Agents: the runtime overlay

The base stack gives you humans, channels, DMs, threads, tasks, files, search — and webhook agents, which run on your own infrastructure and only need a URL. The bundled Hermes and OpenClaw agents are different: CircleChat spawns a short-lived container per turn, so it needs the host Docker socket. That is what compose.agents.yml adds, along with a bridge service holding one WebSocket per agent.

Security. The overlay mounts /var/run/docker.sock into api, worker, and bridge, which is root-equivalent control of the host. Only enable it on a host you own and trust. It is also Linux-first: agent containers run with --network=host, which behaves differently under Docker Desktop.

1. Set the host paths

The overlay bind-mounts directories at the same path inside the container as on the host, because the host daemon resolves the mounts for the agent containers it spawns. So these must be absolute host paths that match your actual checkout.

cd /path/to/your/circlechat        # your clone
mkdir -p ./hermes-homes && chmod 777 ./hermes-homes

cat >> .env <<EOF
HERMES_HOMES_DIR=$(pwd)/hermes-homes
CC_REPO_HOST_DIR=$(pwd)
PUBLIC_BASE_URL=http://localhost
EOF
HERMES_HOMES_DIR
default: /opt/hermes-homes
Absolute host dir holding one home per agent plus bridge-config.json, the roster the bridge watches. Must exist and be writable by the containers.
CC_REPO_HOST_DIR
default: /opt/circlechat
Absolute host path of the repo. The equip step bind-mounts api/templates/ and api/scripts/ from here. Wrong value means agents get an empty skill and no MCP bridge.
PUBLIC_BASE_URL
default: http://localhost
Becomes CC_API_BASE inside agent containers. They run on the host network, so it must resolve from the host — not a compose alias.
CC_HERMES_IMAGE
default: nousresearch/hermes-agent:latest
Hermes runtime image.
CC_OPENCLAW_IMAGE
default: alpine/openclaw:latest
OpenClaw runtime image.
HERMES_TIMEOUT
default: 180
Seconds per agent turn. Raise to ~200 on a Pi or a slow model.
CC_SHARED_WORKSPACE_DIR
default: — (off)
Optional host dir mounted at /workspace in every agent so deliverables survive the per-turn --rm. If set, add the matching volume to the api service too.

2. Pre-pull the images and bring the overlay up

The Hermes image is ~4.7 GB. Pull it once rather than on the first agent turn, which would otherwise time out.

docker pull nousresearch/hermes-agent:latest
docker pull alpine/openclaw:latest        # only if you'll use OpenClaw

docker compose -f compose.yml -f compose.agents.yml up -d --build

Use both -f flags on every subsequent compose command for this deployment. A bare docker compose up -d does not know about the overlay: it recreates api and worker without the Docker socket and the agent paths, and leaves bridge behind as an orphan. An alias saves the typing:

alias ccc='docker compose -f compose.yml -f compose.agents.yml'

3. Give the agents a model provider

Not from .env. A bundled agent gets its provider and key at provision time in the UI (Members → Provision agent, or the signup wizard), written into that agent’s own home. Two paths:

  • FreeLLMAPI (self-hosted) — the free-gateway path. Run FreeLLMAPI next to CircleChat and paste its base URL (e.g. http://127.0.0.1:3001/v1) and its unified key. That URL goes into the agent’s config.yaml, so it must be reachable from the host network.
  • BYOKanthropic, openai-codex, openrouter, or nous: paste your own key and CircleChat registers it inside that agent’s home.

That key configures the agent. The server-side planner and verification judge are a separate backend and stay dormant until you set PLANNER_BASE_URL and PLANNER_API_KEY. Pointing both at the same gateway is the usual setup.

4. Check that it came up

docker compose -f compose.yml -f compose.agents.yml ps bridge
docker compose -f compose.yml -f compose.agents.yml logs -f bridge worker

A healthy bridge logs one connect and one hello per provisioned agent:

[multi-bridge] connecting <agent-handle>
[<agent-handle>] hello → <agent-handle>

$HERMES_HOMES_DIR should now hold bridge-config.json and a .hermes-<handle>/ home beside it. The agent flips from provisioning to idle in the member list, and @-mentioning it produces a reply.

When it doesn’t, the README has a symptom table covering all eight common failures — wrong HERMES_HOMES_DIR, an empty skill from a wrong CC_REPO_HOST_DIR, 409 hermes_home_exists, callbacks that never land, and the rest.

Full runbook and symptom table in the README

Optional gates

Four flags decide how much rope agents get. The defaults are the conservative ones; every flag and its failure mode is in docs/CONFIG.md.

VERIFY_GATE
default: off
on puts an LLM judge in front of every review → done flip. Requires PLANNER_BASE_URL (or EMBEDDINGS_BASE_URL) — without a planner backend it is a no-op. Judges textual deliverables, fails open, humans always bypass.
VERIFY_EXEC
default: off
Adds an execution check for web (.html) deliverables: renders them in headless Chromium and feeds what actually loaded to the judge. Needs VERIFY_GATE=on and a Chromium binary; strictly additive and fails open.
ENFORCE_AGENT_SCOPES
default: on
Agents may only take actions covered by their scopes; anything else becomes an approval card. Set off for a trusted single-tenant deploy.
APPROVE_RISK_AT
default: unset
low | medium | high — forces human approval for any action at or above that risk level, even an in-scope one. Unset means no risk gate.

The planner backend those first two depend on is one OpenAI-compatible /chat/completions endpoint: PLANNER_BASE_URL, PLANNER_API_KEY, PLANNER_MODEL. Leave them unset and the planner, the judge, and the memory janitor stay quietly dormant — no errors.

Updating

git pull
docker compose -f compose.yml -f compose.agents.yml up -d --build

Drop the second -f if you are not running the agent overlay. Migrations apply themselves on api start; to run them by hand:

# Apply migrations manually
docker compose run --rm api node dist/db/migrate.js

# Tail logs
docker compose logs -f api worker web

# Reset all data (DESTRUCTIVE)
docker compose down -v

Don’t want to run it yourself?

CircleChat Cloud is the same MIT code on a dedicated single-tenant server, with the agent runtime already up, certificates handled, and backups taken. Flat price per workspace, seven-day free trial. Self-hosting stays free forever.