# GTI Fiscal Hub — production compose (RC1). Only two containers: PostgreSQL + Hub. No Kubernetes/Redis/RabbitMQ. # # cd deploy/hub && cp .env.example .env && (edit .env) && docker compose -f docker-compose.prod.yml --env-file .env up -d --build # # PostgreSQL is on an internal network only: it publishes NO port and is not reachable from the LAN/internet. # The Hub binds to HUB_BIND (default 127.0.0.1): put a TLS reverse proxy (HTTPS) in front of it for the Agents. # Migrations run automatically at Hub start (Dockerfile CMD: alembic upgrade head). name: gti-fiscal-hub x-logging: &logging driver: json-file options: max-size: "10m" max-file: "5" services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: gti_fiscal POSTGRES_USER: gti POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required (URL-safe, e.g. openssl rand -hex 24)} volumes: - postgres_data:/var/lib/postgresql/data - ${BACKUP_DIR:-./backups}:/backups networks: [internal] healthcheck: test: ["CMD-SHELL", "pg_isready -U gti -d gti_fiscal"] interval: 10s timeout: 5s retries: 12 logging: *logging fiscal-hub: build: context: ../.. dockerfile: central/Dockerfile image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.3.3} restart: unless-stopped depends_on: postgres: condition: service_healthy environment: APP_ENV: production POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} LOG_LEVEL: ${LOG_LEVEL:-INFO} # Product-enrichment providers (free plans). Secrets live ONLY in deploy/hub/.env on the server (never committed, never in the Agent/installer/panel/logs). OSCBR_TOKEN: ${OSCBR_TOKEN:-} OSCBR_USER: ${OSCBR_USER:-} OSCBR_PASSWORD: ${OSCBR_PASSWORD:-} KODEBAR_API_KEY: ${KODEBAR_API_KEY:-} COSMOS_ENABLED: ${COSMOS_ENABLED:-0} OSCBR_BACKFILL_ENABLED: ${OSCBR_BACKFILL_ENABLED:-0} PROVIDER_OPERATIONAL_RESERVE_PERCENT: ${PROVIDER_OPERATIONAL_RESERVE_PERCENT:-20} BASE_URL: ${BASE_URL:?BASE_URL is required (public HTTPS URL of the Hub)} AGENT_ONLINE_THRESHOLD_SECONDS: ${AGENT_ONLINE_THRESHOLD_SECONDS:-120} # Reverse proxy on another host: uvicorn only honours X-Forwarded-For/-Proto # from these addresses (real client IP in audit/session records, scheme=https). FORWARDED_ALLOW_IPS: ${FORWARDED_ALLOW_IPS:-127.0.0.1} # 1 = session/CSRF cookies are sent only over HTTPS. Set once TLS is in front. WEB_COOKIE_SECURE: ${WEB_COOKIE_SECURE:-0} # SSO from GTI Gerenciador (central/app/sso.py): the panel's Ed25519 PUBLIC key # and its /sso/fiscal URL. Both empty = SSO off (local login only). SSO_GERENCIADOR_CHAVE_PUBLICA: ${SSO_GERENCIADOR_CHAVE_PUBLICA:-} SSO_START_URL: ${SSO_START_URL:-} FEDERATED_SESSION_SECONDS: ${FEDERATED_SESSION_SECONDS:-3600} # AGENT_DEV_TOKEN is intentionally NOT set: real Agents are registered with central/scripts/register_agent.py # (manually) or auto-provisioned via a successful POST /v1/agents/enroll (secure enrollment). # Agent distribution (central/app/agent_distribution.py): the universal package zip lives on the # agent_releases volume below, never in PostgreSQL. TTLs have safe defaults; override only if needed. AGENT_RELEASE_STORAGE_PATH: /app/data/releases AGENT_DOWNLOAD_LINK_TTL_SECONDS: ${AGENT_DOWNLOAD_LINK_TTL_SECONDS:-1800} AGENT_DOWNLOAD_MAX_USES_DEFAULT: ${AGENT_DOWNLOAD_MAX_USES_DEFAULT:-1} AGENT_ENROLLMENT_TTL_SECONDS: ${AGENT_ENROLLMENT_TTL_SECONDS:-86400} # Client mirror from GTI Gerenciador (central/app/client_sync.py): the panel is # the single source of truth for clients. Both empty = sync disabled. GERENCIADOR_INTERNAL_URL: ${GERENCIADOR_INTERNAL_URL:-} FISCAL_INTERNAL_API_TOKEN: ${FISCAL_INTERNAL_API_TOKEN:-} volumes: - agent_releases:/app/data/releases ports: - "${HUB_BIND:-127.0.0.1}:${HUB_PORT:-8000}:8000" networks: [internal, edge] healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] interval: 15s timeout: 5s retries: 10 start_period: 30s logging: *logging networks: internal: internal: true # no route to the outside: PostgreSQL is never exposed edge: {} volumes: postgres_data: agent_releases: