Despliegue con Dokploy
Todo el sistema se publica en un VPS con Dokploy, como tres aplicaciones Compose independientes más un servicio PostgreSQL creado en Dokploy (externo al Compose de la API, como en los demás proyectos). La imagen de cada aplicación se construye durante el despliegue; no hace falta compilar nada localmente.
| Aplicación | Repositorio | Servicios del Compose | Puerto interno | Verificación |
|---|---|---|---|---|
| Base de datos | (servicio Database → PostgreSQL de Dokploy) | — | 5432 | — |
| API | ganadero-api | migrate (one-shot), api | 3000 | GET /health/ready |
| Web | ganadero-web | web (nginx) | 80 | abrir el dominio |
| Manual | manual-ganadero | manual-ganadero (nginx, Basic Auth opcional) | 80 | GET /healthz |
Los pasos de la API y de la web, y las variables de entorno de cada una, están en Despliegue de la aplicación y Variables de entorno. Esta página resume la base de datos de la API y trata en detalle el manual.
La base de datos de la API
La API usa un PostgreSQL externo creado en Dokploy y una sola variable, DATABASE_URL, que comparten el paso de migraciones (migrate) y la API.
DATABASE_URL y listoUse la conexión del usuario que creó la base en Dokploy (el Database User del servicio PostgreSQL, que es superusuario). No hay que ejecutar SQL ni crear roles a mano.
Qué ocurre al desplegar:
migratese conecta con ese usuario, crea (si no existe) el rol sin privilegiosganadero_appy aplica el esquema. Es idempotente: redesplegar no repite nada.- La API ejecuta cada transacción como
ganadero_app(SET LOCAL ROLE ganadero_app): ese rol no es superusuario, no tieneBYPASSRLSni es dueño de las tablas, así que las políticas que aíslan a cada organización siempre aplican. Al arrancar lo confirma en el log:modo de privilegios reducidos: las consultas corren como ganadero_app. - Si no puede reducir privilegios de forma verificada (por ejemplo,
ganadero_apptuvieraBYPASSRLS), la API se niega a arrancar con un errorFATAL; nunca atiende peticiones sin aislamiento.
La variable usa como host el nombre interno del servicio PostgreSQL en Dokploy (Internal Host en su ficha) y el puerto 5432. Si el usuario o la clave tienen caracteres especiales, codifíquelos en la URL (@ → %40, : → %3A, / → %2F):
DATABASE_URL=postgres://<usuario>:<clave>@<nombre-interno-del-postgres>:5432/<base>
# ejemplo con un usuario que es un correo electrónico:
DATABASE_URL=postgres://ana%40correo.com:<clave>@ganadero-db-abc123:5432/ganadero
Se recomienda PostgreSQL 15 o superior (probado con 17).
La conexión sigue autenticada como superusuario: una inyección SQL que lograra ejecutar RESET ROLE dentro de una transacción recuperaría esos privilegios. La API no construye SQL dinámico (todas las consultas van parametrizadas), lo que mitiga ese riesgo. Para eliminarlo por completo existe la opción avanzada de dos roles.
Opción avanzada: modo endurecido de dos roles
La API se conecta directamente como ganadero_app y nunca conoce una credencial privilegiada; el dueño de la base solo migra. Requiere ejecutar una vez, como superusuario (claves URL-safe: openssl rand -hex 24):
CREATE ROLE ganadero LOGIN PASSWORD '<clave_owner>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
CREATE ROLE ganadero_app LOGIN PASSWORD '<clave_app>' NOSUPERUSER NOBYPASSRLS;
CREATE DATABASE ganadero OWNER ganadero;
GRANT CONNECT ON DATABASE ganadero TO ganadero_app;
y definir en Environment:
DATABASE_URL=postgres://ganadero_app:<clave_app>@<nombre-interno-del-postgres>:5432/ganadero
DATABASE_MIGRATION_URL=postgres://ganadero:<clave_owner>@<nombre-interno-del-postgres>:5432/ganadero
Las instalaciones hechas con el procedimiento anterior (rol dueño ganadero creado a mano y DATABASE_URL con ese rol) siguen funcionando sin cambios.
El manual
Cómo está construido
Dockerfilemulti-etapa:node:20-alpineejecutanpm ciynpm run build; el resultado estático se copia anginx:1.27-alpine.docker-entrypoint.d/40-manual-auth.shse ejecuta en cada arranque: genera/etc/nginx/conf.d/default.conf(con/healthzy el cacheo de estáticos) y, siMANUAL_AUTH_ENABLED=true, el archivo/etc/nginx/.htpasswdpara HTTP Basic Auth a partir de las variablesMANUAL_*./healthzqueda sin autenticación para el healthcheck.docker-compose.ymlpublica el servicio en la red externadokploy-network.
Pasos
-
Suba el repositorio a GitHub (
https://github.com/ltorresvidal1/SGanaderoM.git). -
En Dokploy cree una aplicación de tipo Compose (o Application con build por Dockerfile) apuntando al repositorio y a la rama de despliegue.
-
En la pestaña de variables de entorno defina (sin guardarlas en el repositorio):
MANUAL_AUTH_ENABLED=true
MANUAL_AUTH_USER=<usuario>
MANUAL_AUTH_PASSWORD=<clave> -
En Domains asigne el dominio y apunte al puerto 80 del contenedor; Traefik gestiona el certificado.
-
Despliegue. Verifique
https://<dominio>/healthz, que respondeok.
Actualizaciones
Cada push a la rama configurada (con autodeploy activado) o un redeploy manual reconstruye la imagen.
Problemas frecuentes
| Síntoma | Causa probable |
|---|---|
network dokploy-network declared as external, but could not be found | La red no existe en el VPS: créela (docker network create dokploy-network) o use la que Dokploy creó. |
| El cambio de credenciales no surte efecto | Redeploy del contenedor: se regeneran en cada arranque; limpie el almacenamiento local del navegador. |
| El build falla por enlaces rotos | onBrokenLinks está en throw: corrija el enlace indicado en el log. |
La API termina con FATAL: no existe el rol "ganadero_app" | No corrieron las migraciones con esa DATABASE_URL: redespliegue (el servicio migrate crea el rol). |
migrate termina con FATAL: no existe el rol "ganadero_app" | El usuario de DATABASE_URL no es superusuario ni tiene CREATEROLE: use el usuario que creó la base en Dokploy o ejecute la sentencia que indica el mensaje. |
La API termina con FATAL: ... no se pudo reducir privilegios a "ganadero_app" | El usuario se salta las políticas (superusuario o BYPASSRLS) y no puede cambiar a ganadero_app: redespliegue para que migrate otorgue la membresía, o ejecute el GRANT que indica el mensaje. |
La API termina con FATAL: el rol "ganadero_app" tiene SUPERUSER o BYPASSRLS | Alguien modificó el rol: ALTER ROLE ganadero_app NOSUPERUSER NOBYPASSRLS; como superusuario. |
Usuario o clave con @, : o / y la API no conecta | Codifique esos caracteres en la DATABASE_URL (@ → %40, : → %3A, / → %2F). |
| La API no arranca en producción | Con MAIL_DRIVER=smtp faltan SMTP_HOST o MAIL_FROM, o JWT_ACCESS_SECRET tiene menos de 32 caracteres. |
| El Compose de la API falla al desplegar | Falta una variable obligatoria (DATABASE_URL, JWT_ACCESS_SECRET, WEB_ORIGIN). |
| La API arranca pero nadie recibe los correos | MAIL_DRIVER está en console (valor por defecto; el log muestra correos deshabilitados (MAIL_DRIVER=console)): configure smtp. |
| Todos los intentos de inicio de sesión se bloquean a la vez | Falta TRUST_PROXY_HOPS=1 detrás de Traefik: el límite se aplica a la IP del proxy. |