All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 36s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m36s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Configure Grafana Unified Alerting with file-based provisioned alert rules, contact points, and notification policies. Add stable UID to Loki datasource for alert rule references. Update LOGGING.md with dashboard descriptions, alerting rules table, and LogQL query reference. Alert rules: Error Rate Spike (critical), Container Silence for backend/postgres/redis (warning), 5xx Response Spike (critical). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
399 lines
14 KiB
YAML
399 lines
14 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: 5s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
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"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# 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: 5s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
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"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# 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: 5s
|
|
timeout: 5s
|
|
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"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Application Services - OCR Processing
|
|
mvp-ocr:
|
|
build:
|
|
context: ./ocr
|
|
dockerfile: Dockerfile
|
|
container_name: mvp-ocr
|
|
restart: unless-stopped
|
|
environment:
|
|
LOG_LEVEL: info
|
|
REDIS_HOST: mvp-redis
|
|
REDIS_PORT: 6379
|
|
REDIS_DB: 1
|
|
networks:
|
|
- backend
|
|
- database
|
|
depends_on:
|
|
- mvp-redis
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# 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
|
|
POSTGRES_LOG_STATEMENT: ${POSTGRES_LOG_STATEMENT:-ddl}
|
|
POSTGRES_LOG_MIN_DURATION_STATEMENT: ${POSTGRES_LOG_MIN_DURATION:-500}
|
|
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: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# 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 --loglevel ${REDIS_LOGLEVEL:-notice}
|
|
volumes:
|
|
- mvp_redis_data:/data
|
|
networks:
|
|
- database
|
|
ports:
|
|
- "6379:6379" # Development access only
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 5s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Log Aggregation - Loki
|
|
mvp-loki:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/grafana/loki:3.6.1
|
|
container_name: mvp-loki
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./config/loki/config.yml:/etc/loki/config.yml:ro
|
|
- mvp_loki_data:/loki
|
|
command: -config.file=/etc/loki/config.yml
|
|
networks:
|
|
- backend
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/ready || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Log Aggregation - Alloy (replaces Promtail)
|
|
mvp-alloy:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/grafana/alloy:v1.12.2
|
|
container_name: mvp-alloy
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./config/alloy/config.alloy:/etc/alloy/config.alloy
|
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
command:
|
|
- run
|
|
- --server.http.listen-addr=0.0.0.0:12345
|
|
- --storage.path=/var/lib/alloy/data
|
|
- /etc/alloy/config.alloy
|
|
networks:
|
|
- backend
|
|
depends_on:
|
|
- mvp-loki
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:12345/ready || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Log Aggregation - Grafana
|
|
mvp-grafana:
|
|
image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/grafana/grafana:12.4.0-21693836646
|
|
container_name: mvp-grafana
|
|
restart: unless-stopped
|
|
environment:
|
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
|
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
|
volumes:
|
|
- ./config/grafana/datasources:/etc/grafana/provisioning/datasources:ro
|
|
- ./config/grafana/provisioning:/etc/grafana/provisioning/dashboards:ro
|
|
- ./config/grafana/alerting:/etc/grafana/provisioning/alerting:ro
|
|
- ./config/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
- mvp_grafana_data:/var/lib/grafana
|
|
networks:
|
|
- backend
|
|
- frontend
|
|
depends_on:
|
|
- mvp-loki
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=motovaultpro_frontend"
|
|
- "traefik.http.routers.grafana.rule=Host(`logs.motovaultpro.com`)"
|
|
- "traefik.http.routers.grafana.entrypoints=websecure"
|
|
- "traefik.http.routers.grafana.tls=true"
|
|
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.grafana.middlewares=grafana-ipwhitelist@file"
|
|
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# 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
|
|
mvp_loki_data:
|
|
name: mvp_loki_data
|
|
mvp_grafana_data:
|
|
name: mvp_grafana_data
|