services: database: image: postgres:16-alpine container_name: ipsextractor-db environment: POSTGRES_DB: ipsextractor POSTGRES_USER: ipsextractor POSTGRES_PASSWORD: ipsextractor volumes: - ipsextractor_db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ipsextractor -d ipsextractor"] interval: 5s timeout: 5s retries: 15 start_period: 90s redis: image: redis:7-alpine container_name: ipsextractor-redis command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --save "" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 db-migrations: build: context: ./services/db dockerfile: Dockerfile container_name: ipsextractor-db-migrations depends_on: database: condition: service_healthy environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor restart: on-failure extraction: build: context: ./services/extraction dockerfile: Dockerfile container_name: ipsextractor-extraction depends_on: database: condition: service_healthy redis: condition: service_healthy db-migrations: condition: service_completed_successfully environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor REDIS_URL: redis://redis:6379/0 EXTRACTOR_BASE_URL: https://api.openai.com/v1 EXTRACTOR_API_KEY: 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 EXTRACTOR_MODEL: ${EXTRACTOR_MODEL:-gpt-5.4-mini} NORMALIZER_BASE_URL: https://api.openai.com/v1 NORMALIZER_API_KEY: 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 NORMALIZER_MODEL: gpt-5.4-mini EXTRACTOR_TIMEOUT_S: ${EXTRACTOR_TIMEOUT_S:-120} PDF_DPI: ${PDF_DPI:-150} PDF_THREAD_COUNT: ${PDF_THREAD_COUNT:-4} CONCURRENCY_LIMIT: ${CONCURRENCY_LIMIT:-50} ALLOWED_DOCUMENT_TYPES: ${ALLOWED_DOCUMENT_TYPES:-CC,TI,CE,RC,PA,PPT,PEP,EPT,NUIP} volumes: - ${IPS_DOCUMENT_ROOT:-./data}:/data - ips_queue:/tmp/ips_queue healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=5)"] interval: 10s timeout: 5s retries: 5 start_period: 30s # Escala horizontalmente: docker compose up --scale worker=4 worker: build: context: ./services/extraction dockerfile: Dockerfile command: python -m worker.main depends_on: database: condition: service_healthy redis: condition: service_healthy db-migrations: condition: service_completed_successfully deploy: replicas: ${WORKER_REPLICAS:-2} environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor REDIS_URL: redis://redis:6379/0 EXTRACTOR_BASE_URL: https://api.openai.com/v1 EXTRACTOR_API_KEY: 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 EXTRACTOR_MODEL: ${EXTRACTOR_MODEL:-gpt-5.4-mini} NORMALIZER_BASE_URL: https://api.openai.com/v1 NORMALIZER_API_KEY: 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 NORMALIZER_MODEL: gpt-5.4-mini EXTRACTOR_TIMEOUT_S: ${EXTRACTOR_TIMEOUT_S:-120} PDF_DPI: ${PDF_DPI:-150} PDF_THREAD_COUNT: ${PDF_THREAD_COUNT:-4} WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-25} ALLOWED_DOCUMENT_TYPES: ${ALLOWED_DOCUMENT_TYPES:-CC,TI,CE,RC,PA,PPT,PEP,EPT,NUIP} volumes: - ips_queue:/tmp/ips_queue cleaning: build: context: ./services/cleaning dockerfile: Dockerfile container_name: ipsextractor-cleaning depends_on: database: condition: service_healthy db-migrations: condition: service_completed_successfully environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor count-api: build: context: ./services/services-count-facturas-medicas-serving dockerfile: Dockerfile container_name: ipsextractor-count-api restart: unless-stopped depends_on: db-migrations: condition: service_completed_successfully environment: PORT: "8080" # OCR_DPI: 150 reduce RAM (~4×) frente a 300; suficiente para contar facturas. OCR_DPI: ${OCR_DPI:-150} OCR_LANG: ${OCR_LANG:-spa} # Con varios lotes paralelos desde el desktop, limitar goroutines evita OOM. OCR_POOL_SIZE: ${OCR_POOL_SIZE:-4} SYNC_MAX_FILES: ${SYNC_MAX_FILES:-50} SYNC_PROCESS_CONCURRENCY: ${SYNC_PROCESS_CONCURRENCY:-4} MAX_PDF_SIZE_MB: ${MAX_PDF_SIZE_MB:-20} LOG_PIPELINE_TIMING: ${LOG_PIPELINE_TIMING:-false} volumes: - ./services/services-count-facturas-medicas-serving/models/latest:/app/models:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 10s timeout: 5s retries: 5 start_period: 90s deploy: resources: limits: memory: ${COUNT_API_MEMORY:-4g} api_gateway: build: context: ./services/api_gateway dockerfile: Dockerfile container_name: ipsextractor-api depends_on: extraction: condition: service_healthy cleaning: condition: service_started count-api: condition: service_healthy environment: EXTRACTION_URL: http://extraction:8001 CLEANING_URL: http://cleaning:8002 COUNT_URL: http://count-api:8080 ports: # Único puerto expuesto al host. Los demás servicios solo son accesibles en la red Docker. - "0.0.0.0:8189:8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health/live"] interval: 10s timeout: 5s retries: 5 start_period: 15s volumes: ipsextractor_db: ips_queue: