Skip to content

Commit f3843cd

Browse files
Merge pull request #32 from vincentgrobler/feature/ticket-5.4-self-hosting
feat(ticket-5.4): self-hosting with Docker Compose
2 parents f1cbca6 + 2f0dfc9 commit f3843cd

8 files changed

Lines changed: 406 additions & 10 deletions

File tree

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
.git
3+
.github
4+
.env
5+
.env.local
6+
*.log
7+
dist
8+
crewform-docs
9+
.DS_Store

.env.example

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
# CrewForm Environment Variables
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# CrewForm — Environment Variables
23
# Copy to .env and fill in your values
4+
# ─────────────────────────────────────────────────────────────────────────────
35

4-
# Supabase — required
6+
# ── PostgreSQL (Docker Compose) ───────────────────────────────────────────
7+
POSTGRES_DB=crewform
8+
POSTGRES_USER=crewform
9+
POSTGRES_PASSWORD= # REQUIRED — choose a strong password
10+
POSTGRES_PORT=5432
11+
12+
# ── Supabase ──────────────────────────────────────────────────────────────
13+
# For hosted Supabase: use your project URL and keys
14+
# For self-hosting without Supabase: leave blank (direct Postgres mode)
515
VITE_SUPABASE_URL=
616
VITE_SUPABASE_ANON_KEY=
17+
SUPABASE_SERVICE_ROLE_KEY=
18+
19+
# ── App URL ───────────────────────────────────────────────────────────────
20+
VITE_APP_URL=http://localhost:3000
21+
FRONTEND_PORT=3000
22+
23+
# ── Encryption ────────────────────────────────────────────────────────────
24+
# 32-byte hex string for AES-256-GCM API key encryption
25+
ENCRYPTION_KEY=
726

8-
# App URL
9-
VITE_APP_URL=http://localhost:5173
27+
# ── AI Provider Keys (optional — BYOK pattern uses workspace API keys) ──
28+
OPENAI_API_KEY=
29+
ANTHROPIC_API_KEY=
30+
GOOGLE_GENERATIVE_AI_API_KEY=

Dockerfile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# CrewForm Frontend — Multi-stage Dockerfile
3+
# Stage 1: Build Vite app
4+
# Stage 2: Serve static files via nginx
5+
# ─────────────────────────────────────────────────────────────────────────────
6+
7+
# ── Build stage ───────────────────────────────────────────────────────────
8+
FROM node:20-alpine AS builder
9+
10+
WORKDIR /app
11+
12+
# Install dependencies
13+
COPY package*.json ./
14+
RUN npm ci
15+
16+
# Accept build-time env vars for Vite
17+
ARG VITE_SUPABASE_URL
18+
ARG VITE_SUPABASE_ANON_KEY
19+
ARG VITE_APP_URL
20+
21+
# Copy source and build
22+
COPY . .
23+
RUN npm run build
24+
25+
# ── Serve stage ───────────────────────────────────────────────────────────
26+
FROM nginx:1.27-alpine
27+
28+
# Copy custom nginx config
29+
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
30+
31+
# Copy built assets from builder
32+
COPY --from=builder /app/dist /usr/share/nginx/html
33+
34+
EXPOSE 80
35+
36+
CMD ["nginx", "-g", "daemon off;"]

