docs/self-hosting/local-development.mdx
This guide walks you through setting up a local Vibe Kanban Cloud instance for development and testing.
Before you begin, ensure you have:
git clone https://github.com/BloopAI/vibe-kanban.git
cd vibe-kanban
You need at least one OAuth provider. Choose GitHub, Google, or both.
<Tabs> <Tab title="GitHub OAuth">http://localhost:3000http://localhost:3000/v1/oauth/github/callbackhttp://localhost:3000/v1/oauth/google/callbackCreate a .env.remote file in crates/remote/:
# Generate a secure JWT secret
openssl rand -base64 48
Copy the output and create your .env.remote:
# Required - JWT secret for authentication
VIBEKANBAN_REMOTE_JWT_SECRET=<paste_your_generated_secret_here>
# Optional - Password for ElectricSQL database role (electric_sync user)
ELECTRIC_ROLE_PASSWORD=
# 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 (leave empty if not using Google login)
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
# Relay (required for relay/tunnel features)
# For plain HTTP local dev:
VITE_RELAY_API_BASE_URL=http://localhost:8082
# Email invitations (optional — leave empty to disable)
LOOPS_EMAIL_API_KEY=
For production or self-hosting on a server, add PUBLIC_BASE_URL (your public URL, e.g. https://kanban.example.com) and REMOTE_SERVER_PORTS=0.0.0.0:3000:8081 so the server is reachable from other hosts. Defaults keep local dev unchanged.
From the crates/remote directory, start all services:
cd crates/remote
docker compose --env-file .env.remote -f docker-compose.yml up --build
Or from the repo root:
pnpm run remote:dev
This starts:
remote-server-1 | INFO remote: Server listening on 0.0.0.0:8081
Open http://localhost:3000 in your browser. You should see the Vibe Kanban Cloud login page.
Sign in with your configured OAuth provider (GitHub or Google).
To use the desktop client with your local server:
# In a new terminal, from the repository root
export VK_SHARED_API_BASE=http://localhost:3000
pnpm install
pnpm run dev
The desktop client will now connect to your local Cloud instance instead of the hosted version.
To test relay/tunnel mode end-to-end, add:
export VK_SHARED_API_BASE=https://localhost:3001
export VK_SHARED_RELAY_API_BASE=https://relay.localhost:3001
export VK_TUNNEL=1
This mode requires local HTTPS + Caddy routing (next step).
Create a Caddy config that routes:
localhost:3001 -> remote server (127.0.0.1:3000)relay.localhost:3001 and *.relay.localhost:3001 -> relay server (127.0.0.1:8082)caddy run --config - --adapter caddyfile <<'EOF'
localhost:3001, relay.localhost:3001, *.relay.localhost:3001 {
tls internal
@relay host relay.localhost *.relay.localhost
handle @relay {
reverse_proxy 127.0.0.1:8082
}
@app expression `{http.request.host} == "localhost:3001" || {http.request.host} == "localhost"`
handle @app {
reverse_proxy 127.0.0.1:3000
}
respond "not found" 404
}
EOF
If you use this HTTPS setup, update OAuth callback URLs to:
https://localhost:3001/v1/oauth/github/callbackhttps://localhost:3001/v1/oauth/google/callbackTo stop all services:
docker compose down
To stop and remove all data (fresh start):
docker compose down -v
# Check database status
docker compose ps
# View database logs
docker compose logs remote-db
ELECTRIC_ROLE_PASSWORD is set in your .env.remotedocker compose --env-file .env.remote -f docker-compose.yml restart electric
ports:
- "127.0.0.1:3001:8081" # Change 3001 to your preferred port
Update your OAuth callback URLs accordingly. </Accordion>
<Accordion title="Relay health endpoint returns HTML instead of JSON"> **Problem:** `curl -sk https://relay.localhost:3001/health` returns HTML, and relay/tunnel fails.Cause: Caddy routed relay hostnames to the remote app (:3000) instead of relay server (:8082).
Solution:
relay.localhost and *.relay.localhostcurl -sk https://relay.localhost:3001/health returns {"status":"ok"}curl -sk https://localhost:3001/v1/health returns remote server health JSON
</Accordion>
Once you have local development working, you can: