Saltar al contenido principal

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ónRepositorioServicios del ComposePuerto internoVerificación
Base de datos(servicio Database → PostgreSQL de Dokploy)—5432—
APIganadero-apimigrate (one-shot), api3000GET /health/ready
Webganadero-webweb (nginx)80abrir el dominio
Manualmanual-ganaderomanual-ganadero (nginx, Basic Auth opcional)80GET /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.

Ponga su DATABASE_URL y listo

Use 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:

  1. migrate se conecta con ese usuario, crea (si no existe) el rol sin privilegios ganadero_app y aplica el esquema. Es idempotente: redesplegar no repite nada.
  2. La API ejecuta cada transacción como ganadero_app (SET LOCAL ROLE ganadero_app): ese rol no es superusuario, no tiene BYPASSRLS ni 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.
  3. Si no puede reducir privilegios de forma verificada (por ejemplo, ganadero_app tuviera BYPASSRLS), la API se niega a arrancar con un error FATAL; 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).

Riesgo residual

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​

  • Dockerfile multi-etapa: node:20-alpine ejecuta npm ci y npm run build; el resultado estático se copia a nginx:1.27-alpine.
  • docker-entrypoint.d/40-manual-auth.sh se ejecuta en cada arranque: genera /etc/nginx/conf.d/default.conf (con /healthz y el cacheo de estáticos) y, si MANUAL_AUTH_ENABLED=true, el archivo /etc/nginx/.htpasswd para HTTP Basic Auth a partir de las variables MANUAL_*. /healthz queda sin autenticación para el healthcheck.
  • docker-compose.yml publica el servicio en la red externa dokploy-network.

Pasos​

  1. Suba el repositorio a GitHub (https://github.com/ltorresvidal1/SGanaderoM.git).

  2. En Dokploy cree una aplicación de tipo Compose (o Application con build por Dockerfile) apuntando al repositorio y a la rama de despliegue.

  3. 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>
  4. En Domains asigne el dominio y apunte al puerto 80 del contenedor; Traefik gestiona el certificado.

  5. Despliegue. Verifique https://<dominio>/healthz, que responde ok.

Actualizaciones​

Cada push a la rama configurada (con autodeploy activado) o un redeploy manual reconstruye la imagen.

Problemas frecuentes​

SíntomaCausa probable
network dokploy-network declared as external, but could not be foundLa 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 efectoRedeploy del contenedor: se regeneran en cada arranque; limpie el almacenamiento local del navegador.
El build falla por enlaces rotosonBrokenLinks 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 BYPASSRLSAlguien modificó el rol: ALTER ROLE ganadero_app NOSUPERUSER NOBYPASSRLS; como superusuario.
Usuario o clave con @, : o / y la API no conectaCodifique esos caracteres en la DATABASE_URL (@ → %40, : → %3A, / → %2F).
La API no arranca en producciónCon 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 desplegarFalta una variable obligatoria (DATABASE_URL, JWT_ACCESS_SECRET, WEB_ORIGIN).
La API arranca pero nadie recibe los correosMAIL_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 vezFalta TRUST_PROXY_HOPS=1 detrás de Traefik: el límite se aplica a la IP del proxy.