Despliegue
Topología
VPS único — firewall solo ssh/http/https, todo bind a 127.0.0.1
Cloudflare — deploy manual, sin CI en ningún caso
Pages, vía dashboard
Pages, vía dashboard
Worker,
npm run deploypetid-api
Único repo con CI/CD de verdad: push a main → SSH al VPS →docker compose down && up -d --build.
NOTIFICATIONS_TENANT_ID(el UUID del tenant"petid-system") está hardcodeado directo en el YAML del workflow, no es un secret de GitHub — si buscás de dónde sale, no lo vas a encontrar en Settings → Secrets.ENABLE_CRONes una variable de repo (no un secret) — se pasa por alto fácil si solo se audita la lista de secrets al revisar qué está configurado.- La validación de env es estricta, no silenciosa: un
superRefineexigeNOTIFICATIONS_BASE_URL/_API_KEY/_TENANT_ID/STAFF_WEB_URL/OWNER_WEB_URLen cuantoEMAIL_DRIVER=http(el caso de prod) — si falta alguna, el proceso ni arranca. - La única variable realmente opcional-con-consecuencia:
FCM_CLEANER_WEBHOOK_SECRET. Si falta, no rompe el boot — el guard del webhook de tokens muertos simplemente devuelve401siempre, con mensaje explícito. petid-redises una instancia separada denotification-redis— mismo VPS, dos Redis distintos, no compartidos.
petid-crm / petid-admin
Cero rastro de deploy en ninguno de los dos repos — sin.github/workflows, sin wrangler.*, sin Dockerfile. Si buscás un workflow file acá y no lo encontrás, no es que se perdió: nunca existió.
Confirmado directamente (no inferible del código): están deployados igual, a mano, vía el dashboard de Cloudflare Pages.
petid-landing
Cloudflare Worker (no Pages) vía @astrojs/cloudflare, con SSR real — no es un sitio estático. Deploy 100% manual: npm run deploy = astro build && wrangler deploy. Sin CI.
- Un deploy hoy rompería producción:
wrangler.jsonctienePUBLIC_API_URLapuntando ahttps://staging-api.petid.example/v1— dominio placeholder, no el real. - Sin
route/dominio custom enwrangler.jsonc— unwrangler deploytal cual cae en un*.workers.devpor default salvo que el binding esté seteado a mano en el dashboard.
petid-client
Sin deploy real todavía.
eas.jsontiene los 3 profiles (development/preview/production) apuntando a dominios placeholder (petid.example,staging-api.petid.example,api.petid.example).app.jsonno tieneextra.eas.projectId—eas init/eas loginnunca se corrieron.- Lo único probado hasta ahora es
expo run:androidlocal (ver push-token-registration).
dev-identity-server
No tiene wiring con ningún app de petid (client/api/crm/admin/landing) — no lo confundas con parte de este ecosistema todavía. La única conexión real es indirecta, vía notification-api: IDENTITY_JWT_SECRET/IDENTITY_SERVER_URL protegen el dashboard de Bull Board de notification-api, con BULL_BOARD_PASSWORD como fallback si dev-identity-server no está disponible.
Pasos
Runbook de cero para petid-api + notification-stack, con Nginx como reverse proxy en el propio VPS y TLS por Let's Encrypt. No cubre petid-crm/admin/landing (Cloudflare, deploy manual sin runbook formal — ver arriba) ni petid-client (sin deploy real todavía).
Preparar el VPS (una sola vez, compartido por ambos stacks)
Instalar Docker (docs oficiales). Crear los registros DNS en Cloudflare antes de llegar a certbot:
| Tipo | Nombre | Contenido | Proxy |
|---|---|---|---|
A | api | <IP del VPS> | DNS only (nube gris) |
A | notificaciones | <IP del VPS> | DNS only (nube gris) |
Nube gris, no naranja, al menos para el primer certbot — con el proxy de Cloudflare activo, certbot nunca llega a validar el reto HTTP-01 contra tu Nginx. Se puede reactivar después de emitido el certificado.
Desplegar petid-api
git clone <url-petid-api> && cd petid-api
cp .env.example .envFijos en producción: NODE_ENV=production, DATABASE_URL real, CORS_ORIGINS con los dominios reales de Cloudflare Pages (no localhost). REDIS_URL/NOTIFICATIONS_BASE_URL apuntan al nombre del servicio dentro del propio compose (redis, notification-api), no a una URL pública.
Generar secretos — 5 valores distintos, no reusar el mismo:
openssl rand -base64 48 # → JWT_ACCESS_SECRET
openssl rand -base64 48 # → JWT_REFRESH_SECRET
openssl rand -base64 48 # → JWT_RESET_SECRET (token stateless de reset, sin tabla en BD)
openssl rand -base64 48 # → JWT_VERIFY_SECRET (token stateless de verificación de email)
openssl rand -base64 48 # → JWT_MFA_SECRET (challengeToken del login en dos pasos)
openssl rand -base64 32 # → TWO_FACTOR_ENC_KEY (AES, debe decodificar a exactamente 32 bytes)docker compose up -d --buildMigrar petid-api
Desde tu máquina o CI (la imagen final del Dockerfile nunca instala pnpm, no se puede correr con docker compose exec):
DATABASE_URL="<prod>" pnpm prisma:deployVerificar: curl http://127.0.0.1:$PORT/health desde el VPS.
Desplegar notification-stack
git clone <url-notification-api>
git clone <url-notification-worker>
git clone <url-notification-stack>
cp notification-api/.env.example notification-api/.env
cp notification-worker/.env.example notification-worker/.envSecretos que deben coincidir EXACTO entre repos — la causa más común de que algo falle en este paso:
NOTIFICATIONS_API_KEY: petid-api ↔ notification-apiFCM_CLEANER_WEBHOOK_SECRET: petid-api ↔ notification-workerPROVIDER_ENCRYPTION_KEY: notification-api ↔ notification-worker —openssl rand -base64 32, mismo valor pegado en los dos.envIDENTITY_JWT_SECRET: notification-api ↔ dev-identity-server (solo si se usa —BULL_BOARD_PASSWORDes el fallback sin dev-identity-server)
En ambos: REDIS_URL=redis://notification-redis:6379 (nombre del servicio en el propio compose). Solo notification-api corre migraciones — es la dueña del schema; el worker lo copia en build time desde su contexto.
cd notification-stack && ./deploy.sh
DATABASE_URL="<prod de notification-api>" pnpm prisma:deployNginx + Let's Encrypt
server {
listen 80;
server_name api.tudominio.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Mismo patrón para notification-api (su propio puerto, server_name notificaciones.tudominio.com). notification-worker no necesita site — no expone HTTP.
sudo ln -s /etc/nginx/sites-available/petid-api /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/notification-api /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.tudominio.com
sudo certbot --nginx -d notificaciones.tudominio.comSi certbot da Failed authorization procedure o timeout: confirmar con dig que ambos dominios resuelven a la IP del VPS — no a una IP de Cloudflare — antes de reintentar (volver al paso 1, nube gris).
Verificación end-to-end
curl https://api.tudominio.com/health
curl https://notificaciones.tudominio.com/health
docker network inspect main-network # confirma los 5 contenedores conectadosDesde petid-crm/petid-admin (Cloudflare Pages, ya deployados a mano), confirmar que las llamadas a api.tudominio.com pasan CORS y que un flujo que dispare notificación (ej. reset password) llega a notificaciones.tudominio.com internamente. petid-client no aplica todavía — sin deploy real (ver arriba).
logger-dozzle (opcional) — logs en vivo sin SSH por contenedor
Repo aparte, mismo VPS (comparte main-network). Un solo contenedor que lee el socket de Docker del host y muestra logs de todos los contenedores del VPS vía UI web — petid-api, notification-api, notification-worker, ambos Redis — no solo los que están en main-network con él.
git clone <url-logger-dozzle> && cd logger-dozzle
cp .env.example .env # ajustar DOZZLE_PORT si hace falta
docker compose up -dExposición pública (fuera del repo, se gestiona en el VPS como el resto):
- Subdominio nuevo (p. ej.
logs.tudominio.com) apuntando al VPS. - Vhost de Nginx con
proxy_pass http://127.0.0.1:$DOZZLE_PORT;+ certbot. - Basic Auth (
htpasswd) delante, obligatorio — ver abajo.
El socket de Docker montado (/var/run/docker.sock) es acceso root-equivalente al host — este contenedor puede controlar todos los demás. Por eso el puerto va solo a 127.0.0.1 (nunca 0.0.0.0) y la exposición pública real pasa siempre por Nginx con Basic Auth como mínimo, nunca el puerto directo.