Files
motovaultpro/docker-compose.yml
Eric Gullickson 4b2b318aff
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
feat: add Grafana alerting rules and documentation (refs #111)
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>
2026-02-06 10:19:00 -06:00

399 lines
14 KiB
YAML

# Base registry for mirrored images (override with environment variable)
x-registry: &registry
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