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).
| Momento | Qué corre |
|---|---|
| PR | CI: pnpm lint + pnpm build (bloqueantes) y typecheck informativo |
Push/merge a dev | CI + despliegue del entorno dev |
Push/merge a main | CI + 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
| Variable | Ejemplo | Para qué |
|---|---|---|
DOKPLOY_DEPLOY_ENABLED | true | Enciende el deploy de producción |
NUXT_APP_URL | https://tudominio.com | baseUrl de i18n (build-arg) |
HEALTHCHECK_URL | https://tudominio.com/api/health | Verifica el rollout |
DOKPLOY_DEV_DEPLOY_ENABLED | true | Enciende el deploy del entorno dev |
DEV_NUXT_APP_URL | https://dev.tudominio.com | baseUrl de i18n de dev (build-arg) |
DEV_HEALTHCHECK_URL | https://dev.tudominio.com/api/health | Verifica el rollout de dev |
Secretos
| Secret | Para qué |
|---|---|
DOKPLOY_WEBHOOK_URL | Webhook de la app de producción en Dokploy |
PRODUCTION_DATABASE_URL | Postgres alcanzable desde GitHub para migrar (ver nota) |
DOKPLOY_DEV_WEBHOOK_URL | Webhook de la app dev |
DEV_DATABASE_URL | Migraciones de la base de dev |
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.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:
- Base de datos: una rama (branch) de Neon para
dev, creada desde la principal. Su connection string seráDEV_DATABASE_URL. - Aplicación en Dokploy: Source → Docker con la imagen
ghcr.io/<owner>/foodstack:dev, puerto del contenedor3000. Health check en/api/health. - Variables de la app dev:
NUXT_DATABASE_URL(rama dev),BETTER_AUTH_SECRETpropio yNUXT_APP_URLapuntando al dominio de dev. Better Auth usa esa URL como baseURL: magic links, invitaciones y callbacks deben apuntar ahí. - Dominio: agrega el host en Dokploy (HTTPS con Let's Encrypt) y crea un registro DNS
Aapuntando a la IP del servidor (oCNAMEal dominio raíz). - Automatización: pega el webhook en el secreto
DOKPLOY_DEV_WEBHOOK_URLy enciendeDOKPLOY_DEV_DEPLOY_ENABLED. - E2E remoto (opcional):
E2E_ENABLED=true+E2E_BASE_URLy los secretosE2E_EMAIL/E2E_PASSWORDde una cuentaownerde 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
./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.
| Ruta | Cómo se sirve |
|---|---|
/ | Estática (HTML + _payload.json), sin coste SSR |
/docs + /docs/** | Estática, una por archivo Markdown |
| Todo lo demás | SSR en node .output/server/index.mjs |
- La lista vive en
routeRules(nuxt.config.ts):'/','/_payload.json','/docs'y'/docs/**'conprerender: true, y un catch-all'/**': { prerender: false }que mantiene al crawler fuera de/appy/auth. - Agregar una sección no requiere tocar la config: el crawler (
crawlLinks) descubre los archivos nuevos decontent/docs/desde los enlaces del índice. - El build pasa sin
.env(CI no tiene secretos): auth, colas y Resend se protegen conimport.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.jsonen.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:
- Expand: agrega (columna nullable, tabla nueva, índice
CONCURRENTLY). - Migrar código: despliega la versión que escribe ambos formatos o que usa lo nuevo.
- Backfill: rellena datos con un script o una tarea, fuera del deploy.
- 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_dumpen la imagen: lo instala elDockerfile(postgresql-client-17desde el repo PGDG); debe ser una versión ≥ a la del servidor PostgreSQL.- Conexión directa: si
NUXT_DATABASE_URLes la pooled de Neon (-pooler), el dump deriva la conexión directa automáticamente; también se puede fijarNUXT_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/:devson 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.