services: timescaledb: image: timescale/timescaledb:latest-pg16 container_name: data-service-db restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5433:5432" # se pone el el puerto 5433 para no interferir con una instalación local de postgres que use el 5432 volumes: # Volumen con nombre: aquí vive el archivo físico de la base de # datos. Persiste aunque el contenedor se apague, se reinicie o # se borre (docker rm) — solo se pierde con un `docker volume rm` # explícito. - timescale-data:/var/lib/postgresql/data # Cualquier .sql en /docker-entrypoint-initdb.d se ejecuta # automáticamente la PRIMERA vez que el volumen está vacío (o # sea, la primera vez que se crea el contenedor). Así el esquema # completo (extensión, tablas, seed) se aplica solo, sin pasos # manuales. - ./data_service/docs/data_service_schema.sql:/docker-entrypoint-initdb.d/01_schema.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 10 data_service: build: ./data_service container_name: data-service-app restart: unless-stopped environment: # Dentro de la red de docker-compose, el host de la base de datos # es el NOMBRE DEL SERVICIO ("timescaledb"), no "localhost". DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB} DEFAULT_START_DATE: ${DEFAULT_START_DATE} PIPELINE_VERSION: ${PIPELINE_VERSION} DATA_SERVICE_LOG_DIR: /app/logs ports: - "8000:8000" volumes: - ./logs/data_service:/app/logs depends_on: timescaledb: condition: service_healthy mlflow: image: ghcr.io/mlflow/mlflow:v2.16.2 container_name: mlflow-server restart: unless-stopped command: > mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri sqlite:///mlflow-data/mlflow.db --default-artifact-root /mlflow-data/artifacts ports: - "5000:5000" volumes: - mlflow-data:/mlflow-data healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:5000')\" || exit 1"] interval: 10s timeout: 5s retries: 10 ml_service: build: ./ml_service container_name: ml-service-app restart: unless-stopped environment: DATA_SERVICE_BASE_URL: http://data_service:8000 DATA_SERVICE_PAIR_CODE: ${DATA_SERVICE_PAIR_CODE:-SP500_VIX} INTERNAL_SERVICE_KEY: ${INTERNAL_SERVICE_KEY} ML_SERVICE_API_KEY: ${ML_SERVICE_API_KEY} REQUIRE_API_KEY: "true" MLFLOW_TRACKING_URI: http://mlflow:5000 MLFLOW_EXPERIMENT_NAME: ${MLFLOW_EXPERIMENT_NAME:-sp500_volatility} MLFLOW_REGISTERED_MODEL_NAME: ${MLFLOW_REGISTERED_MODEL_NAME:-sp500_range_predictor} PREDICTIONS_DB_PATH: /app/data/predictions.db HISTORICAL_BACKTEST_START: ${HISTORICAL_BACKTEST_START:-2022-01-01} HISTORICAL_BACKTEST_OUTPUT_DIR: /app/data/exports MODEL_LOCAL_DIR: /app/data/models RETRAIN_SCHEDULE_DAY: "1" RETRAIN_SCHEDULE_HOUR: "3" RETRAIN_SCHEDULE_MINUTE: "0" RETRAIN_SCHEDULE_TIMEZONE: ${RETRAIN_SCHEDULE_TIMEZONE:-America/Bogota} SCHEDULER_ENABLED: "true" ML_SERVICE_LOG_DIR: /app/logs ports: # RNF20: en producción real, esto no debería mapearse al host # (el ML Service no debe exponerse a internet). Se deja aquí # solo para que puedas probarlo manualmente con curl/Swagger # mientras lo revisas. - "8001:8001" volumes: - ml-service-data:/app/data - ./logs/ml_service:/app/logs:Z depends_on: data_service: condition: service_started mlflow: condition: service_healthy volumes: timescale-data: mlflow-data: ml-service-data: