docs/self-hosting/deploy-docker.mdx
This guide covers deploying Vibe Kanban Cloud on any Linux server using Docker Compose. This approach works with any cloud provider (AWS, DigitalOcean, Hetzner, etc.) or on-premises server.
SSH into your server and install Docker if not already installed:
# Install Docker (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group changes to take effect
Clone the Vibe Kanban repository:
git clone https://github.com/BloopAI/vibe-kanban.git
cd vibe-kanban
Update your OAuth application callback URLs to use your domain:
<Tabs> <Tab title="GitHub"> - **Authorization callback URL**: `https://your-domain.com/v1/oauth/github/callback` </Tab> <Tab title="Google"> - **Authorized redirect URI**: `https://your-domain.com/v1/oauth/google/callback` </Tab> </Tabs>Generate a secure JWT secret:
openssl rand -base64 48
Create .env.remote in the repository root:
# Required secrets
VIBEKANBAN_REMOTE_JWT_SECRET=<your_generated_jwt_secret>
ELECTRIC_ROLE_PASSWORD=<secure_password_for_electric>
DB_PASSWORD=<secure_database_password>
# Your domain
DOMAIN=your-domain.com
# Relay API base URL (required if you enable relay/tunnel)
VITE_RELAY_API_BASE_URL=https://relay.your-domain.com
# OAuth — configure at least one provider. Leave the other empty or remove it.
GITHUB_OAUTH_CLIENT_ID=your_github_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_github_client_secret
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
# Email (optional — leave empty to disable invitation emails)
LOOPS_EMAIL_API_KEY=
Create docker-compose.prod.yml in the crates/remote directory:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
DOMAIN: ${DOMAIN}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- remote-server
remote-db:
image: postgres:16-alpine
command: ["postgres", "-c", "wal_level=logical"]
restart: unless-stopped
environment:
POSTGRES_DB: remote
POSTGRES_USER: remote
POSTGRES_PASSWORD: ${DB_PASSWORD:-remote}
volumes:
- remote-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remote -d remote"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
electric:
image: electricsql/electric:1.3.3
working_dir: /app
restart: unless-stopped
environment:
DATABASE_URL: postgresql://electric_sync:${ELECTRIC_ROLE_PASSWORD}@remote-db:5432/remote?sslmode=disable
PG_PROXY_PORT: 65432
LOGICAL_PUBLISHER_HOST: electric
AUTH_MODE: insecure
ELECTRIC_INSECURE: true
ELECTRIC_MANUAL_TABLE_PUBLISHING: true
ELECTRIC_USAGE_REPORTING: false
ELECTRIC_FEATURE_FLAGS: allow_subqueries,tagged_subqueries
volumes:
- electric-data:/app/persistent
depends_on:
remote-db:
condition: service_healthy
remote-server:
condition: service_healthy
remote-server:
build:
context: ../..
dockerfile: crates/remote/Dockerfile
args:
VITE_RELAY_API_BASE_URL: ${VITE_RELAY_API_BASE_URL:-}
restart: unless-stopped
depends_on:
remote-db:
condition: service_healthy
environment:
RUST_LOG: info,remote=info
SERVER_DATABASE_URL: postgres://remote:${DB_PASSWORD:-remote}@remote-db:5432/remote
SERVER_LISTEN_ADDR: 0.0.0.0:8081
ELECTRIC_URL: http://electric:3000
SERVER_PUBLIC_BASE_URL: https://${DOMAIN}
GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID:-}
GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET:-}
GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-}
GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-}
VIBEKANBAN_REMOTE_JWT_SECRET: ${VIBEKANBAN_REMOTE_JWT_SECRET}
ELECTRIC_ROLE_PASSWORD: ${ELECTRIC_ROLE_PASSWORD}
LOOPS_EMAIL_API_KEY: ${LOOPS_EMAIL_API_KEY:-}
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8081/v1/health"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
volumes:
remote-db-data:
electric-data:
caddy_data:
caddy_config:
Create a Caddyfile in the crates/remote directory for automatic HTTPS (core app/API):
{$DOMAIN} {
reverse_proxy remote-server:8081
}
cd crates/remote
# Build and start all services
docker compose --env-file ../../.env.remote -f docker-compose.prod.yml up -d --build
# View logs
docker compose -f docker-compose.prod.yml logs -f
https://your-domain.com in your browserRelay/tunnel support requires:
relay-server servicerelay.your-domain.com and *.relay.your-domain.com*.relay.your-domain.com (or equivalent managed TLS at your edge)VITE_RELAY_API_BASE_URL set to your public relay API base URL before building remote-server relay-server:
build:
context: ../..
dockerfile: crates/relay-tunnel/Dockerfile
restart: unless-stopped
depends_on:
remote-db:
condition: service_healthy
environment:
RUST_LOG: info
SERVER_DATABASE_URL: postgres://remote:${DB_PASSWORD:-remote}@remote-db:5432/remote
RELAY_LISTEN_ADDR: 0.0.0.0:8082
VIBEKANBAN_REMOTE_JWT_SECRET: ${VIBEKANBAN_REMOTE_JWT_SECRET}
Your reverse proxy must route:
relay.your-domain.com -> relay-server:8082*.relay.your-domain.com -> relay-server:8082To update to a new version:
cd vibe-kanban
git pull origin main
cd crates/remote
docker compose --env-file ../../.env.remote -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml exec remote-db \
pg_dump -U remote remote > backup_$(date +%Y%m%d).sql
docker compose -f docker-compose.prod.yml exec -T remote-db \
psql -U remote remote < backup_20240101.sql
# All services
docker compose -f docker-compose.prod.yml logs -f
# Specific service
docker compose -f docker-compose.prod.yml logs -f remote-server
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs remote-db
docker compose -f docker-compose.prod.yml restart remote-server
ELECTRIC_ROLE_PASSWORD matches in both your .env.remote and the Electric service configdocker compose -f docker-compose.prod.yml restart electric
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile