From e94952ad20a85d8e2539354d2513d2c8657e115c Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 24 Nov 2025 14:21:42 +0100 Subject: [PATCH] Add Docker support with configuration files and environment setup --- .dockerignore | 21 ++++ .env.example | 98 +++++++++++++++++ DOCKER.md | 191 ++++++++++++++++++++++++++++++++ Dockerfile | 39 +++++++ docker-compose.selfhosted.yml | 201 ++++++++++++++++++++++++++++++++++ docker/kong.yml | 138 +++++++++++++++++++++++ nginx.conf | 29 +++++ 7 files changed, 717 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 docker-compose.selfhosted.yml create mode 100644 docker/kong.yml create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6514d48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +node_modules +dist +.git +.gitignore +.env +.env.* +!.env.example +*.md +!DOCKER.md +.vscode +.idea +*.log +npm-debug.log* +.DS_Store +Thumbs.db +coverage +.nyc_output +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a5d1991 --- /dev/null +++ b/.env.example @@ -0,0 +1,98 @@ +# ===================================================== +# DECKERR DOCKER CONFIGURATION +# ===================================================== +# Copy this file to .env and configure your settings +# +# Two deployment modes available: +# 1. External Supabase: Use docker-compose.yml (simpler) +# 2. Self-hosted Supabase: Use docker-compose.selfhosted.yml (full stack) +# ===================================================== + +# ===================================================== +# MODE 1: EXTERNAL SUPABASE (docker-compose.yml) +# ===================================================== +# Use this if you have: +# - A Supabase cloud account (supabase.com) +# - A separately self-hosted Supabase instance +# - Access to a paid hosted Supabase service + +# Your Supabase project URL +VITE_SUPABASE_URL=https://your-project.supabase.co + +# Your Supabase anonymous/public key +VITE_SUPABASE_ANON_KEY=your-anon-key-here + +# Port to run Deckerr on (default: 3000) +PORT=3000 + + +# ===================================================== +# MODE 2: SELF-HOSTED SUPABASE (docker-compose.selfhosted.yml) +# ===================================================== +# Use this to run everything locally, including Supabase + +# --- Site Configuration --- +# Your domain or IP address (used for redirects) +SITE_URL=http://localhost:3000 + +# External API URL (Kong gateway) +API_EXTERNAL_URL=http://localhost:8000 + +# --- Port Configuration --- +DECKERR_PORT=3000 +KONG_HTTP_PORT=8000 +KONG_HTTPS_PORT=8443 +POSTGRES_PORT=5432 + +# --- Security Keys --- +# IMPORTANT: Generate secure random values for production! +# You can use: openssl rand -base64 32 + +# PostgreSQL password +POSTGRES_PASSWORD=your-super-secret-postgres-password + +# JWT Secret (must be at least 32 characters) +# Generate with: openssl rand -base64 32 +JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters + +# JWT Expiry in seconds (default: 3600 = 1 hour) +JWT_EXPIRY=3600 + +# Supabase Anonymous Key +# Generate at: https://supabase.com/docs/guides/self-hosting#api-keys +# Or use: npx @supabase/cli@latest gen key --type anon --jwt-secret "YOUR_JWT_SECRET" +ANON_KEY=your-anon-key + +# Supabase Service Role Key (admin access) +# Generate at: https://supabase.com/docs/guides/self-hosting#api-keys +# Or use: npx @supabase/cli@latest gen key --type service_role --jwt-secret "YOUR_JWT_SECRET" +SERVICE_ROLE_KEY=your-service-role-key + +# --- Email Configuration (Optional) --- +# Required for email verification and password reset +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=your-email@example.com +SMTP_PASS=your-email-password +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_SENDER_NAME=Deckerr + +# Enable email auto-confirm (set to true to skip email verification) +ENABLE_EMAIL_AUTOCONFIRM=true + +# --- Feature Flags --- +# Disable new user signups +DISABLE_SIGNUP=false + +# Enable email signup +ENABLE_EMAIL_SIGNUP=true + +# Enable anonymous users +ENABLE_ANONYMOUS_USERS=false + +# --- Advanced --- +# Additional redirect URLs (comma-separated) +ADDITIONAL_REDIRECT_URLS= + +# PostgREST schemas +PGRST_DB_SCHEMAS=public,graphql_public diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..21a1340 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,191 @@ +# Deckerr Docker Deployment + +Self-host Deckerr with two deployment options: + +## Deployment Options + +| Option | Use Case | Complexity | +|--------|----------|------------| +| **External Supabase** | Use hosted Supabase (cloud or paid) | Simple | +| **Self-hosted Supabase** | Run everything locally | Advanced | + +--- + +## Option 1: External Supabase (Recommended) + +Use your own Supabase instance (cloud, paid hosted, or separately self-hosted). + +### Quick Start + +```bash +# 1. Copy environment template +cp .env.example .env + +# 2. Edit .env with your Supabase credentials +VITE_SUPABASE_URL=https://your-project.supabase.co +VITE_SUPABASE_ANON_KEY=your-anon-key +PORT=3000 + +# 3. Run database migrations on your Supabase +# Go to Supabase Dashboard > SQL Editor and run: +# Contents of supabase/migrations/20250131132458_black_frost.sql + +# 4. Start Deckerr +docker-compose up -d + +# 5. Access at http://localhost:3000 +``` + +### Using Hosted Supabase (Paid Service) + +Contact the Deckerr team for access credentials to use the hosted backend. + +--- + +## Option 2: Self-Hosted Supabase (Full Stack) + +Run Deckerr with a complete self-hosted Supabase stack. + +### Prerequisites + +- Docker & Docker Compose +- 2GB+ RAM +- Ports: 3000, 5432, 8000 + +### Quick Start + +```bash +# 1. Copy environment template +cp .env.example .env + +# 2. Generate secure keys +# JWT Secret (required) +openssl rand -base64 32 + +# Generate Supabase API keys +# Option A: Use online generator at https://supabase.com/docs/guides/self-hosting#api-keys +# Option B: Use Supabase CLI +npx @supabase/cli@latest gen key --type anon --jwt-secret "YOUR_JWT_SECRET" +npx @supabase/cli@latest gen key --type service_role --jwt-secret "YOUR_JWT_SECRET" + +# 3. Update .env with your generated values +POSTGRES_PASSWORD= +JWT_SECRET= +ANON_KEY= +SERVICE_ROLE_KEY= + +# 4. Start all services +docker-compose -f docker-compose.selfhosted.yml up -d + +# 5. Access Deckerr at http://localhost:3000 +# API available at http://localhost:8000 +``` + +### Generate Keys Script + +```bash +#!/bin/bash +JWT_SECRET=$(openssl rand -base64 32) +echo "JWT_SECRET=$JWT_SECRET" +echo "" +echo "Now generate API keys at:" +echo "https://supabase.com/docs/guides/self-hosting#api-keys" +echo "Use this JWT secret: $JWT_SECRET" +``` + +### Self-Hosted Services + +| Service | Port | Description | +|---------|------|-------------| +| Deckerr | 3000 | Frontend app | +| Kong | 8000 | API Gateway | +| PostgreSQL | 5432 | Database | +| Auth | 9999 | Authentication (internal) | +| REST | 3000 | PostgREST API (internal) | +| Realtime | 4000 | WebSocket (internal) | + +--- + +## Environment Variables + +### External Supabase Mode + +| Variable | Required | Description | +|----------|----------|-------------| +| `VITE_SUPABASE_URL` | Yes | Supabase project URL | +| `VITE_SUPABASE_ANON_KEY` | Yes | Supabase anonymous key | +| `PORT` | No | App port (default: 3000) | + +### Self-Hosted Mode + +| Variable | Required | Description | +|----------|----------|-------------| +| `POSTGRES_PASSWORD` | Yes | PostgreSQL password | +| `JWT_SECRET` | Yes | JWT signing secret (32+ chars) | +| `ANON_KEY` | Yes | Supabase anonymous key | +| `SERVICE_ROLE_KEY` | Yes | Supabase service role key | +| `SITE_URL` | No | Your domain (default: localhost) | +| `SMTP_*` | No | Email configuration | + +--- + +## Commands + +```bash +# Start (external Supabase) +docker-compose up -d + +# Start (self-hosted) +docker-compose -f docker-compose.selfhosted.yml up -d + +# Stop +docker-compose down + +# View logs +docker-compose logs -f + +# Rebuild after code changes +docker-compose build --no-cache +docker-compose up -d + +# Reset database (self-hosted only) +docker-compose -f docker-compose.selfhosted.yml down -v +docker-compose -f docker-compose.selfhosted.yml up -d +``` + +--- + +## Production Checklist + +- [ ] Use strong passwords (generate with `openssl rand -base64 32`) +- [ ] Configure HTTPS with reverse proxy (nginx, Traefik, Caddy) +- [ ] Set up email (SMTP) for password reset +- [ ] Configure firewall rules +- [ ] Set up backups for PostgreSQL volume +- [ ] Consider rate limiting at reverse proxy level + +--- + +## Troubleshooting + +### Container won't start +```bash +docker-compose logs +``` + +### Database connection issues +```bash +# Check if database is healthy +docker-compose exec db pg_isready -U postgres +``` + +### Reset everything +```bash +docker-compose down -v +docker-compose up -d --build +``` + +### Check service health +```bash +docker-compose ps +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c882f66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build arguments for Supabase configuration +ARG VITE_SUPABASE_URL +ARG VITE_SUPABASE_ANON_KEY + +# Set environment variables for build +ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL +ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.selfhosted.yml b/docker-compose.selfhosted.yml new file mode 100644 index 0000000..2a6d68c --- /dev/null +++ b/docker-compose.selfhosted.yml @@ -0,0 +1,201 @@ +version: '3.8' + +# Full self-hosted deployment with Supabase included +# This includes PostgreSQL, Auth, REST API, and the Deckerr frontend + +services: + # ============================================ + # DECKERR FRONTEND + # ============================================ + deckerr: + build: + context: . + dockerfile: Dockerfile + args: + - VITE_SUPABASE_URL=http://${SITE_URL:-localhost}:${KONG_HTTP_PORT:-8000} + - VITE_SUPABASE_ANON_KEY=${ANON_KEY} + container_name: deckerr + ports: + - "${DECKERR_PORT:-3000}:80" + restart: unless-stopped + depends_on: + kong: + condition: service_healthy + + # ============================================ + # SUPABASE SERVICES + # ============================================ + + # PostgreSQL Database + db: + image: supabase/postgres:15.1.1.78 + container_name: supabase-db + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + ports: + - "${POSTGRES_PORT:-5432}:5432" + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: 5432 + POSTGRES_PORT: 5432 + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: postgres + POSTGRES_DB: postgres + volumes: + - supabase-db-data:/var/lib/postgresql/data + - ./supabase/migrations:/docker-entrypoint-initdb.d/migrations + restart: unless-stopped + + # Supabase Kong API Gateway + kong: + image: kong:2.8.1 + container_name: supabase-kong + restart: unless-stopped + ports: + - "${KONG_HTTP_PORT:-8000}:8000/tcp" + - "${KONG_HTTPS_PORT:-8443}:8443/tcp" + depends_on: + db: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + volumes: + - ./docker/kong.yml:/home/kong/kong.yml:ro + healthcheck: + test: ["CMD", "kong", "health"] + interval: 10s + timeout: 10s + retries: 5 + + # Supabase Auth (GoTrue) + auth: + image: supabase/gotrue:v2.143.0 + container_name: supabase-auth + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"] + interval: 5s + timeout: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL:-http://localhost:8000} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres + + GOTRUE_SITE_URL: ${SITE_URL:-http://localhost:3000} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS:-} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP:-true} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS:-false} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM:-false} + + GOTRUE_SMTP_HOST: ${SMTP_HOST:-} + GOTRUE_SMTP_PORT: ${SMTP_PORT:-587} + GOTRUE_SMTP_USER: ${SMTP_USER:-} + GOTRUE_SMTP_PASS: ${SMTP_PASS:-} + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Deckerr} + GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify + + # Supabase REST API (PostgREST) + rest: + image: postgrest/postgrest:v12.0.1 + container_name: supabase-rest + depends_on: + db: + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS:-public,graphql_public} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY:-3600} + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/ready || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + + # Supabase Realtime + realtime: + image: supabase/realtime:v2.28.32 + container_name: supabase-realtime + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "-H", "Authorization: Bearer ${ANON_KEY}", "http://localhost:4000/api/tenants/realtime-dev/health"] + interval: 10s + timeout: 5s + retries: 3 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: db + DB_PORT: 5432 + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: postgres + DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: ${SECRET_KEY_BASE:-UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq} + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: true + REPLICATION_MODE: RLS + REPLICATION_POLL_INTERVAL: 100 + SECURE_CHANNELS: "true" + SLOT_NAME: supabase_realtime_rls + TEMPORARY_SLOT: "true" + + # Supabase Meta (for Studio - optional) + meta: + image: supabase/postgres-meta:v0.80.0 + container_name: supabase-meta + depends_on: + db: + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: db + PG_META_DB_PORT: 5432 + PG_META_DB_NAME: postgres + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + +volumes: + supabase-db-data: diff --git a/docker/kong.yml b/docker/kong.yml new file mode 100644 index 0000000..77103ce --- /dev/null +++ b/docker/kong.yml @@ -0,0 +1,138 @@ +_format_version: "2.1" +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: ${SUPABASE_ANON_KEY} + - username: service_role + keyauth_credentials: + - key: ${SUPABASE_SERVICE_KEY} + +### +### Access Control Lists +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### API Routes +### +services: + ## Open Auth routes + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: "GoTrue: /auth/v1/* -> http://auth:9999/*" + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: "PostgREST: /rest/v1/* -> http://rest:3000/*" + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Realtime routes + - name: realtime-v1 + _comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*" + url: http://realtime:4000/socket/ + routes: + - name: realtime-v1-all + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Meta routes (for Supabase Studio) + - name: meta + _comment: "pg-meta: /pg/* -> http://meta:8080/*" + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..97860c4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Handle SPA routing - serve index.html for all 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 X-XSS-Protection "1; mode=block" always; +}