services: db: image: postgres:16-alpine environment: POSTGRES_USER: ${APIX_DB_USER:-apix} POSTGRES_PASSWORD: ${APIX_DB_PASSWORD:-apix} POSTGRES_DB: ${APIX_DB_NAME:-apix} ports: - "127.0.0.1:${APIX_DB_PORT:-5432}:5432" volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${APIX_DB_USER:-apix}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # ── Registry (a/b for rolling zero-downtime deploys) ────────────────────── registry-a: image: apix-registry:latest ports: - "8180:8180" environment: QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${APIX_DB_NAME:-apix} QUARKUS_DATASOURCE_USERNAME: ${APIX_DB_USER:-apix} QUARKUS_DATASOURCE_PASSWORD: ${APIX_DB_PASSWORD:-apix} APIX_API_KEY: ${APIX_API_KEY} APIX_REGISTRY_BASE_URL: ${APIX_REGISTRY_BASE_URL:-https://api-index.org} APIX_REGISTRY_NAME: ${APIX_REGISTRY_NAME:-APIX Registry} GLEIF_API_URL: ${GLEIF_API_URL:-https://api.gleif.org/api/v1} OPENCORPORATES_API_KEY: ${OPENCORPORATES_API_KEY:-} APIX_MAIL_SIGNING_PRIVATE_KEY: ${APIX_MAIL_SIGNING_PRIVATE_KEY:-} APIX_MAIL_SIGNING_PUBLIC_KEY: ${APIX_MAIL_SIGNING_PUBLIC_KEY:-} APIX_MAIL_SIGNING_KID: ${APIX_MAIL_SIGNING_KID:-dev} APIX_PORTAL_BASE_URL: ${APIX_PORTAL_BASE_URL:-https://www.api-index.org} SANCTIONS_CACHE_PATH: /app/sanctions LOG_LEVEL: ${LOG_LEVEL:-INFO} volumes: - sanctions_cache:/app/sanctions depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8180/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped registry-b: image: apix-registry:latest environment: QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${APIX_DB_NAME:-apix} QUARKUS_DATASOURCE_USERNAME: ${APIX_DB_USER:-apix} QUARKUS_DATASOURCE_PASSWORD: ${APIX_DB_PASSWORD:-apix} APIX_API_KEY: ${APIX_API_KEY} APIX_REGISTRY_BASE_URL: ${APIX_REGISTRY_BASE_URL:-https://api-index.org} APIX_REGISTRY_NAME: ${APIX_REGISTRY_NAME:-APIX Registry} GLEIF_API_URL: ${GLEIF_API_URL:-https://api.gleif.org/api/v1} OPENCORPORATES_API_KEY: ${OPENCORPORATES_API_KEY:-} APIX_MAIL_SIGNING_PRIVATE_KEY: ${APIX_MAIL_SIGNING_PRIVATE_KEY:-} APIX_MAIL_SIGNING_PUBLIC_KEY: ${APIX_MAIL_SIGNING_PUBLIC_KEY:-} APIX_MAIL_SIGNING_KID: ${APIX_MAIL_SIGNING_KID:-dev} APIX_PORTAL_BASE_URL: ${APIX_PORTAL_BASE_URL:-https://www.api-index.org} SANCTIONS_CACHE_PATH: /app/sanctions LOG_LEVEL: ${LOG_LEVEL:-INFO} volumes: - sanctions_cache:/app/sanctions depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8180/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped # ── Spider (single — cron job, no in-flight request concern) ───────────── spider: image: apix-spider:latest environment: QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${APIX_DB_NAME:-apix} QUARKUS_DATASOURCE_USERNAME: ${APIX_DB_USER:-apix} QUARKUS_DATASOURCE_PASSWORD: ${APIX_DB_PASSWORD:-apix} SPIDER_INTERVAL_MINUTES: ${SPIDER_INTERVAL_MINUTES:-15} LOG_LEVEL: ${LOG_LEVEL:-INFO} depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8082/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped # ── Demo (a/b) ──────────────────────────────────────────────────────────── demo-a: image: apix-demo:latest environment: QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${APIX_DB_NAME:-apix} QUARKUS_DATASOURCE_USERNAME: ${APIX_DB_USER:-apix} QUARKUS_DATASOURCE_PASSWORD: ${APIX_DB_PASSWORD:-apix} APIX_REGISTRY_URL: http://registry-a:8180 APIX_API_KEY: ${APIX_API_KEY} APIX_DEMO_BASE_URL: ${APIX_DEMO_BASE_URL:-https://demo.api-index.org} LOG_LEVEL: ${LOG_LEVEL:-INFO} depends_on: registry-a: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8083/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped demo-b: image: apix-demo:latest environment: QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${APIX_DB_NAME:-apix} QUARKUS_DATASOURCE_USERNAME: ${APIX_DB_USER:-apix} QUARKUS_DATASOURCE_PASSWORD: ${APIX_DB_PASSWORD:-apix} APIX_REGISTRY_URL: http://registry-b:8180 APIX_API_KEY: ${APIX_API_KEY} APIX_DEMO_BASE_URL: ${APIX_DEMO_BASE_URL:-https://demo.api-index.org} LOG_LEVEL: ${LOG_LEVEL:-INFO} depends_on: registry-b: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8083/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped # ── Portal (a/b) ────────────────────────────────────────────────────────── portal-a: image: apix-portal:latest ports: - "8081:8081" environment: APIX_REGISTRY_URL: http://registry-a:8180 LOG_LEVEL: ${LOG_LEVEL:-INFO} depends_on: - registry-a healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8081/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped portal-b: image: apix-portal:latest environment: APIX_REGISTRY_URL: http://registry-b:8180 LOG_LEVEL: ${LOG_LEVEL:-INFO} depends_on: - registry-b healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8081/q/health/live || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped # ── Edge proxy ──────────────────────────────────────────────────────────── caddy: image: caddy:2-alpine ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: - registry-a - portal-a restart: unless-stopped # ── Source control & CI ─────────────────────────────────────────────────── gitea: image: gitea/gitea:1 environment: USER_UID: "1000" USER_GID: "1000" GITEA__server__DOMAIN: git.api-index.org GITEA__server__ROOT_URL: https://git.api-index.org GITEA__server__HTTP_PORT: "3001" GITEA__server__SSH_PORT: "2222" GITEA__server__SSH_DOMAIN: git.api-index.org GITEA__database__DB_TYPE: sqlite3 GITEA__security__SECRET_KEY: ${GITEA_SECRET_KEY} GITEA__security__INTERNAL_TOKEN: ${GITEA_INTERNAL_TOKEN} GITEA__security__INSTALL_LOCK: "true" GITEA__service__DISABLE_REGISTRATION: "true" GITEA__service__REQUIRE_SIGNIN_VIEW: "false" GITEA__actions__ENABLED: "true" GITEA__log__LEVEL: warn ports: - "127.0.0.1:3001:3001" - "2222:2222" volumes: - gitea_data:/data restart: unless-stopped # ── Observability ───────────────────────────────────────────────────────── prometheus: image: prom/prometheus:v2.53.1 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus_data:/prometheus command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=30d - --web.enable-lifecycle ports: - "127.0.0.1:9090:9090" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:9090/-/healthy || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped grafana: image: grafana/grafana:11.1.3 ports: - "127.0.0.1:3000:3000" environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} GF_USERS_ALLOW_SIGN_UP: "false" GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000} volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning:ro depends_on: - prometheus healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped volumes: db_data: sanctions_cache: caddy_data: caddy_config: prometheus_data: grafana_data: gitea_data: