version: '3.8' services: # Base de datos PostgreSQL con extensión PostGIS # NOTA: Usar versión 16 para compatibilidad con datos migrados de producción postgres: image: postgis/postgis:16-3.4-alpine container_name: trenes-postgres restart: unless-stopped environment: POSTGRES_DB: trenes_db POSTGRES_USER: trenes_user POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-trenes_password_change_me} PGDATA: /var/lib/postgresql/data/pgdata volumes: - postgres_data:/var/lib/postgresql/data - ./database/init:/docker-entrypoint-initdb.d ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U trenes_user -d trenes_db"] interval: 10s timeout: 5s retries: 5 networks: - trenes-network # Redis para cache redis: image: redis:7-alpine container_name: trenes-redis restart: unless-stopped command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis_password_change_me} volumes: - redis_data:/data ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] interval: 10s timeout: 5s retries: 5 networks: - trenes-network # Flyway - Gestor de migraciones de base de datos flyway: image: flyway/flyway:10-alpine container_name: trenes-flyway command: migrate environment: FLYWAY_URL: jdbc:postgresql://postgres:5432/trenes_db FLYWAY_USER: trenes_user FLYWAY_PASSWORD: ${POSTGRES_PASSWORD:-trenes_password_change_me} FLYWAY_BASELINE_ON_MIGRATE: "true" FLYWAY_BASELINE_VERSION: "0" FLYWAY_SCHEMAS: public FLYWAY_LOCATIONS: filesystem:/flyway/sql FLYWAY_VALIDATE_ON_MIGRATE: "true" FLYWAY_OUT_OF_ORDER: "false" volumes: - ./database/migrations:/flyway/sql depends_on: postgres: condition: service_healthy networks: - trenes-network profiles: - migration # Worker para polling GTFS-RT Vehicle Positions worker: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-worker restart: unless-stopped environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 GTFS_RT_URL: https://gtfsrt.renfe.com/vehicle_positions.pb POLLING_INTERVAL: 30000 LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # Worker para sincronización GTFS Static gtfs-static-syncer: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-gtfs-static-syncer restart: unless-stopped command: node src/worker/gtfs-static-syncer.js environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 GTFS_STATIC_URL: https://data.renfe.com/dataset/horarios-trenes-largo-recorrido-ave/resource/horarios-trenes-largo-recorrido-ave-gtfs.zip SYNC_SCHEDULE: 0 3 * * * LOG_LEVEL: info volumes: - gtfs_static_data:/tmp/gtfs depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # Worker para polling GTFS-RT Trip Updates trip-updates-poller: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-trip-updates-poller restart: unless-stopped command: node src/worker/trip-updates-poller.js environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 GTFS_RT_TRIP_UPDATES_URL: https://gtfsrt.renfe.com/trip_updates_cercanias.pb POLLING_INTERVAL: 30000 LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # Worker para polling GTFS-RT Service Alerts alerts-poller: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-alerts-poller restart: unless-stopped command: node src/worker/alerts-poller.js environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 GTFS_RT_ALERTS_URL: https://gtfsrt.renfe.com/alerts.pb POLLING_INTERVAL: 30000 LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # Worker para datos de flota Renfe (delay, estaciones, etc.) renfe-fleet-poller: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-renfe-fleet-poller restart: unless-stopped command: node src/worker/renfe-fleet-poller.js environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # Worker para refrescar vistas de analytics analytics-refresher: build: context: ./backend dockerfile: Dockerfile target: worker container_name: trenes-analytics-refresher restart: unless-stopped command: node src/worker/analytics-refresher.js environment: NODE_ENV: production DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 ANALYTICS_REFRESH_SCHEDULE: "*/15 * * * *" LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - trenes-network # API Backend api: build: context: ./backend dockerfile: Dockerfile target: api container_name: trenes-api restart: unless-stopped environment: NODE_ENV: production PORT: 3000 DATABASE_URL: postgresql://trenes_user:${POSTGRES_PASSWORD:-trenes_password_change_me}@postgres:5432/trenes_db REDIS_URL: redis://:${REDIS_PASSWORD:-redis_password_change_me}@redis:6379 CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:80} JWT_SECRET: ${JWT_SECRET:-jwt_secret_change_me} LOG_LEVEL: info depends_on: postgres: condition: service_healthy redis: condition: service_healthy ports: - "3000:3000" healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 networks: - trenes-network # Frontend frontend: build: context: ./frontend dockerfile: Dockerfile target: production args: # IMPORTANTE: VITE_WS_URL no debe incluir /ws, Socket.io añade /socket.io/ automáticamente VITE_API_URL: ${VITE_API_URL:-http://localhost/api} VITE_WS_URL: ${VITE_WS_URL:-http://localhost} container_name: trenes-frontend restart: unless-stopped ports: - "5173:80" depends_on: - api networks: - trenes-network # Nginx como reverse proxy nginx: image: nginx:alpine container_name: trenes-nginx restart: unless-stopped volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - nginx_logs:/var/log/nginx ports: - "80:80" - "443:443" depends_on: - api - frontend healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 networks: - trenes-network # Adminer (opcional) - UI para gestionar PostgreSQL adminer: image: adminer:latest container_name: trenes-adminer restart: unless-stopped ports: - "8080:8080" environment: ADMINER_DEFAULT_SERVER: postgres ADMINER_DESIGN: dracula depends_on: - postgres networks: - trenes-network profiles: - debug # Redis Commander (opcional) - UI para gestionar Redis redis-commander: image: rediscommander/redis-commander:latest container_name: trenes-redis-commander restart: unless-stopped environment: REDIS_HOSTS: local:redis:6379:0:${REDIS_PASSWORD:-redis_password_change_me} ports: - "8081:8081" depends_on: - redis networks: - trenes-network profiles: - debug volumes: postgres_data: driver: local redis_data: driver: local nginx_logs: driver: local gtfs_static_data: driver: local networks: trenes-network: driver: bridge