DocsGetting started

Architecture & deployment topology

How KHUB's containers, storage volumes, and public ingress fit together in production.

Process topology#

A production KHUB instance is a fixed set of containers (see docker-compose.yml for the reference topology):

ServiceExposed asNotes
caddyPublic HTTPS (80/443)Sole public ingress. Automatic TLS via ACME when given a real domain. Routes by path — see the table below.
webInternal onlyNext.js server in standalone output mode; renders pages and proxies its own /api/* fetches to api.
apiInternal onlyThe NestJS monolith. Stateless with respect to HTTP (state lives in Postgres/Redis/disk volumes), so it can be scaled horizontally behind Caddy if needed.
runnerInternal only, no inbound trafficLong-polls Redis for CI jobs. Scale this independently of api to add CI capacity; each replica registers itself as a distinct Runner row keyed by RUNNER_NAME.
ssh-gitPublic TCP (22, or a configured alternate port)Only speaks the Git SSH protocol; not part of the HTTPS surface.
mcpInternal only (proxied at /mcp)See MCP integration.
postgresInternal onlySingle source of truth for all relational data.
redisInternal onlyCI job queue (BullMQ) only — not used as a cache or session store.

Caddy path routing#

Caddyfile routing table
/v2*                       → oci registry (api, Docker Distribution v2)
/mcp*                      → mcp bridge
/*.git/info/refs, /*/git-* → api (Git Smart HTTP)
/api*                      → api (REST, WebSocket upgrade for /api/socket.io)
/*                         → web (Next.js)

WebSocket upgrades for chat/voice signaling are negotiated over /api/socket.io, so they pass through the same /api* rule — there is no separate WebSocket ingress to configure.

Storage volumes#

State that must survive a redeploy lives in named volumes, not container filesystems:

VolumeContents
postgres_dataThe PostgreSQL data directory.
git_dataEvery bare repository (<repositoryId>.git), including hooks.
ci_dataCI job logs and cached job workspaces (see CI/CD pipelines).
lfs_dataGit LFS object storage, content-addressed by OID.
packages_datanpm-compatible package tarballs.
oci_dataContainer image blobs and manifests (content-addressed by digest).
release_assets, uploads_dataRelease download assets and general file uploads (avatars, banners, attachments).

A complete backup is: a pg_dump of postgres_data, plus a file-level copy of every other volume. There is no separate metadata store to reconcile — Postgres is authoritative for everything except the byte content addressed by the volumes above.

Why one process for so much#

apps/api deliberately hosts the REST API, the Git protocol endpoints, the OCI registry, the npm registry, SCIM, and the chat WebSocket gateway in a single NestJS application rather than as separate microservices. They share one Prisma client, one permission-resolution code path, and one audit log, which is what makes "the same role change instantly affects Git access, package publish rights, and API tokens" true without a synchronization step between services. The only components pulled out of that process are the ones with a genuinely different execution model: runner (needs Docker socket access and untrusted-code isolation), ssh-git (a different wire protocol entirely), and mcp (a translation layer with its own protocol, kept stateless and disposable).