Skip to main content
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.

Prerequisites

  • A Linux server with:
    • Docker and Docker Compose v2.0+ installed
    • 2GB RAM minimum (4GB recommended)
    • 10GB disk space
    • A domain name pointing to your server
  • SSL certificate (we’ll use Caddy for automatic HTTPS)
  • OAuth credentials from GitHub or Google

Step 1: Prepare Your 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

Step 2: Configure OAuth

Update your OAuth application callback URLs to use your domain:
  • Authorization callback URL: https://your-domain.com/v1/oauth/github/callback

Step 3: Create Environment File

Generate a secure JWT secret:
openssl rand -base64 48
Create .env.remote in the repository root:
.env.remote
# 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=

Step 4: Create Production Docker Compose

Create docker-compose.prod.yml in the crates/remote directory:
docker-compose.prod.yml
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:

Step 5: Create Caddyfile

Create a Caddyfile in the crates/remote directory for automatic HTTPS (core app/API):
Caddyfile
{$DOMAIN} {
    reverse_proxy remote-server:8081
}
This base deployment serves the main Cloud app/API only. Relay/tunnel support is optional and requires additional relay routing plus wildcard DNS/TLS for your relay domain.

Step 6: Deploy

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
The first build takes 10-15 minutes. Subsequent deployments are faster as Docker caches the build layers.

Step 7: Verify Deployment

  1. Open https://your-domain.com in your browser
  2. You should see the Vibe Kanban Cloud login page
  3. Sign in with your OAuth provider
  4. Create your first organisation and project

Optional: Enable Relay/Tunnel in Production

Relay/tunnel support requires:
  1. A running relay-server service
  2. Reverse proxy routing for both relay.your-domain.com and *.relay.your-domain.com
  3. A wildcard certificate for *.relay.your-domain.com (or equivalent managed TLS at your edge)
  4. VITE_RELAY_API_BASE_URL set to your public relay API base URL before building remote-server

Add relay-server to docker compose

docker-compose.prod.yml
  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}

Add relay proxy routes

Your reverse proxy must route:
  • relay.your-domain.com -> relay-server:8082
  • *.relay.your-domain.com -> relay-server:8082
Standard ACME HTTP challenge does not issue wildcard certificates. For wildcard relay hostnames, use a DNS-based ACME challenge or another edge provider that can terminate wildcard TLS certificates.

Updating

To 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

Backup and Restore

Backup Database

docker compose -f docker-compose.prod.yml exec remote-db \
  pg_dump -U remote remote > backup_$(date +%Y%m%d).sql

Restore Database

docker compose -f docker-compose.prod.yml exec -T remote-db \
  psql -U remote remote < backup_20240101.sql

Monitoring

View Logs

# All services
docker compose -f docker-compose.prod.yml logs -f

# Specific service
docker compose -f docker-compose.prod.yml logs -f remote-server

Check Service Health

docker compose -f docker-compose.prod.yml ps

Troubleshooting

Caddy automatically obtains SSL certificates from Let’s Encrypt. Ensure:
  • Your domain’s DNS is correctly pointing to your server
  • Ports 80 and 443 are open in your firewall
  • Your domain is correctly set in the environment
The server may start before the database is ready. Check:
docker compose -f docker-compose.prod.yml logs remote-db
docker compose -f docker-compose.prod.yml restart remote-server
ElectricSQL requires the electric_sync database user, which the Remote Server creates automatically on first startup. If ElectricSQL cannot connect:
  1. Check that the Remote Server started successfully and ran its migrations
  2. Verify ELECTRIC_ROLE_PASSWORD matches in both your .env.remote and the Electric service config
  3. Restart ElectricSQL after the Remote Server is healthy:
docker compose -f docker-compose.prod.yml restart electric
If the build fails with memory errors, you may need a server with more RAM or add swap:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile