name: nyx-platform # Fase 1 — infra local de desenvolvimento. SOMENTE servicos de infraestrutura. # Os app services (backend-api / realtime-server / frontend) entram em fases posteriores. # WebSockets em tempo real vao DIRETO ao Hocuspocus (Node) — nunca ao FastAPI. services: # ── MySQL: estado final dos documentos (onStoreDocument) + dados de negocio ── mysql: image: mysql:8.4 container_name: nyx-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} ports: - "${MYSQL_PORT:-3306}:3306" volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"] interval: 10s timeout: 5s retries: 10 start_period: 30s networks: - nyx_net phpmyadmin: image: phpmyadmin/phpmyadmin:latest container_name: nyx-phpmyadmin restart: unless-stopped environment: PMA_HOST: mysql PMA_PORT: "3306" # porta INTERNA do mysql na rede do compose (nao e mapping) PMA_USER: ${MYSQL_USER} PMA_PASSWORD: ${MYSQL_PASSWORD} ports: - "${PHPMYADMIN_PORT:-8080}:80" depends_on: mysql: condition: service_healthy networks: - nyx_net # ── Redis: pub/sub do @hocuspocus/extension-redis (escala N instancias Node) ── redis: image: redis:7-alpine container_name: nyx-redis restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] ports: - "${REDIS_PORT:-6379}:6379" volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 10 networks: - nyx_net # ── OpenSearch: banco vetorial para RAG (LangChain embeddings via FastAPI) ── opensearch: image: opensearchproject/opensearch:2.15.0 container_name: nyx-opensearch restart: unless-stopped environment: - discovery.type=single-node - DISABLE_SECURITY_PLUGIN=true # ponytail: dev only. Prod = plugin de seguranca + TLS + auth. - DISABLE_INSTALL_DEMO_CONFIG=true - bootstrap.memory_lock=true - "OPENSEARCH_JAVA_OPTS=${OPENSEARCH_JAVA_OPTS:--Xms512m -Xmx512m}" ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 ports: - "${OPENSEARCH_PORT:-9200}:9200" - "${OPENSEARCH_PERF_PORT:-9600}:9600" volumes: - opensearch_data:/usr/share/opensearch/data healthcheck: # porta interna fixa (9200) — ${OPENSEARCH_PORT} e a porta do HOST, nao vale aqui dentro test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"] interval: 15s timeout: 5s retries: 10 start_period: 40s networks: - nyx_net # ── Cerbos: autorizacao ABAC. FastAPI consulta antes de liberar documento ── cerbos: image: ghcr.io/cerbos/cerbos:0.40.0 # ponytail: pinned. Bump quando quiser versao mais nova. container_name: nyx-cerbos restart: unless-stopped command: ["server", "--config=/config/config.yaml"] ports: - "${CERBOS_HTTP_PORT:-3592}:3592" # REST/HTTP - "${CERBOS_GRPC_PORT:-3593}:3593" # gRPC volumes: - ./infra/cerbos/config:/config:ro - ./infra/cerbos/policies:/policies:ro healthcheck: # imagem distroless (sem shell) -> CMD com path absoluto do binario test: ["CMD", "/cerbos", "healthcheck", "--config=/config/config.yaml"] interval: 15s timeout: 5s retries: 10 start_period: 15s networks: - nyx_net # ── backend-api: FastAPI (Fase 2). Gatekeeper REST + ABAC. NAO faz proxy de WS ── backend-api: build: ./backend-api container_name: nyx-backend-api restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy opensearch: condition: service_healthy cerbos: condition: service_healthy environment: DATABASE_URL: mysql+aiomysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE} CERBOS_HTTP_URL: http://cerbos:3592 JWT_SECRET: ${JWT_SECRET} JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-60} HOCUSPOCUS_WEBHOOK_SECRET: ${HOCUSPOCUS_WEBHOOK_SECRET} INTERNAL_API_KEY: ${INTERNAL_API_KEY} OPENSEARCH_URL: http://opensearch:9200 EMBEDDINGS_PROVIDER: ${EMBEDDINGS_PROVIDER:-local} LLM_PROVIDER: ${LLM_PROVIDER:-claude} LLM_MODEL: ${LLM_MODEL:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_API_BASE_URL: ${OPENAI_API_BASE_URL:-https://api.openai.com/v1} OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173} ports: - "${BACKEND_PORT:-8000}:8000" healthcheck: # python nativo (stdlib) — sem curl na imagem; porta interna fixa 8000 test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=3)"] interval: 10s timeout: 5s retries: 10 start_period: 40s # migrations rodam no entrypoint antes do uvicorn volumes: - ./backend-api:/app # bind-mount p/ hot-reload em dev (--reload) - hf_cache:/root/.cache/huggingface # cache do modelo de embeddings (Fase 4) networks: - nyx_net - proxy-net # ── realtime-server: Hocuspocus (Fase 3). Via Expressa WS/CRDT. WS vai DIRETO aqui ── realtime-server: build: ./realtime-server container_name: nyx-realtime restart: unless-stopped depends_on: redis: condition: service_healthy backend-api: condition: service_healthy environment: HOCUSPOCUS_PORT: "1234" REDIS_HOST: redis REDIS_PORT: "6379" FASTAPI_INTERNAL_URL: http://backend-api:8000 WEBHOOK_URL: http://backend-api:8000/api/webhooks/hocuspocus INTERNAL_API_KEY: ${INTERNAL_API_KEY} HOCUSPOCUS_WEBHOOK_SECRET: ${HOCUSPOCUS_WEBHOOK_SECRET} ports: - "${HOCUSPOCUS_PORT:-1234}:1234" healthcheck: # node nativo (fetch global do Node 20) — sem curl na imagem; porta interna fixa 1234 test: ["CMD", "node", "-e", "fetch('http://localhost:1234').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 10s timeout: 5s retries: 5 start_period: 20s volumes: - ./realtime-server:/app - /app/node_modules # preserva node_modules do container (bind-mount nao mascara) command: ["npm", "run", "dev"] networks: - nyx_net - proxy-net # ── frontend: React + Vite + Tiptap + Y.js (Fase 5). Browser conecta WS DIRETO ao Node ── frontend: build: ./frontend container_name: nyx-frontend restart: unless-stopped depends_on: backend-api: condition: service_healthy realtime-server: condition: service_healthy environment: # URLs lidas pelo browser (host) -> portas expostas, nao nomes de servico VITE_API_URL: ${VITE_API_URL:-https://colabora-api.melky.tech} VITE_WS_URL: ${VITE_WS_URL:-wss://colabora-ws.melky.tech} CHOKIDAR_USEPOLLING: "true" ports: - "${FRONTEND_PORT:-5173}:5173" healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:5173').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 10s timeout: 5s retries: 5 start_period: 20s volumes: - ./frontend:/app - /app/node_modules networks: - nyx_net - proxy-net volumes: mysql_data: redis_data: opensearch_data: hf_cache: networks: nyx_net: driver: bridge proxy-net: external: true