services: redis: image: redis:alpine restart: unless-stopped networks: - internal postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: farmafinder POSTGRES_USER: farmafinder POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD must be set} networks: - internal healthcheck: test: ["CMD-SHELL", "pg_isready -U farmafinder"] interval: 5s timeout: 3s retries: 5 volumes: - postgres_data:/var/lib/postgresql/data backend: image: git.hacecalor.net/ichitux/farmafinder-backend:latest build: context: . dockerfile: apps/backend/Dockerfile restart: unless-stopped env_file: - ./apps/backend/.env environment: PORT: "3001" NODE_ENV: production SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set} CORS_ORIGIN: ${CORS_ORIGIN:?CORS_ORIGIN must be set} REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD:-} DATABASE_PATH: /app/data/database.sqlite FARMACIAS_WEBHOOK_URL: ${FARMACIAS_WEBHOOK_URL:-} PG_URL: postgresql://farmafinder:${PG_PASSWORD:?PG_PASSWORD must be set}@postgres:5432/farmafinder # OpenTelemetry — exported via OTLP gRPC to the shared Alloy collector OTEL_SERVICE_NAME: farmafinder-backend OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4317 OTEL_TRACES_EXPORTER: otlp OTEL_METRICS_EXPORTER: otlp OTEL_LOGS_EXPORTER: otlp PARAPHARMACY_API_URL: http://parapharmacy-api:3002 OTEL_RESOURCE_ATTRIBUTES: service.namespace=farmafinder volumes: - backend_data:/app/data depends_on: - redis - postgres networks: - internal frontend: image: git.hacecalor.net/ichitux/farmafinder-frontend:latest build: context: ./apps/frontend args: VITE_FARO_ENDPOINT: ${VITE_FARO_ENDPOINT:-http://localhost:4318} VITE_FARO_APP_NAME: ${VITE_FARO_APP_NAME:-farmafinder-frontend} VITE_FARO_ENV: ${VITE_FARO_ENV:-production} VITE_FARO_APP_VERSION: ${VITE_FARO_APP_VERSION:-1.0.0} restart: unless-stopped ports: - "4000:80" depends_on: - backend networks: - public - internal # --- Observability exporters (scraped by the shared Prometheus on srv84-macos) --- redis-exporter: image: oliver006/redis_exporter:latest restart: unless-stopped environment: REDIS_ADDR: redis://redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-} depends_on: - redis networks: - internal postgres-exporter: image: prometheuscommunity/postgres-exporter:latest restart: unless-stopped environment: DATA_SOURCE_NAME: postgresql://farmafinder:${PG_PASSWORD:?PG_PASSWORD must be set}@postgres:5432/farmafinder?sslmode=disable depends_on: - postgres networks: - internal # --- Parapharmacy API --- parapharmacy-api: image: git.hacecalor.net/ichitux/farmafinder-parapharmacy-api:latest build: context: . dockerfile: apps/parapharmacy-api/Dockerfile env_file: - ./apps/parapharmacy-api/.env restart: unless-stopped environment: PORT: "3002" NODE_ENV: production MONGODB_URI: mongodb://mongodb:27017/parapharmacy CORS_ORIGIN: ${CORS_ORIGIN:?CORS_ORIGIN must be set} INGEST_API_KEY: ${INGEST_API_KEY:?INGEST_API_KEY must be set} ADMIN_API_KEY: ${ADMIN_API_KEY:?ADMIN_API_KEY must be set} healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3002/api/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"] interval: 10s timeout: 5s retries: 5 start_period: 15s depends_on: - mongodb networks: - internal # --- MongoDB for Parapharmacy --- mongodb: image: mongo:7 restart: unless-stopped volumes: - mongodb_data:/data/db networks: - internal # --- N8N Workflow Automation --- n8n: image: n8nio/n8n:latest restart: unless-stopped environment: # Owner account (skips /setup) N8N_USER_MANAGEMENT_DISABLED: "false" N8N_OWNER_EMAIL: ${N8N_EMAIL:-admin@farmafinder.com} N8N_OWNER_PASSWORD: ${N8N_PASSWORD:?N8N_PASSWORD must be set} # Auth N8N_BASIC_AUTH_ACTIVE: "true" N8N_BASIC_AUTH_USER: ${N8N_USER:-admin} N8N_BASIC_AUTH_PASSWORD: ${N8N_PASSWORD:?N8N_PASSWORD must be set} # Database DB_TYPE: postgresdb DB_POSTGRESDB_HOST: postgres DB_POSTGRESDB_DATABASE: farmafinder DB_POSTGRESDB_USER: farmafinder DB_POSTGRESDB_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD must be set} # Network N8N_HOST: localhost N8N_PORT: 5678 N8N_PROTOCOL: http # Webhook URL for external calls WEBHOOK_URL: http://localhost:5678/ healthcheck: test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:5678/healthz"] interval: 10s timeout: 5s retries: 5 start_period: 30s volumes: - n8n_data:/home/node/.n8n - ./n8n/workflows:/home/node/workflows depends_on: postgres: condition: service_healthy networks: - internal # --- N8N Init: import + activate workflows BEFORE n8n starts, then seed DB --- n8n-init: image: n8nio/n8n:latest entrypoint: ["/bin/sh", "/home/node/init-import.sh"] environment: DB_TYPE: postgresdb DB_POSTGRESDB_HOST: postgres DB_POSTGRESDB_DATABASE: farmafinder DB_POSTGRESDB_USER: farmafinder DB_POSTGRESDB_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD must be set} N8N_OWNER_EMAIL: ${N8N_EMAIL:-admin@farmafinder.com} N8N_OWNER_PASSWORD: ${N8N_PASSWORD:?N8N_PASSWORD must be set} volumes: - n8n_data:/home/node/.n8n - ./n8n/workflows:/home/node/workflows - ./n8n/init-import.sh:/home/node/init-import.sh:ro - ./n8n/cleanup-db.js:/home/node/cleanup-db.js:ro - ./n8n/seed.json:/home/node/seed.json:ro depends_on: postgres: condition: service_healthy parapharmacy-api: condition: service_healthy n8n: condition: service_healthy networks: - internal volumes: backend_data: postgres_data: mongodb_data: n8n_data: networks: public: internal: