# 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 - traefik_data:/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} 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 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 # 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: 3 start_period: 40s 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" # 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: traefik_data: null mvp_postgres_data: name: mvp_postgres_data mvp_redis_data: name: mvp_redis_data