Self-hosting
Docker compose
A reference docker-compose.yml that runs the entire stack on one host. Good enough for a small team; the building block for larger deployments.
There's an easier way
The npm installer generates this exact file for you, picks free ports, and brings the stack up in one command. Use this page when you want to hand-write or customize the compose file.
Layout
my-team/
├── .env
└── docker-compose.ymlPostgres and the per-user code-tunnel auth data live in named Docker volumes — no bind mounts to manage.
.env
# Postgres — used by the postgres service AND the api DATABASE_URL
POSTGRES_USER=withvibe
POSTGRES_PASSWORD=$(openssl rand -hex 16)
POSTGRES_DB=withvibe
# Signs session JWTs. Generate with: openssl rand -hex 32
INTERNAL_JWT_SECRET=replace-with-32-bytes-of-randomness
# Optional — can also be set later from the workspace UI
ANTHROPIC_API_KEY=
GITHUB_TOKEN=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Public-facing URLs — whatever the browser uses to reach the stack
WEB_PUBLIC_URL=http://localhost:3000
API_PUBLIC_URL=http://localhost:4000
PUBLIC_HOST=localhost
# Where the api stores per-env clones on the host (bind-mounted at the
# SAME path inside the container — see note below)
REPO_BASE_DIR=${HOME}/.withvibe/reposdocker-compose.yml
name: withvibe
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-withvibe}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-withvibe}
POSTGRES_DB: ${POSTGRES_DB:-withvibe}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-withvibe} -d ${POSTGRES_DB:-withvibe}"]
interval: 5s
timeout: 3s
retries: 20
networks: [withvibe]
api:
image: ghcr.io/withvibe/api:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-withvibe}:${POSTGRES_PASSWORD:-withvibe}@postgres:5432/${POSTGRES_DB:-withvibe}
INTERNAL_JWT_SECRET: ${INTERNAL_JWT_SECRET}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
GITHUB_TOKEN: ${GITHUB_TOKEN}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
WEB_PUBLIC_URL: ${WEB_PUBLIC_URL:-http://localhost:3000}
API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:4000}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
REPO_BASE_DIR: ${REPO_BASE_DIR:-/root/.withvibe/repos}
NODE_ENV: production
PORT: "4000"
ports:
- "${API_HOST_PORT:-4000}:4000"
volumes:
# DooD: the api drives the host Docker daemon to spawn env sidecars
- /var/run/docker.sock:/var/run/docker.sock
# Path-parity: host and container see clones at the SAME absolute path
- ${REPO_BASE_DIR:-/root/.withvibe/repos}:${REPO_BASE_DIR:-/root/.withvibe/repos}
extra_hosts:
- "host.docker.internal:host-gateway"
networks: [withvibe]
web:
image: ghcr.io/withvibe/web:latest
restart: unless-stopped
depends_on:
- api
environment:
API_BASE_URL: http://api:4000
NODE_ENV: production
PORT: "3000"
ports:
- "${WEB_HOST_PORT:-3000}:3000"
networks: [withvibe]
volumes:
postgres-data:
networks:
withvibe:
name: withvibeThe api mounts the Docker socket
There is no separate "runner" service — the api spawns one container per env directly on the host daemon (Docker-out-of-Docker), which is why
/var/run/docker.sock is mounted into it. That grants the api root-equivalent access to the host. Run it on a dedicated host, or use a rootless / DinD setup, before exposing this to untrusted users.Why the matching REPO_BASE_DIR mount
The api emits
docker run -v <path>:... commands that the host daemon executes, so the clone path must resolve identically inside and outside the container. Keep both sides of that bind mount equal.Bring it up
docker compose pull
docker compose up -d
docker compose logs -f apiFirst boot runs Prisma migrations against an empty database. Visit WEB_PUBLIC_URL and register the first user — they become the workspace owner. Add the Anthropic key from Settings if you left it blank in .env.
Private images
The
ghcr.io/withvibe/* images must be public, or run docker login ghcr.io with a token that has read:packages before docker compose pull.Reverse proxy notes
Put your TLS-terminating proxy in front of the web service (port 3000). The api is reached same-origin through the web app — you do not need to expose port 4000 publicly.
- The chat stream uses SSE. Set proxy
read_timeout≥ 1 hour. - Disable buffering on
/api/chat/*routes (X-Accel-Buffering: no). - WebSockets power the terminal view — pass
Upgrade/Connectionheaders.