withvibe
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.yml

Postgres 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/repos

docker-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: withvibe
The 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 api

First 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/Connection headers.