Skip to content

Commit 5d64d2d

Browse files
committed
feat: Add self-hosted Betterbase deployment support
Implements full self-hosting capability with Docker Compose: - New @betterbase/server package with admin API routes - Database schema migrations for metadata (admin_users, projects, etc.) - Device auth flow for CLI (OAuth 2.0 device code) - CLI updates for self-hosted server URL support - Docker Compose stack: postgres, minio, server, dashboard, nginx - Environment configuration and documentation Includes: - Server package with auth, projects, users, metrics, storage, webhooks, functions, logs - Migration runner and database pool management - CLI credentials with server_url for self-hosted targeting - Device auth endpoints for bb login flow - Dockerfiles and nginx config for full stack deployment - SELF_HOSTED.md documentation
1 parent b381dd8 commit 5d64d2d

32 files changed

Lines changed: 3900 additions & 135 deletions

BetterBase_SelfHosted_Spec.md

Lines changed: 2288 additions & 0 deletions
Large diffs are not rendered by default.

SELF_HOSTED.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Self-Hosting Betterbase
2+
3+
## Prerequisites
4+
5+
- Docker and Docker Compose
6+
- Ports 80 (or your chosen `HTTP_PORT`) available
7+
8+
## Quick Start
9+
10+
**1. Copy the example env file:**
11+
```bash
12+
cp .env.self-hosted.example .env
13+
```
14+
15+
**2. Edit `.env` — at minimum set these two values:**
16+
```bash
17+
BETTERBASE_JWT_SECRET=your-random-string-here # min 32 chars
18+
BETTERBASE_ADMIN_EMAIL=you@example.com
19+
BETTERBASE_ADMIN_PASSWORD=yourpassword
20+
```
21+
Generate a secret: `openssl rand -base64 32`
22+
23+
**3. Start everything:**
24+
```bash
25+
docker compose -f docker-compose.self-hosted.yml up -d
26+
```
27+
28+
**4. Open the dashboard:**
29+
Navigate to `http://localhost` (or your configured `BETTERBASE_PUBLIC_URL`).
30+
31+
**5. Connect your CLI:**
32+
```bash
33+
bb login --url http://localhost
34+
```
35+
36+
---
37+
38+
## What Runs
39+
40+
| Service | Internal Port | Description |
41+
|---------|--------------|-------------|
42+
| nginx | 80 (public) | Reverse proxy — only public-facing port |
43+
| betterbase-server | 3001 (internal) | API server |
44+
| betterbase-dashboard | 80 (internal) | Dashboard UI |
45+
| postgres | 5432 (internal) | Betterbase metadata database |
46+
| minio | 9000 (internal) | S3-compatible object storage |
47+
48+
---
49+
50+
## CLI Usage Against Self-Hosted
51+
52+
After `bb login --url http://your-server`, all CLI commands automatically target your server.
53+
54+
```bash
55+
bb login --url http://localhost # authenticate
56+
bb init my-project # create a project (registered to your local instance)
57+
bb sync # sync local project to server
58+
```
59+
60+
---
61+
62+
## Production Checklist
63+
64+
- [ ] `BETTERBASE_JWT_SECRET` is a random 32+ character string
65+
- [ ] `POSTGRES_PASSWORD` changed from default
66+
- [ ] `STORAGE_ACCESS_KEY` and `STORAGE_SECRET_KEY` changed from defaults
67+
- [ ] `BETTERBASE_PUBLIC_URL` set to your actual domain
68+
- [ ] SSL/TLS termination configured (add HTTPS to the nginx config or use a load balancer)
69+
- [ ] Remove `BETTERBASE_ADMIN_EMAIL` / `BETTERBASE_ADMIN_PASSWORD` from `.env` after first start (or keep — seeding is idempotent)
70+
71+
---
72+
73+
## Troubleshooting
74+
75+
**Server won't start:**
76+
Check that `BETTERBASE_JWT_SECRET` is set (minimum 32 characters). Run:
77+
```bash
78+
docker compose -f docker-compose.self-hosted.yml logs betterbase-server
79+
```
80+
81+
**Can't log in with CLI:**
82+
Ensure `BETTERBASE_PUBLIC_URL` in your `.env` matches the URL you pass to `bb login --url`.
83+
84+
**Storage not working:**
85+
The `minio-init` container initialises the default bucket on first start. Check its logs:
86+
```bash
87+
docker compose -f docker-compose.self-hosted.yml logs minio-init
88+
```

