Confer — deployment & self-hosting
How to run a full Confer instance yourself — on your laptop to try it, or on a server to share with others. Everything here is a real, tested path; nothing is aspirational.
Scope: this guide covers the single-instance, self-hosted setup. Public multi-tenant hosting, TLS termination, and federation hardening are out of scope for v0.1 — see
docs/02-architecture.mdfor the architectural direction.
What you get
One command starts the whole platform:
| Service | Image / build | Role |
|---|---|---|
client |
built from infra/client.Dockerfile |
Web UI + nginx reverse proxy (the only port exposed) |
gateway |
built from infra/gateway.Dockerfile |
Hono API, A2A endpoints, WebSocket |
migrate |
one-shot | runs Drizzle migrations, then exits |
postgres |
postgres:18-alpine |
primary datastore |
qdrant |
qdrant/qdrant:v1.19.0 |
vector search for the RAG knowledge base |
minio |
minio/minio |
S3-compatible file storage |
nginx (inside client) serves the SPA on port 80 and reverse-proxies
/api, /ws, /a2a, and /.well-known to the gateway. The gateway’s own port
(3000) is not published in production — everything goes through nginx on 80.
Prerequisites
- Docker with Compose v2 (
docker compose, notdocker-compose). The only hard requirement. - Node 18+ — only for
npx confer-cli(option A). The plain-Compose path below does without it. - Roughly 4 GB free RAM and 2 GB disk for images + volumes.
- Bun ≥ 1.1 — only if you want the hot-reload dev workflow (option B below) or to regenerate migrations.
A. One-command self-host (recommended)
Nothing to clone, nothing to build — this runs the published images:
npx confer-cli
confer-cli refuses to start unless Docker
is actually running, writes docker-compose.ghcr.yml and a 0600 .env into
~/.confer — JWT_SECRET, ENCRYPTION_KEY and the database and object-store
passwords, generated with crypto.randomBytes on first run and then reused — pulls the
images, applies migrations, and polls /health until the stack actually answers rather
than reporting success the moment containers start. npx confer-cli down stops it and
keeps the data; npx confer-cli logs follows the gateway.
Flags: --port (default 80), --dir (default ~/.confer), --version (image tag),
--project (compose project name).
The same thing by hand, for a host without Node:
curl -O https://raw.githubusercontent.com/hyhmrright/Confer/main/docker-compose.ghcr.yml
printf 'JWT_SECRET=%s\nENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
docker compose -f docker-compose.ghcr.yml up -d
This leaves POSTGRES_PASSWORD and MINIO_ROOT_PASSWORD at the compose file’s
defaults, which the CLI would have randomised — set both in .env on a shared host.
To run a modified tree instead, clone it and build with docker-compose.prod.yml:
git clone https://github.com/hyhmrright/Confer.git
cd Confer
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build
Either way, when it finishes:
- Open http://localhost.
- Click Register and create the first account. (Registration is rate-limited to 3 attempts per hour per IP.)
- Go to Settings and add an LLM API key (Claude / OpenAI / DeepSeek / Qwen /
Ollama). Keys are encrypted at rest with
ENCRYPTION_KEY(AES-256-GCM) and are never sent to the client.
That’s it — you now have a working Agent. Talk to it in the web UI, add contacts, and consult peer Agents.
Started with
npx confer-cli? Everything below that says-f docker-compose.prod.ymlapplies from~/.conferwith-f docker-compose.ghcr.ymlinstead — except updating, where there is nothing to rebuild: runnpx confer-cliagain.
Check it’s healthy
docker compose -f docker-compose.prod.yml ps # all services "running"/"healthy"; migrate is "exited (0)"
docker compose -f docker-compose.prod.yml logs -f gateway
Configuration
.env drives the production stack. The defaults in .env.example are functional
for local use but insecure — change the secrets before exposing the instance to
anyone else.
| Variable | Default (.env.example) |
Notes |
|---|---|---|
JWT_SECRET |
change-me-in-production |
Change this. Signs user session tokens. |
ENCRYPTION_KEY |
64 zeros | Change this. Must be 32 bytes as 64 hex chars. Generate: openssl rand -hex 32. Encrypts stored LLM keys. |
POSTGRES_PASSWORD |
confer (compose default) |
Database password. |
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD |
confer / confer-secret |
Object storage credentials. |
EXPOSE_PORT |
80 |
Host port the web UI binds to. Set e.g. 8080 if 80 is taken. |
TAVILY_API_KEY |
empty | Optional fallback for web search; a per-user key in Settings takes precedence. |
ADMIN_USERNAMES |
empty | Comma-separated usernames auto-promoted to the admin role on gateway startup. The accounts must already be registered. Admins log in with their normal account password and get the admin panel; they can then promote others from the UI. |
LLM / embedding / Tavily keys are not set in
.env— they live encrypted per user in the database and are configured through the Settings UI. The.envkeys are infrastructure secrets only.
After editing .env, apply it with:
docker compose -f docker-compose.prod.yml up -d
Updating
git pull
docker compose -f docker-compose.prod.yml up -d --build # migrate re-runs automatically
Instances created before 2026-08-29 need one migration first. The stack moved to PostgreSQL 18 and Qdrant 1.19, and neither reads storage the older version wrote. Nothing is lost and both failures are loud — postgres refuses to start and says why, qdrant panics on load — but the move needs a dump and restore. The procedure is in the Chinese guide;
npx confer-clichecks for it before starting anything and prints the same steps.
Resetting (wipes all data)
docker compose -f docker-compose.prod.yml down -v # -v also deletes the volumes
B. Local development (hot reload)
Run only the infra in Docker and the app code with Bun:
bun install
docker compose up -d # infra only — Postgres, Redis, NATS, Qdrant, MinIO (ports published on localhost)
bun run db:migrate
bun run dev # gateway on :3000, client (Vite) on :1420
- Web preview: http://localhost:1420 (Vite proxies
/api→ gateway on :3000). - Native desktop app:
cd packages/client && bunx tauri dev.
The dev docker-compose.yml publishes each infra port to localhost (5432, 6379,
4222, 6333, 9000/9001) so the locally-run gateway can reach them. See
CONTRIBUTING.md for the full developer workflow and the isolated test stack.
Connecting the Claude Code plugin
The confer-a2a plugin talks to the gateway over HTTP. Point it at the right
URL for your setup:
| Your setup | CONFER_GATEWAY_URL |
|---|---|
| One-command self-host (option A) | http://localhost (nginx on port 80; the gateway’s 3000 is not published) |
| Local dev (option B) | http://localhost:3000 (the default) |
| Remote instance | https://your-host |
/plugin marketplace add hyhmrright/Confer
/plugin install confer-a2a@confer
export CONFER_USERNAME=you
export CONFER_PASSWORD=secret
export CONFER_GATEWAY_URL=http://localhost # match the table above
The peer Agents you consult must already be contacts of your account (adding a
contact is the consent gate). Full plugin reference:
plugins/confer-a2a/README.md.
Exposing the instance to others
The one-command stack is single-tenant and listens on plain HTTP. Before putting it on the public internet:
- Put it behind a TLS-terminating reverse proxy (Caddy, Traefik, or nginx with a cert). A2A signature verification and DID:web both assume HTTPS in the real world.
- Set
PUBLIC_HOST(in.env) to the externally reachable host — including a non-defaultEXPOSE_PORT. Every DID this instance mints is derived from it, so this is not cosmetic: left atlocalhost, the identities you hand a peer resolve to the peer’s own loopback and federation cannot work at all. Set it before you create accounts. Changing it later re-hosts identities still carrying the oldlocalhostdefault on the next start (a one-off, logged); any peer already holding an old DID has to re-add the contact. - Change every default secret (
JWT_SECRET,ENCRYPTION_KEY, DB and MinIO passwords). - Registration is open by default. An admin can close it at any time from the
Admin → Config tab (
registration_open), or front it with an invite/allowlist.
Free public instance on Oracle Cloud (Always Free)
The cheapest way to run a always-on public test instance is Oracle Cloud’s
Always Free ARM tier (4 OCPU / 24 GB / 10 TB egress, no time limit). The whole
stack builds and runs on arm64.
- Create a VM: shape VM.Standard.A1.Flex (up to 4 OCPU / 24 GB), image Ubuntu 22.04+ (arm64). ARM capacity is tight in popular regions — pick a large region (Ashburn, London) and retry if you hit “out of capacity”.
- In the Console, open the VCN security list / NSG to allow inbound TCP 80 (and 443 later if you add TLS).
-
SSH in and run the bootstrap (installs Docker, opens the host firewall, clones, generates secrets, builds and starts the stack):
curl -fsSL https://raw.githubusercontent.com/hyhmrright/Confer/main/infra/oracle-bootstrap.sh | bashOr clone first and run
bash infra/oracle-bootstrap.sh. It is idempotent. - Open
http://<vm-ip>/, register, then grant yourself admin: setADMIN_USERNAMES=<you>in~/Confer/.envanddocker compose -f docker-compose.prod.yml up -d gateway.
This serves over plain HTTP by IP — fine for testing. For a stable did:web
identity and federation, point a domain at the IP, set PUBLIC_HOST, and add TLS
(see above).
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
port is already allocated on 80 |
Something else owns port 80. Set EXPOSE_PORT=8080 in .env and open http://localhost:8080. |
| Web UI loads but every request 500s | Check docker compose -f docker-compose.prod.yml logs gateway. Most often JWT_SECRET or ENCRYPTION_KEY is empty — they have no compose default, so they must be present in .env. |
migrate exits non-zero |
Postgres wasn’t healthy yet or DATABASE_URL is wrong. Re-run docker compose -f docker-compose.prod.yml up -d; migrate is idempotent. |
Plugin: login failed / 401 |
Wrong CONFER_GATEWAY_URL (see the table — prod is port 80, not 3000), or wrong username/password. |
Plugin: connection refused on :3000 |
You’re on the one-command setup; use http://localhost instead of :3000. |
| LLM calls fail | No LLM key configured for your user. Add one in Settings. |
| Embedding/RAG errors | See .claude/skills/rag-debug or run the rag-debug skill for Qdrant/embedding/MinIO diagnostics. |
See also
docs/02-architecture.md— system architecture and service boundariesCONTRIBUTING.md— developer setup, test stack, conventionsplugins/confer-a2a/README.md— Claude Code plugin reference