This guide covers deploying Relipa 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 Relipa Kanban repository:
git clone <your-repo-url>
cd relipa-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=

# Loops transactional email template IDs (optional — defaults are the upstream templates).
# Override these with your own Loops account template IDs if using a custom Loops account.
LOOPS_INVITE_TEMPLATE_ID=cmhvy2wgs3s13z70i1pxakij9
LOOPS_REVIEW_READY_TEMPLATE_ID=cmj47k5ge16990iylued9by17
LOOPS_REVIEW_FAILED_TEMPLATE_ID=cmj49ougk1c8s0iznavijdqpo

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:-}
      LOOPS_INVITE_TEMPLATE_ID: ${LOOPS_INVITE_TEMPLATE_ID:-cmhvy2wgs3s13z70i1pxakij9}
      LOOPS_REVIEW_READY_TEMPLATE_ID: ${LOOPS_REVIEW_READY_TEMPLATE_ID:-cmj47k5ge16990iylued9by17}
      LOOPS_REVIEW_FAILED_TEMPLATE_ID: ${LOOPS_REVIEW_FAILED_TEMPLATE_ID:-cmj49ougk1c8s0iznavijdqpo}
    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 Relipa 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