Deploy y CI/CD

El pipeline completo (hooks, CI, Dokploy), los dos entornos y las reglas de migración y rollback.

El despliegue corre sobre Dokploy, disparado por GitHub Actions después de un CI en verde. Hay dos entornos: producción (rama main) y dev (rama dev, con su propio dominio y su propia base de datos).

MomentoQué corre
PRCI: pnpm lint + pnpm build (bloqueantes) y typecheck informativo
Push/merge a devCI + despliegue del entorno dev
Push/merge a mainCI + despliegue de producción con health check
feature/* · fix/* ──push──▶ PR ──▶ merge a dev ──▶ CI ──▶ Deploy (dev) ──▶ https://dev.tudominio.com
                                                     │
                                       (cuando dev está estable)
                                                     ▼
                                    PR release: dev ──▶ main ──▶ CI ──▶ Deploy (prod) ──▶ health check

Activar el despliegue

Los workflows vienen apagados; se encienden con variables y secretos del repositorio (Settings → Secrets and variables → Actions).

Variables

VariableEjemploPara qué
DOKPLOY_DEPLOY_ENABLEDtrueEnciende el deploy de producción
NUXT_APP_URLhttps://tudominio.combaseUrl de i18n (build-arg)
HEALTHCHECK_URLhttps://tudominio.com/api/healthVerifica el rollout
DOKPLOY_DEV_DEPLOY_ENABLEDtrueEnciende el deploy del entorno dev
DEV_NUXT_APP_URLhttps://dev.tudominio.combaseUrl de i18n de dev (build-arg)
DEV_HEALTHCHECK_URLhttps://dev.tudominio.com/api/healthVerifica el rollout de dev

Secretos

SecretPara qué
DOKPLOY_WEBHOOK_URLWebhook de la app de producción en Dokploy
PRODUCTION_DATABASE_URLPostgres alcanzable desde GitHub para migrar (ver nota)
DOKPLOY_DEV_WEBHOOK_URLWebhook de la app dev
DEV_DATABASE_URLMigraciones de la base de dev
Migraciones. El paso de migraciones corre en el runner: con un Postgres interno de Dokploy hay que publicar su puerto externo (servicio de base de datos → External Credentials) y usar postgresql://<user>:<password>@<ip-del-servidor>:<puerto>/<base>. Esa base queda expuesta a internet: contraseña larga y, si puedes, filtra el puerto por firewall a las IPs de GitHub.
Si la app usa un provider Git en Dokploy, elige un solo mecanismo: o su auto-deploy (al hacer push) o el workflow — con los dos activos, cada merge despliega dos veces. Con provider Docker (imagen de GHCR) no hay conflicto, pero Autodeploy debe quedar activado: es lo que habilita el webhook de despliegue.

Imagen (GHCR)

El build corre en GitHub y publica una imagen en GitHub Container Registry (ghcr.io/<owner>/foodstack): tag :main para producción, :dev para dev y :sha-<commit> inmutable para rollback. Dokploy deja de construir el repo: en cada aplicación, Source → Docker → la imagen del entorno (puerto 3000). Si el paquete es privado, agrega credenciales de registro (PAT con read:packages).

El entorno dev (dev.tudominio.com)

Una segunda aplicación en Dokploy que sirve la rama dev, con base de datos propia. Los pasos completos están en la sección Entorno dev de DEPLOYMENT.md; en resumen:

  1. Base de datos: una rama (branch) de Neon para dev, creada desde la principal. Su connection string será DEV_DATABASE_URL.
  2. Aplicación en Dokploy: Source → Docker con la imagen ghcr.io/<owner>/foodstack:dev, puerto del contenedor 3000. Health check en /api/health.
  3. Variables de la app dev: NUXT_DATABASE_URL (rama dev), BETTER_AUTH_SECRET propio y NUXT_APP_URL apuntando al dominio de dev. Better Auth usa esa URL como baseURL: magic links, invitaciones y callbacks deben apuntar ahí.
  4. Dominio: agrega el host en Dokploy (HTTPS con Let's Encrypt) y crea un registro DNS A apuntando a la IP del servidor (o CNAME al dominio raíz).
  5. Automatización: pega el webhook en el secreto DOKPLOY_DEV_WEBHOOK_URL y enciende DOKPLOY_DEV_DEPLOY_ENABLED.
  6. E2E remoto (opcional): E2E_ENABLED=true + E2E_BASE_URL y los secretos E2E_EMAIL / E2E_PASSWORD de una cuenta owner de dev. La suite escribe y borra datos: apúntala solo a dev.
# Primera vez: crea la rama dev desde un main que ya tenga los workflows
git switch main && git pull
git switch -c dev
git push -u origin dev
Atajo interactivo: ./scripts/setup-dokploy-dev.sh (raíz del repo) recorre este montaje paso a paso — abre Neon y Dokploy, captura los valores, genera el secreto de auth, escribe los secrets/variables en GitHub y compone el bloque de variables listo para pegar en Dokploy.

Variables por entorno

Cada aplicación de Dokploy tiene su propio Environment: la app de dev y la de producción no comparten variables aunque vivan en el mismo proyecto. Hay tres niveles — compartidas del proyecto (${{project.VAR}}), del entorno (${{environment.VAR}}, si tu versión los soporta) y del servicio (las de cada app, que pisan a las anteriores); lo que cambia por entorno va siempre a nivel de servicio.

Guardar cambios en la pestaña Environment no toca el contenedor en marcha: lanza un Deploy después de editar. NUXT_APP_URL además se lee al construir (i18n baseUrl): va como build-arg desde las variables NUXT_APP_URL / DEV_NUXT_APP_URL de GitHub (ver Imagen (GHCR)).

Páginas estáticas (prerender)

Las páginas públicas se generan en el build y se sirven como archivos estáticos desde .output/public; el resto (dashboard, auth, API) sigue renderizándose en el servidor.

RutaCómo se sirve
/Estática (HTML + _payload.json), sin coste SSR
/docs + /docs/**Estática, una por archivo Markdown
Todo lo demásSSR en node .output/server/index.mjs
  • La lista vive en routeRules (nuxt.config.ts): '/', '/_payload.json', '/docs' y '/docs/**' con prerender: true, y un catch-all '/**': { prerender: false } que mantiene al crawler fuera de /app y /auth.
  • Agregar una sección no requiere tocar la config: el crawler (crawlLinks) descubre los archivos nuevos de content/docs/ desde los enlaces del índice.
  • El build pasa sin .env (CI no tiene secretos): auth, colas y Resend se protegen con import.meta.prerender. No hay nada que configurar.
  • El HTML se genera en inglés; el idioma del visitante (cookie i18n_locale) se aplica en el cliente inmediatamente después de la hidratación, sin recargar.
  • Si en el futuro agregas otra página pública, dale su propia regla prerender: true (el catch-all la bloquearía) y verificá que exista su _payload.json en .output/public.
pnpm build                              # 13 rutas prerenderizadas
(nohup node .output/server/index.mjs &) # sirve .output: estáticas + SSR
mv .env .env.ci-bak && pnpm build; mv .env.ci-bak .env   # la prueba de CI, sin secretos

Migraciones (expand/contract)

El paso de migraciones corre antes del rollout, así que durante unos minutos conviven dos versiones del código contra el mismo esquema. Toda migración sigue el patrón expand/contract:

  1. Expand: agrega (columna nullable, tabla nueva, índice CONCURRENTLY).
  2. Migrar código: despliega la versión que escribe ambos formatos o que usa lo nuevo.
  3. Backfill: rellena datos con un script o una tarea, fuera del deploy.
  4. Contract: borra lo viejo (NOT NULL, drop) recién en un deploy posterior.
pnpm db:generate   # después de cambiar el schema
pnpm db:migrate    # lo aplica también el workflow de deploy

Backups de la base de datos

El dashboard admin (/app/admin/dashboard) genera backups manuales: un job de pg-boss corre pg_dump --format=custom y sube el dump al bucket dedicado (NUXT_APP_S3_BACKUP_BUCKET_NAME). Dos requisitos de despliegue:

  • pg_dump en la imagen: lo instala el Dockerfile (postgresql-client-17 desde el repo PGDG); debe ser una versión ≥ a la del servidor PostgreSQL.
  • Conexión directa: si NUXT_DATABASE_URL es la pooled de Neon (-pooler), el dump deriva la conexión directa automáticamente; también se puede fijar NUXT_BACKUP_DATABASE_URL.

Salud y rollback

  • Liveness (/api/health): público y barato, no toca la base de datos. Es el que usan Dokploy y el paso final del workflow.
  • Rollback: los tags :main/:dev son móviles; para volver atrás apunta la app a un :sha-<commit> inmutable y lanza un Deploy. Gracias a expand/contract, la versión anterior del código sigue siendo compatible con el esquema.
  • Salud profunda (/api/admin/system/health): base de datos, Redis, colas y backups, solo para administradores.