From 4b943fb9978000a662c7df4404557ce3befc294e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 1 Feb 2026 01:50:02 -0600 Subject: [PATCH] feat: Add Docker build & push to Woodpecker CI pipeline - Add docker-build-api, docker-build-web, docker-build-postgres steps - Images pushed to reg.diversecanvas.com/mosaic/* on main/develop - Create docker-compose.prod.yml for production deployments - Add .env.prod.example with production configuration Requires Harbor secrets in Woodpecker: - harbor_username - harbor_password Co-Authored-By: Claude Opus 4.5 --- .env.prod.example | 66 +++++++++++++++ .woodpecker.yml | 73 ++++++++++++++++ docker-compose.prod.yml | 179 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 .env.prod.example create mode 100644 docker-compose.prod.yml diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..1b21644 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,66 @@ +# ============================================== +# Mosaic Stack Production Environment +# ============================================== +# Copy to .env and configure for production deployment + +# ====================== +# PostgreSQL Database +# ====================== +# CRITICAL: Use a strong, unique password +POSTGRES_USER=mosaic +POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD +POSTGRES_DB=mosaic +POSTGRES_SHARED_BUFFERS=256MB +POSTGRES_EFFECTIVE_CACHE_SIZE=1GB +POSTGRES_MAX_CONNECTIONS=100 + +# ====================== +# Valkey Cache +# ====================== +VALKEY_MAXMEMORY=256mb + +# ====================== +# API Configuration +# ====================== +API_PORT=3001 +API_HOST=0.0.0.0 + +# ====================== +# Web Configuration +# ====================== +WEB_PORT=3000 +NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev + +# ====================== +# Authentication (Authentik OIDC) +# ====================== +OIDC_ISSUER=https://auth.diversecanvas.com/application/o/mosaic-stack/ +OIDC_CLIENT_ID=your-client-id +OIDC_CLIENT_SECRET=your-client-secret +OIDC_REDIRECT_URI=https://api.mosaicstack.dev/auth/callback/authentik + +# ====================== +# JWT Configuration +# ====================== +# CRITICAL: Generate a random secret (openssl rand -base64 32) +JWT_SECRET=REPLACE_WITH_RANDOM_SECRET +JWT_EXPIRATION=24h + +# ====================== +# Traefik Integration +# ====================== +# Set to true if using external Traefik +TRAEFIK_ENABLE=true +TRAEFIK_ENTRYPOINT=websecure +TRAEFIK_TLS_ENABLED=true +TRAEFIK_DOCKER_NETWORK=traefik-public +TRAEFIK_CERTRESOLVER=letsencrypt + +# Domain configuration +MOSAIC_API_DOMAIN=api.mosaicstack.dev +MOSAIC_WEB_DOMAIN=app.mosaicstack.dev + +# ====================== +# Optional: Ollama +# ====================== +# OLLAMA_ENDPOINT=http://ollama.diversecanvas.com:11434 diff --git a/.woodpecker.yml b/.woodpecker.yml index 433d908..01ee8bc 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -78,3 +78,76 @@ steps: - typecheck # Only block on critical checks - security-audit - prisma-generate + + # ====================== + # Docker Build & Push (main/develop only) + # ====================== + # Requires secrets: harbor_username, harbor_password + + docker-build-api: + image: woodpeckerci/plugin-docker-buildx + settings: + registry: reg.diversecanvas.com + repo: reg.diversecanvas.com/mosaic/api + dockerfile: apps/api/Dockerfile + context: . + platforms: + - linux/amd64 + tags: + - "${CI_COMMIT_SHA:0:8}" + - latest + username: + from_secret: harbor_username + password: + from_secret: harbor_password + when: + - branch: [main, develop] + event: push + depends_on: + - build + + docker-build-web: + image: woodpeckerci/plugin-docker-buildx + settings: + registry: reg.diversecanvas.com + repo: reg.diversecanvas.com/mosaic/web + dockerfile: apps/web/Dockerfile + context: . + platforms: + - linux/amd64 + build_args: + - NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev + tags: + - "${CI_COMMIT_SHA:0:8}" + - latest + username: + from_secret: harbor_username + password: + from_secret: harbor_password + when: + - branch: [main, develop] + event: push + depends_on: + - build + + docker-build-postgres: + image: woodpeckerci/plugin-docker-buildx + settings: + registry: reg.diversecanvas.com + repo: reg.diversecanvas.com/mosaic/postgres + dockerfile: docker/postgres/Dockerfile + context: docker/postgres + platforms: + - linux/amd64 + tags: + - "${CI_COMMIT_SHA:0:8}" + - latest + username: + from_secret: harbor_username + password: + from_secret: harbor_password + when: + - branch: [main, develop] + event: push + depends_on: + - build diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..cf0f806 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,179 @@ +# Production Docker Compose - Uses pre-built images from Harbor +# +# Prerequisites: +# - Images built and pushed to reg.diversecanvas.com/mosaic/* +# - .env file configured with production values +# +# Usage: +# docker compose -f docker-compose.prod.yml up -d +# +# For Portainer: +# - Stack → Add Stack → Repository +# - Compose file: docker-compose.prod.yml + +services: + # ====================== + # PostgreSQL Database + # ====================== + postgres: + image: reg.diversecanvas.com/mosaic/postgres:latest + container_name: mosaic-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-mosaic} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-mosaic} + POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-256MB} + POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB} + POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mosaic} -d ${POSTGRES_DB:-mosaic}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - mosaic-internal + labels: + - "com.mosaic.service=database" + - "com.mosaic.description=PostgreSQL 17 with pgvector" + + # ====================== + # Valkey Cache + # ====================== + valkey: + image: valkey/valkey:8-alpine + container_name: mosaic-valkey + restart: unless-stopped + command: + - valkey-server + - --maxmemory ${VALKEY_MAXMEMORY:-256mb} + - --maxmemory-policy allkeys-lru + - --appendonly yes + volumes: + - valkey_data:/data + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - mosaic-internal + labels: + - "com.mosaic.service=cache" + - "com.mosaic.description=Valkey Redis-compatible cache" + + # ====================== + # Mosaic API + # ====================== + api: + image: reg.diversecanvas.com/mosaic/api:latest + container_name: mosaic-api + restart: unless-stopped + environment: + NODE_ENV: production + PORT: ${API_PORT:-3001} + API_HOST: ${API_HOST:-0.0.0.0} + DATABASE_URL: postgresql://${POSTGRES_USER:-mosaic}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-mosaic} + VALKEY_URL: redis://valkey:6379 + OIDC_ISSUER: ${OIDC_ISSUER} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} + OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI} + JWT_SECRET: ${JWT_SECRET} + JWT_EXPIRATION: ${JWT_EXPIRATION:-24h} + OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434} + ports: + - "${API_PORT:-3001}:${API_PORT:-3001}" + depends_on: + postgres: + condition: service_healthy + valkey: + condition: service_healthy + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:${API_PORT:-3001}/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - mosaic-internal + - mosaic-public + labels: + - "com.mosaic.service=api" + - "com.mosaic.description=Mosaic NestJS API" + - "traefik.enable=${TRAEFIK_ENABLE:-false}" + - "traefik.http.routers.mosaic-api.rule=Host(`${MOSAIC_API_DOMAIN:-api.mosaicstack.dev}`)" + - "traefik.http.routers.mosaic-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.mosaic-api.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.services.mosaic-api.loadbalancer.server.port=${API_PORT:-3001}" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-mosaic-public}" + - "traefik.http.routers.mosaic-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + + # ====================== + # Mosaic Web + # ====================== + web: + image: reg.diversecanvas.com/mosaic/web:latest + container_name: mosaic-web + restart: unless-stopped + environment: + NODE_ENV: production + PORT: ${WEB_PORT:-3000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://api.mosaicstack.dev} + ports: + - "${WEB_PORT:-3000}:${WEB_PORT:-3000}" + depends_on: + api: + condition: service_healthy + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:${WEB_PORT:-3000}'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - mosaic-public + labels: + - "com.mosaic.service=web" + - "com.mosaic.description=Mosaic Next.js Web App" + - "traefik.enable=${TRAEFIK_ENABLE:-false}" + - "traefik.http.routers.mosaic-web.rule=Host(`${MOSAIC_WEB_DOMAIN:-app.mosaicstack.dev}`)" + - "traefik.http.routers.mosaic-web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.mosaic-web.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.services.mosaic-web.loadbalancer.server.port=${WEB_PORT:-3000}" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-mosaic-public}" + - "traefik.http.routers.mosaic-web.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + +# ====================== +# Volumes +# ====================== +volumes: + postgres_data: + name: mosaic-postgres-data + driver: local + valkey_data: + name: mosaic-valkey-data + driver: local + +# ====================== +# Networks +# ====================== +networks: + mosaic-internal: + name: mosaic-internal + driver: bridge + mosaic-public: + name: mosaic-public + driver: bridge