All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 30s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 31s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m19s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
The CI was failing because Docker marked the backend unhealthy before the CI wait loop completed. The backend needs time to run migrations and seed vehicle data on startup. Changes: - start_period: 40s -> 180s (3 minutes) - retries: 3 -> 5 (more tolerance) Total time before unhealthy: 180s + (5 × 30s) = 5.5 minutes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
266 lines
9.8 KiB
YAML
266 lines
9.8 KiB
YAML
# Base registry for mirrored images (override with environment variable)
|
|
x-registry: ®istry
|
|
REGISTRY_MIRRORS: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}
|
|
|
|
services:
|
|
# Traefik - Service Discovery and Load Balancing
|
|
mvp-traefik:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/traefik:v3.6
|
|
container_name: mvp-traefik
|
|
restart: unless-stopped
|
|
command:
|
|
- --configFile=/etc/traefik/traefik.yml
|
|
environment:
|
|
CLOUDFLARE_DNS_API_TOKEN_FILE: /run/secrets/cloudflare-dns-token
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "8080:8080" # Dashboard
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
- ./config/traefik/dynamic:/etc/traefik/dynamic:ro
|
|
- ./certs:/certs:ro
|
|
- ./data/traefik:/data
|
|
- ./secrets/app/cloudflare-dns-token.txt:/run/secrets/cloudflare-dns-token:ro
|
|
networks:
|
|
frontend:
|
|
ipv4_address: 10.96.1.50
|
|
backend:
|
|
healthcheck:
|
|
test: ["CMD", "traefik", "healthcheck"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 20s
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.motovaultpro.local`)"
|
|
- "traefik.http.routers.traefik-dashboard.tls=true"
|
|
- "traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080"
|
|
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$10$$foobar"
|
|
|
|
# Application Services - Frontend SPA
|
|
mvp-frontend:
|
|
build:
|
|
context: ./frontend
|
|
dockerfile: Dockerfile
|
|
cache_from:
|
|
- node:lts-alpine
|
|
- nginx:alpine
|
|
args:
|
|
VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com}
|
|
VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3}
|
|
VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com}
|
|
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api}
|
|
VITE_STRIPE_PUBLISHABLE_KEY: ${VITE_STRIPE_PUBLISHABLE_KEY:-pk_live_51Sr2yQJk87CpWj04YNBIaUWUtnJjeVTgk5NqHdpjqxgsbjy3dMKkIsqhjcpSkCzp3KvLi23BGgxhwV021EnEW3H400HhPYVyfN}
|
|
container_name: mvp-frontend
|
|
restart: unless-stopped
|
|
environment:
|
|
VITE_API_BASE_URL: /api
|
|
VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com}
|
|
VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3}
|
|
VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com}
|
|
SECRETS_DIR: /run/secrets
|
|
volumes:
|
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
|
networks:
|
|
- frontend
|
|
depends_on:
|
|
- mvp-backend
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000 || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 20s
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- traefik.docker.network=motovaultpro_frontend
|
|
- "traefik.http.routers.mvp-frontend.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && !PathPrefix(`/api`)"
|
|
- "traefik.http.routers.mvp-frontend.entrypoints=websecure"
|
|
- "traefik.http.routers.mvp-frontend.tls=true"
|
|
- "traefik.http.routers.mvp-frontend.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.mvp-frontend.priority=10"
|
|
- "traefik.http.services.mvp-frontend.loadbalancer.server.port=3000"
|
|
- "traefik.http.services.mvp-frontend.loadbalancer.healthcheck.path=/"
|
|
- "traefik.http.services.mvp-frontend.loadbalancer.healthcheck.interval=30s"
|
|
- "traefik.http.services.mvp-frontend.loadbalancer.passhostheader=true"
|
|
|
|
# Application Services - Backend API
|
|
mvp-backend:
|
|
build:
|
|
context: .
|
|
dockerfile: backend/Dockerfile
|
|
cache_from:
|
|
- node:lts-alpine
|
|
container_name: mvp-backend
|
|
restart: unless-stopped
|
|
environment:
|
|
# Core configuration (K8s pattern)
|
|
NODE_ENV: production
|
|
CONFIG_PATH: /app/config/production.yml
|
|
SECRETS_DIR: /run/secrets
|
|
# Service references
|
|
DATABASE_HOST: mvp-postgres
|
|
REDIS_HOST: mvp-redis
|
|
#Stripe Variables
|
|
STRIPE_PRO_MONTHLY_PRICE_ID: prod_Toj6BG9Z9JwREl
|
|
STRIPE_PRO_YEARLY_PRICE_ID: prod_Toj8oo0RpVBQmB
|
|
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID: prod_Toj8xGEui9jl6j
|
|
STRIPE_ENTERPRISE_YEARLY_PRICE_ID: prod_Toj9A7A773xrdn
|
|
volumes:
|
|
# Configuration files (K8s ConfigMap equivalent)
|
|
- ./config/app/production.yml:/app/config/production.yml:ro
|
|
- ./config/shared/production.yml:/app/config/shared.yml:ro
|
|
# Secrets (K8s Secrets equivalent)
|
|
- ./secrets/app/postgres-password.txt:/run/secrets/postgres-password:ro
|
|
- ./secrets/app/auth0-client-secret.txt:/run/secrets/auth0-client-secret:ro
|
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
|
- ./secrets/app/resend-api-key.txt:/run/secrets/resend-api-key:ro
|
|
- ./secrets/app/auth0-management-client-id.txt:/run/secrets/auth0-management-client-id:ro
|
|
- ./secrets/app/auth0-management-client-secret.txt:/run/secrets/auth0-management-client-secret:ro
|
|
- ./secrets/app/stripe-secret-key.txt:/run/secrets/stripe-secret-key:ro
|
|
- ./secrets/app/stripe-webhook-secret.txt:/run/secrets/stripe-webhook-secret:ro
|
|
# Filesystem storage for documents
|
|
- ./data/documents:/app/data/documents
|
|
# Filesystem storage for backups
|
|
- ./data/backups:/app/data/backups
|
|
networks:
|
|
- backend
|
|
- database
|
|
depends_on:
|
|
- mvp-postgres
|
|
- mvp-redis
|
|
healthcheck:
|
|
test:
|
|
- CMD-SHELL
|
|
- node -e "require('http').get('http://localhost:3001/health', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))"
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 180s
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=motovaultpro_backend"
|
|
# Main API router
|
|
- "traefik.http.routers.mvp-backend.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && PathPrefix(`/api`)"
|
|
- "traefik.http.routers.mvp-backend.entrypoints=websecure"
|
|
- "traefik.http.routers.mvp-backend.tls=true"
|
|
- "traefik.http.routers.mvp-backend.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.mvp-backend.priority=20"
|
|
# Health check router (bypass auth)
|
|
- "traefik.http.routers.mvp-backend-health.rule=(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && Path(`/api/health`)"
|
|
- "traefik.http.routers.mvp-backend-health.entrypoints=websecure"
|
|
- "traefik.http.routers.mvp-backend-health.tls=true"
|
|
- "traefik.http.routers.mvp-backend-health.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.mvp-backend-health.priority=30"
|
|
# Service configuration
|
|
- "traefik.http.services.mvp-backend.loadbalancer.server.port=3001"
|
|
- "traefik.http.services.mvp-backend.loadbalancer.healthcheck.path=/health"
|
|
- "traefik.http.services.mvp-backend.loadbalancer.healthcheck.interval=30s"
|
|
- "traefik.http.services.mvp-backend.loadbalancer.healthcheck.timeout=10s"
|
|
- "traefik.http.services.mvp-backend.loadbalancer.passhostheader=true"
|
|
|
|
# Application Services - OCR Processing
|
|
mvp-ocr:
|
|
build:
|
|
context: ./ocr
|
|
dockerfile: Dockerfile
|
|
container_name: mvp-ocr
|
|
restart: unless-stopped
|
|
environment:
|
|
LOG_LEVEL: info
|
|
networks:
|
|
- backend
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
# Database Services - Application PostgreSQL
|
|
mvp-postgres:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/postgres:18-alpine
|
|
container_name: mvp-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: motovaultpro
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
|
|
POSTGRES_INITDB_ARGS: --encoding=UTF8
|
|
volumes:
|
|
- mvp_postgres_data:/var/lib/postgresql/data
|
|
# Secrets (K8s Secrets equivalent)
|
|
- ./secrets/app/postgres-password.txt:/run/secrets/postgres-password:ro
|
|
networks:
|
|
- database
|
|
ports:
|
|
- "5432:5432" # Development access only
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
|
|
# Database Services - Application Redis
|
|
mvp-redis:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/redis:8.4-alpine
|
|
container_name: mvp-redis
|
|
restart: unless-stopped
|
|
command: redis-server --appendonly yes
|
|
volumes:
|
|
- mvp_redis_data:/data
|
|
networks:
|
|
- database
|
|
ports:
|
|
- "6379:6379" # Development access only
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# Network Definition
|
|
networks:
|
|
frontend:
|
|
driver: bridge
|
|
internal: false # Only for Traefik public access
|
|
ipam:
|
|
config:
|
|
- subnet: 10.96.1.0/24
|
|
labels:
|
|
- "com.motovaultpro.network=frontend"
|
|
- "com.motovaultpro.purpose=public-traffic-only"
|
|
|
|
backend:
|
|
driver: bridge
|
|
internal: false # Needs external access for Auth0 JWT validation
|
|
ipam:
|
|
config:
|
|
- subnet: 10.96.20.0/24
|
|
labels:
|
|
- "com.motovaultpro.network=backend"
|
|
- "com.motovaultpro.purpose=api-services"
|
|
|
|
database:
|
|
driver: bridge
|
|
internal: true # Data isolation
|
|
ipam:
|
|
config:
|
|
- subnet: 10.96.64.0/24
|
|
labels:
|
|
- "com.motovaultpro.network=database"
|
|
- "com.motovaultpro.purpose=data-layer"
|
|
|
|
# Volume Definitions
|
|
volumes:
|
|
mvp_postgres_data:
|
|
name: mvp_postgres_data
|
|
mvp_redis_data:
|
|
name: mvp_redis_data
|