# 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:10.0.0 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 - 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