apps/dashboard/Dockerfile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Dashboard Dockerfile placeholder
2+
# This is a placeholder. The actual dashboard needs to be created first.
3+
# If the dashboard uses Bun instead of npm, replace node:20-alpine with oven/bun:1.2-alpine
4+
# and replace npm install with bun install and npm run build with bun run build
5+
6+
FROM node:20-alpine AS builder
7+
8+
WORKDIR /app
9+
10+
COPY apps/dashboard/package.json apps/dashboard/package-lock.json* ./
11+
RUN npm install --frozen-lockfile
12+
13+
COPY apps/dashboard ./
14+
15+
# Inject API URL at build time
16+
ARG VITE_API_URL=http://localhost
17+
ENV VITE_API_URL=$VITE_API_URL
18+
19+
RUN npm run build
20+
21+
# --- Runtime: serve static files with nginx ---
22+
FROM nginx:alpine
23+
24+
COPY --from=builder /app/dist /usr/share/nginx/html
25+
26+
# SPA routing: serve index.html for all unknown paths
27+
RUN echo 'server { \
28+
listen 80; \
29+
root /usr/share/nginx/html; \
30+
index index.html; \
31+
location / { try_files $uri $uri/ /index.html; } \
32+
}' > /etc/nginx/conf.d/default.conf
33+
34+
EXPOSE 80

