Skip to main content

Deploy (prod y dev)

El gateway corre en dos ambientes con mecanismos distintos: prod en un droplet dedicado vía GitHub Actions, y dev en Dokploy. Ambos usan la imagen ya construida en ghcr (el build con el paquete privado de ajolote lo hace el CI, con github_token de GitHub Actions — nunca Dokploy).

Prod — droplet (GitHub Actions)

.github/workflows/deploy.yml: en push a main, tras el CI, buildea y pushea ghcr.io/cuatro-quinas/proteus-gateway:{latest,<sha>}, entra a la tailnet, y por SSH al droplet (tag:proteus) corre docker-compose.droplet.yml:

  1. docker pull de la imagen.
  2. Migra primero: docker compose run --rm api pnpm migration:up (con las réplicas viejas aún sirviendo).
  3. docker compose up -d.

El .env de prod (secretos) se configura a mano una vez en el droplet, junto con los certs de nginx. Ver el encabezado de docker-compose.droplet.yml.

Migrar antes de levantar

El orden es migrar → up. Así el código nuevo nunca corre contra un schema sin migrar, y si la migración falla, aborta el deploy dejando la versión vieja sirviendo. Requiere migraciones backward-compatible (aditivas).

Dev — Dokploy

Vive en el droplet dokploy-dev (100.64.188.115, en la tailnet). Dokploy no buildea: pullea la imagen :dev que el CI construye.

Flujo

push a dev
→ CI (.github/workflows/deploy-dev.yml): build & push ghcr :dev (token vía GitHub Actions)
→ CI: connect Tailscale → POST /api/application.deploy (x-api-key)
→ Dokploy: pull :dev (pull_policy: always) + redeploy (api + worker)

Piezas en el repo

  • dokploy/docker-compose.dokploy.ymlapi + worker desde la misma imagen (ghcr.io/cuatro-quinas/proteus-gateway:${GATEWAY_IMAGE_TAG:-latest}), en dokploy-network, con pull_policy: always.
  • .github/workflows/deploy-dev.yml — build :dev + Tailscale + trigger por API.
  • dokploy/.env.dokploy.example — set de variables.

Config en Dokploy

  • App tipo Application (source docker-compose) apuntando a dokploy/docker-compose.dokploy.yml, branch dev.
  • Registry credential de ghcr (Settings → Registry: ghcr.io + usuario + PAT read:packages) para pullear la imagen privada.
  • Environment (todo por nombre de contenedor sobre dokploy-network, puertos internos):
    • GATEWAY_IMAGE_TAG=dev
    • DATABASE_URL=postgres://…@databases-postgresqldb-…:5432/proteus_gateway_dev
    • DATABASE_SSL=false — el Postgres interno de Dokploy no tiene SSL.
    • REDIS_HOST=databases-redis-…, RABBITMQ_URL=amqp://…@databases-and-services-rabbitmq-…:5672
    • AUTH_SERVICE_URL=http://ajolote-backend-…:3010, PROTEUS_BASE_URL=http://proteus-backend-…:3005
    • PROTEUS_API_KEY = el GATEWAY_INBOUND_API_KEY del backend.
    • NEW_RELIC_ENABLED=false
  • Pre-deploy: pnpm migration:up.
  • El servicio de RabbitMQ debe estar atachado a dokploy-network (se le agrega la red en su compose).

Secrets de GitHub (para deploy-dev.yml)

  • DOKPLOY_API_TOKEN — token de Dokploy (Settings → API).
  • DOKPLOY_APPLICATION_ID — id de la app (de la URL del panel .../services/application/<id>).
  • TS_OAUTH_CLIENT_ID / TS_OAUTH_SECRET — para entrar a la tailnet (compartidos con prod).

Exposición: sólo tailnet

El api se publica atado a la IP de Tailscale, no a 0.0.0.0:

ports:
- "100.64.188.115:3001:3000"

Así no escucha en la IP pública del droplet. Quién puede llegar lo controla la ACL de Tailscale — el grant group:devs → tag:dokploy-dev debe incluir el puerto 3001. Los devs le pegan a http://100.64.188.115:3001/fhir (y /health/live para probar). Encima sigue la auth del gateway (x-api-key).

Gotchas que aparecieron al montarlo
  • ${} en valores de env: en el .env de Dokploy, ${algo} es interpolación de variable. Las credenciales van literales (un RABBITMQ_URL=amqp://${admin}:${pass}@… rompe el deploy).
  • Clone viejo: si el compose desplegado no refleja el último commit, Dokploy no re-pulleó el repo — forzá un Redeploy (el deploy loguea qué commit clona).
  • Imagen :dev cacheada: :dev es mutable; pull_policy: always garantiza bajar la fresca.
  • New Relic: sin NEW_RELIC_APP_NAME tira error y no puede escribir su log (EACCES) → NEW_RELIC_ENABLED=false en dev.

Log de arranque

Tanto api como worker loguean un RuntimeConfig al bootear (ver src/common/log-runtime-config.ts): Proteus (mode + baseUrl), auth, DB (url enmascarada + ssl), redis, rabbit y el puerto. Sirve para ver a qué quedó apuntando el proceso sin exponer secretos. La conectividad real a Proteus/DB la confirma GET /health/live.