Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.git
.github
.env
.env.local
*.log
dist
crewform-docs
.DS_Store
29 changes: 25 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
# CrewForm Environment Variables
# ─────────────────────────────────────────────────────────────────────────────
# CrewForm — Environment Variables
# Copy to .env and fill in your values
# ─────────────────────────────────────────────────────────────────────────────

# Supabase — required
# ── PostgreSQL (Docker Compose) ───────────────────────────────────────────
POSTGRES_DB=crewform
POSTGRES_USER=crewform
POSTGRES_PASSWORD= # REQUIRED — choose a strong password
POSTGRES_PORT=5432

# ── Supabase ──────────────────────────────────────────────────────────────
# For hosted Supabase: use your project URL and keys
# For self-hosting without Supabase: leave blank (direct Postgres mode)
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

# ── App URL ───────────────────────────────────────────────────────────────
VITE_APP_URL=http://localhost:3000
FRONTEND_PORT=3000

# ── Encryption ────────────────────────────────────────────────────────────
# 32-byte hex string for AES-256-GCM API key encryption
ENCRYPTION_KEY=

# App URL
VITE_APP_URL=http://localhost:5173
# ── AI Provider Keys (optional — BYOK pattern uses workspace API keys) ──
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
36 changes: 36 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ─────────────────────────────────────────────────────────────────────────────
# CrewForm Frontend — Multi-stage Dockerfile
# Stage 1: Build Vite app
# Stage 2: Serve static files via nginx
# ─────────────────────────────────────────────────────────────────────────────

# ── Build stage ───────────────────────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Accept build-time env vars for Vite
ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY
ARG VITE_APP_URL

# Copy source and build
COPY . .
RUN npm run build

# ── Serve stage ───────────────────────────────────────────────────────────
FROM nginx:1.27-alpine

# Copy custom nginx config
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf

# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
86 changes: 86 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ─────────────────────────────────────────────────────────────────────────────
# CrewForm — Docker Compose (Self-Hosting)
# ─────────────────────────────────────────────────────────────────────────────
#
# Usage:
# cp .env.example .env # edit with your values
# docker compose up -d
#
# Services:
# postgres — PostgreSQL 15 with persistent volume
# migrate — Runs all SQL migrations, then exits
# frontend — Vite build served via nginx (port 3000)
# task-runner — Node.js polling service
# ─────────────────────────────────────────────────────────────────────────────

services:
# ── PostgreSQL ──────────────────────────────────────────────────────────
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-crewform}
POSTGRES_USER: ${POSTGRES_USER:-crewform}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-crewform}" ]
interval: 5s
timeout: 5s
retries: 10

# ── Migrations ──────────────────────────────────────────────────────────
migrate:
image: postgres:15-alpine
depends_on:
postgres:
condition: service_healthy
environment:
PGHOST: postgres
PGPORT: "5432"
PGDATABASE: ${POSTGRES_DB:-crewform}
PGUSER: ${POSTGRES_USER:-crewform}
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
volumes:
- ./supabase/migrations:/migrations:ro
- ./docker/migrate.sh:/migrate.sh:ro
entrypoint: [ "sh", "/migrate.sh" ]

# ── Frontend ────────────────────────────────────────────────────────────
frontend:
build:
context: .
dockerfile: Dockerfile
args:
VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:-http://localhost:8000}
VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY:-}
VITE_APP_URL: ${VITE_APP_URL:-http://localhost:3000}
restart: unless-stopped
ports:
- "${FRONTEND_PORT:-3000}:80"
depends_on:
migrate:
condition: service_completed_successfully

# ── Task Runner ─────────────────────────────────────────────────────────
task-runner:
build:
context: ./task-runner
dockerfile: Dockerfile
restart: unless-stopped
environment:
VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:-http://localhost:8000}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:-}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
depends_on:
migrate:
condition: service_completed_successfully

volumes:
pgdata:
46 changes: 46 additions & 0 deletions docker/migrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/sh
# ─────────────────────────────────────────────────────────────────────────────
# CrewForm — Auto-migration script
# Runs all SQL migrations in sorted order against PostgreSQL.
# Environment: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD
# ─────────────────────────────────────────────────────────────────────────────

set -e

echo "═══════════════════════════════════════════════════"
echo " CrewForm — Running database migrations"
echo "═══════════════════════════════════════════════════"

# Create a tracking table to avoid re-running migrations
psql -c "
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
" 2>/dev/null

MIGRATION_DIR="/migrations"
APPLIED=0
SKIPPED=0

for f in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
filename=$(basename "$f")

# Check if already applied
already=$(psql -tAc "SELECT 1 FROM _migrations WHERE name = '$filename'" 2>/dev/null || echo "")
if [ "$already" = "1" ]; then
SKIPPED=$((SKIPPED + 1))
continue
fi

echo " ▸ Applying: $filename"
psql -f "$f" -v ON_ERROR_STOP=1

# Record migration
psql -c "INSERT INTO _migrations (name) VALUES ('$filename')" 2>/dev/null
APPLIED=$((APPLIED + 1))
done

echo "═══════════════════════════════════════════════════"
echo " Done! Applied: $APPLIED | Skipped: $SKIPPED"
echo "═══════════════════════════════════════════════════"
31 changes: 31 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ─────────────────────────────────────────────────────────────────────────────
# CrewForm — nginx config for SPA routing
# ─────────────────────────────────────────────────────────────────────────────

server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 256;

# Cache static assets aggressively
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# SPA fallback — serve index.html for all non-file routes
location / {
try_files $uri $uri/ /index.html;
}

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Loading
Loading