docker-compose.self-hosted.yml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
version: "3.9"
2+
3+
services:
4+
# ─── Postgres ──────────────────────────────────────────────────────────────
5+
postgres:
6+
image: postgres:16-alpine
7+
container_name: betterbase-postgres
8+
restart: unless-stopped
9+
environment:
10+
POSTGRES_USER: betterbase
11+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-betterbase}
12+
POSTGRES_DB: betterbase
13+
volumes:
14+
- postgres_data:/var/lib/postgresql/data
15+
healthcheck:
16+
test: ["CMD-SHELL", "pg_isready -U betterbase"]
17+
interval: 10s
18+
timeout: 5s
19+
retries: 5
20+
networks:
21+
- betterbase-internal
22+
23+
# ─── MinIO (S3-compatible storage) ─────────────────────────────────────────
24+
minio:
25+
image: minio/minio:latest
26+
container_name: betterbase-minio
27+
restart: unless-stopped
28+
command: server /data --console-address ":9001"
29+
environment:
30+
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:-minioadmin}
31+
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:-minioadmin}
32+
volumes:
33+
- minio_data:/data
34+
healthcheck:
35+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
36+
interval: 10s
37+
timeout: 5s
38+
retries: 5
39+
networks:
40+
- betterbase-internal
41+
42+
# ─── MinIO bucket init (runs once, exits) ──────────────────────────────────
43+
minio-init:
44+
image: minio/mc:latest
45+
container_name: betterbase-minio-init
46+
depends_on:
47+
minio:
48+
condition: service_healthy
49+
entrypoint: >
50+
/bin/sh -c "
51+
mc alias set local http://minio:9000 ${STORAGE_ACCESS_KEY:-minioadmin} ${STORAGE_SECRET_KEY:-minioadmin};
52+
mc mb --ignore-existing local/betterbase;
53+
mc anonymous set public local/betterbase;
54+
echo 'MinIO bucket initialized.';
55+
"
56+
networks:
57+
- betterbase-internal
58+
59+
# ─── Betterbase Server ─────────────────────────────────────────────────────
60+
betterbase-server:
61+
build:
62+
context: .
63+
dockerfile: packages/server/Dockerfile
64+
container_name: betterbase-server
65+
restart: unless-stopped
66+
depends_on:
67+
postgres:
68+
condition: service_healthy
69+
minio:
70+
condition: service_healthy
71+
environment:
72+
DATABASE_URL: postgresql://betterbase:${POSTGRES_PASSWORD:-betterbase}@postgres:5432/betterbase
73+
BETTERBASE_JWT_SECRET: ${BETTERBASE_JWT_SECRET:?JWT secret required - set BETTERBASE_JWT_SECRET in .env}
74+
BETTERBASE_ADMIN_EMAIL: ${BETTERBASE_ADMIN_EMAIL:-}
75+
BETTERBASE_ADMIN_PASSWORD: ${BETTERBASE_ADMIN_PASSWORD:-}
76+
BETTERBASE_PUBLIC_URL: ${BETTERBASE_PUBLIC_URL:-http://localhost}
77+
STORAGE_ENDPOINT: http://minio:9000
78+
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-minioadmin}
79+
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY:-minioadmin}
80+
PORT: "3001"
81+
NODE_ENV: production
82+
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost}
83+
networks:
84+
- betterbase-internal
85+
healthcheck:
86+
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"]
87+
interval: 10s
88+
timeout: 5s
89+
retries: 5
90+
91+
# ─── Dashboard ─────────────────────────────────────────────────────────────
92+
betterbase-dashboard:
93+
build:
94+
context: .
95+
dockerfile: apps/dashboard/Dockerfile # Dashboard Dockerfile — see SH-25
96+
container_name: betterbase-dashboard
97+
restart: unless-stopped
98+
depends_on:
99+
betterbase-server:
100+
condition: service_healthy
101+
environment:
102+
VITE_API_URL: ${BETTERBASE_PUBLIC_URL:-http://localhost}
103+
networks:
104+
- betterbase-internal
105+
106+
# ─── Nginx Reverse Proxy ───────────────────────────────────────────────────
107+
nginx:
108+
image: nginx:alpine
109+
container_name: betterbase-nginx
110+
restart: unless-stopped
111+
depends_on:
112+
- betterbase-server
113+
- betterbase-dashboard
114+
ports:
115+
- "${HTTP_PORT:-80}:80"
116+
volumes:
117+
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
118+
networks:
119+
- betterbase-internal
120+
healthcheck:
121+
test: ["CMD", "nginx", "-t"]
122+
interval: 30s
123+
timeout: 10s
124+
retries: 3
125+
126+
volumes:
127+
postgres_data:
128+
minio_data:
129+
130+
networks:
131+
betterbase-internal:
132+
driver: bridge

docker/nginx/nginx.conf

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
events {
2+
worker_connections 1024;
3+
}
4+
5+
http {
6+
upstream betterbase_server {
7+
server betterbase-server:3001;
8+
}
9+
10+
upstream betterbase_dashboard {
11+
server betterbase-dashboard:80;
12+
}
13+
14+
upstream minio {
15+
server minio:9000;
16+
}
17+
18+
server {
19+
listen 80;
20+
server_name _;
21+
22+
# API + admin + device auth
23+
location /admin/ {
24+
proxy_pass http://betterbase_server;
25+
proxy_set_header Host $host;
26+
proxy_set_header X-Real-IP $remote_addr;
27+
proxy_read_timeout 60s;
28+
}
29+
30+
location /device/ {
31+
proxy_pass http://betterbase_server;
32+
proxy_set_header Host $host;
33+
proxy_set_header X-Real-IP $remote_addr;
34+
}
35+
36+
location /health {
37+
proxy_pass http://betterbase_server;
38+
}
39+
40+
# Storage (MinIO)
41+
location /storage/ {
42+
rewrite ^/storage/(.*) /$1 break;
43+
proxy_pass http://minio;
44+
proxy_set_header Host $host;
45+
proxy_set_header X-Real-IP $remote_addr;
46+
client_max_body_size 100m;
47+
}
48+
49+
# Dashboard (catch-all)
50+
location / {
51+
proxy_pass http://betterbase_dashboard;
52+
proxy_set_header Host $host;
53+
proxy_set_header X-Real-IP $remote_addr;
54+
# SPA fallback
55+
proxy_intercept_errors on;
56+
error_page 404 = @dashboard_fallback;
57+
}
58+
59+
location @dashboard_fallback {
60+
proxy_pass http://betterbase_dashboard;
61+
proxy_set_header Host $host;
62+
}
63+
64+
# WebSocket support for realtime
65+
location /realtime/ {
66+
proxy_pass http://betterbase_server;
67+
proxy_http_version 1.1;
68+
proxy_set_header Upgrade $http_upgrade;
69+
proxy_set_header Connection "upgrade";
70+
proxy_set_header Host $host;
71+
proxy_read_timeout 3600s;
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)