Files
motovaultpro/docker-compose.yml
Eric Gullickson 9b6417379b chore: update Docker and compose files for PaddleOCR engine (refs #119)
- Replace libtesseract-dev with libgomp1 (OpenMP for PaddlePaddle)
- Pre-download PP-OCRv4 models during Docker build
- Add OCR engine env vars to all compose files (base, staging, prod)
- Add optional Google Vision secret mount (commented, enable on demand)
- Create google-vision-key.json.example placeholder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 11:17:44 -06:00

414 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:
LOG_LEVEL: debug
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
LOG_LEVEL: debug
# 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: debug
REDIS_HOST: mvp-redis
REDIS_PORT: 6379
REDIS_DB: 1
# OCR engine configuration (PaddleOCR primary, cloud fallback optional)
OCR_PRIMARY_ENGINE: paddleocr
OCR_FALLBACK_ENGINE: ${OCR_FALLBACK_ENGINE:-none}
OCR_FALLBACK_THRESHOLD: ${OCR_FALLBACK_THRESHOLD:-0.6}
GOOGLE_VISION_KEY_PATH: /run/secrets/google-vision-key.json
volumes:
- /tmp/vin-debug:/tmp/vin-debug
# Optional: Uncomment to enable Google Vision cloud fallback.
# Requires: secrets/app/google-vision-key.json and OCR_FALLBACK_ENGINE=google_vision
# - ./secrets/app/google-vision-key.json:/run/secrets/google-vision-key.json:ro
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
LOG_LEVEL: debug
POSTGRES_LOG_STATEMENT: all
POSTGRES_LOG_MIN_DURATION_STATEMENT: 0
PGDATA: /var/lib/postgresql/data
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 ${LOG_LEVEL:-info}
environment:
LOG_LEVEL: debug
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:
# Loki 3.x uses a distroless image with no shell or HTTP client.
# Disable in-container healthcheck; Alloy and Grafana verify connectivity.
disable: true
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", "bash -c 'echo > /dev/tcp/localhost/12345'"]
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