docker-compose.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# CrewForm — Docker Compose (Self-Hosting)
3+
# ─────────────────────────────────────────────────────────────────────────────
4+
#
5+
# Usage:
6+
# cp .env.example .env # edit with your values
7+
# docker compose up -d
8+
#
9+
# Services:
10+
# postgres — PostgreSQL 15 with persistent volume
11+
# migrate — Runs all SQL migrations, then exits
12+
# frontend — Vite build served via nginx (port 3000)
13+
# task-runner — Node.js polling service
14+
# ─────────────────────────────────────────────────────────────────────────────
15+
16+
services:
17+
# ── PostgreSQL ──────────────────────────────────────────────────────────
18+
postgres:
19+
image: postgres:15-alpine
20+
restart: unless-stopped
21+
environment:
22+
POSTGRES_DB: ${POSTGRES_DB:-crewform}
23+
POSTGRES_USER: ${POSTGRES_USER:-crewform}
24+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
25+
ports:
26+
- "${POSTGRES_PORT:-5432}:5432"
27+
volumes:
28+
- pgdata:/var/lib/postgresql/data
29+
healthcheck:
30+
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-crewform}" ]
31+
interval: 5s
32+
timeout: 5s
33+
retries: 10
34+
35+
# ── Migrations ──────────────────────────────────────────────────────────
36+
migrate:
37+
image: postgres:15-alpine
38+
depends_on:
39+
postgres:
40+
condition: service_healthy
41+
environment:
42+
PGHOST: postgres
43+
PGPORT: "5432"
44+
PGDATABASE: ${POSTGRES_DB:-crewform}
45+
PGUSER: ${POSTGRES_USER:-crewform}
46+
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
47+
volumes:
48+
- ./supabase/migrations:/migrations:ro
49+
- ./docker/migrate.sh:/migrate.sh:ro
50+
entrypoint: [ "sh", "/migrate.sh" ]
51+
52+
# ── Frontend ────────────────────────────────────────────────────────────
53+
frontend:
54+
build:
55+
context: .
56+
dockerfile: Dockerfile
57+
args:
58+
VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:-http://localhost:8000}
59+
VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY:-}
60+
VITE_APP_URL: ${VITE_APP_URL:-http://localhost:3000}
61+
restart: unless-stopped
62+
ports:
63+
- "${FRONTEND_PORT:-3000}:80"
64+
depends_on:
65+
migrate:
66+
condition: service_completed_successfully
67+
68+
# ── Task Runner ─────────────────────────────────────────────────────────
69+
task-runner:
70+
build:
71+
context: ./task-runner
72+
dockerfile: Dockerfile
73+
restart: unless-stopped
74+
environment:
75+
VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:-http://localhost:8000}
76+
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:-}
77+
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-}
78+
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
79+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
80+
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
81+
depends_on:
82+
migrate:
83+
condition: service_completed_successfully
84+
85+
volumes:
86+
pgdata:

docker/migrate.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
# ─────────────────────────────────────────────────────────────────────────────
3+
# CrewForm — Auto-migration script
4+
# Runs all SQL migrations in sorted order against PostgreSQL.
5+
# Environment: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD
6+
# ─────────────────────────────────────────────────────────────────────────────
7+
8+
set -e
9+
10+
echo "═══════════════════════════════════════════════════"
11+
echo " CrewForm — Running database migrations"
12+
echo "═══════════════════════════════════════════════════"
13+
14+
# Create a tracking table to avoid re-running migrations
15+
psql -c "
16+
CREATE TABLE IF NOT EXISTS _migrations (
17+
name TEXT PRIMARY KEY,
18+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
19+
);
20+
" 2>/dev/null
21+
22+
MIGRATION_DIR="/migrations"
23+
APPLIED=0
24+
SKIPPED=0
25+
26+
for f in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
27+
filename=$(basename "$f")
28+
29+
# Check if already applied
30+
already=$(psql -tAc "SELECT 1 FROM _migrations WHERE name = '$filename'" 2>/dev/null || echo "")
31+
if [ "$already" = "1" ]; then
32+
SKIPPED=$((SKIPPED + 1))
33+
continue
34+
fi
35+
36+
echo " ▸ Applying: $filename"
37+
psql -f "$f" -v ON_ERROR_STOP=1
38+
39+
# Record migration
40+
psql -c "INSERT INTO _migrations (name) VALUES ('$filename')" 2>/dev/null
41+
APPLIED=$((APPLIED + 1))
42+
done
43+
44+
echo "═══════════════════════════════════════════════════"
45+
echo " Done! Applied: $APPLIED | Skipped: $SKIPPED"
46+
echo "═══════════════════════════════════════════════════"

docker/nginx.conf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# CrewForm — nginx config for SPA routing
3+
# ─────────────────────────────────────────────────────────────────────────────
4+
5+
server {
6+
listen 80;
7+
server_name _;
8+
root /usr/share/nginx/html;
9+
index index.html;
10+
11+
# Gzip compression
12+
gzip on;
13+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
14+
gzip_min_length 256;
15+
16+
# Cache static assets aggressively
17+
location /assets/ {
18+
expires 1y;
19+
add_header Cache-Control "public, immutable";
20+
}
21+
22+
# SPA fallback — serve index.html for all non-file routes
23+
location / {
24+
try_files $uri $uri/ /index.html;
25+
}
26+
27+
# Security headers
28+
add_header X-Frame-Options "SAMEORIGIN" always;
29+
add_header X-Content-Type-Options "nosniff" always;
30+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
31+
}

0 commit comments

Comments
 (0)