Desplegar en Dokploy
De una VPS vacía al sistema funcionando en producción, paso a paso y sin saltarse nada.
Esta guía lleva de una VPS con Dokploy recién instalado a Royal Clean CRM sirviendo en su dominio, con HTTPS, respaldos y mantenimiento automático.
Se puede seguir de principio a fin sin conocer el proyecto. Cada paso dice qué hacer, qué esperar y cómo comprobar que salió bien antes de pasar al siguiente.
Lo que vas a montar
Cuatro piezas, y sólo una de ellas mira a Internet directamente:
| Pieza | Qué es | Expuesta |
|---|---|---|
| Aplicación | El CRM. Imagen Docker construida del repositorio | Sí, por su dominio |
| Documentación | Este sitio. Segundo servicio, independiente | Sí, por su dominio |
| PostgreSQL 17 | La base de datos | No. Sólo red interna |
| Cloudflare R2 | Las fotografías | No. Bucket privado |
PostgreSQL no se publica nunca
Si Dokploy te ofrece exponer el puerto de la base, déjalo desactivado. La aplicación la alcanza por la red interna de Docker; nadie más tiene por qué poder hacerlo.
Antes de empezar
Ten esto a mano. Si falta algo, consíguelo primero: a mitad de la guía es peor.
- Una VPS con Dokploy instalado y accesible.
- Un dominio apuntando a la IP de la VPS. Por ejemplo
crm.royalcleancr.com, y opcionalmentedocs.royalcleancr.com. - Acceso al repositorio privado en GitHub.
- Una cuenta de Cloudflare para el bucket de R2.
- Un gestor de contraseñas donde guardar los secretos que vas a generar.
Comprueba el DNS antes de nada
dig +short crm.royalcleancr.comDebe devolver la IP de tu VPS. Si todavía no propagó, el certificado HTTPS fallará más adelante y tendrás que rehacer ese paso.
1 · Crear el proyecto
Entra a Dokploy.
Projects → Create Project.
Nombre: royal-clean.
Todo lo demás vivirá dentro de este proyecto, que es lo que hace que los servicios se vean entre sí por su nombre.
2 · Crear la base de datos
Crea el servicio
Create Service → Database → PostgreSQL.
| Campo | Valor |
|---|---|
| Name | royalclean-db |
| Version | 17 |
| Database | royalclean |
| User | royalclean |
| Password | Genera una larga y guárdala |
Genera la contraseña
openssl rand -base64 32Guárdala en tu gestor de contraseñas antes de continuar. La vas a necesitar en el paso 5 y no querrás regenerarla entonces.
Despliega y anota el host interno
Pulsa Deploy. Cuando termine, anota el host interno que muestra Dokploy
—normalmente el propio nombre del servicio, royalclean-db—. Es lo que irá en
DATABASE_URL.
3 · Crear el bucket de R2
En el panel de Cloudflare:
El bucket
R2 → Create bucket
| Campo | Valor |
|---|---|
| Name | royal-clean-evidence |
| Location | Automatic |
Deja el acceso público desactivado. El bucket tiene que ser privado: las fotografías se sirven a través de la aplicación, que comprueba permisos en cada petición.
El token de API
Manage R2 API Tokens → Create API Token
| Campo | Valor |
|---|---|
| Permissions | Object Read & Write |
| Bucket | Sólo royal-clean-evidence |
Acotarlo a un solo bucket importa: si ese token se filtra, no da acceso al resto de tu cuenta de Cloudflare.
Guarda las tres cosas
- Access Key ID
- Secret Access Key — no se vuelve a mostrar
- Account ID — aparece en la barra lateral de R2
4 · Crear la aplicación
Create Service → Application.
| Campo | Valor |
|---|---|
| Provider | GitHub (autoriza Dokploy la primera vez) |
| Repository | El repositorio privado de Royal Clean |
| Branch | main |
| Build Type | Dockerfile |
| Dockerfile Path | Dockerfile |
No pulses Deploy todavía: sin variables de entorno, el contenedor arrancará y se detendrá.
5 · Variables de entorno
En Environment, pega esto y sustituye los valores en mayúsculas:
NODE_ENV=production
APP_URL=https://crm.royalcleancr.com
DATABASE_URL=postgresql://royalclean:LA_CONTRASENA@royalclean-db:5432/royalclean?schema=public
AUTH_SECRET=PEGA_AQUI_48_BYTES_ALEATORIOS
R2_ACCOUNT_ID=TU_ACCOUNT_ID
R2_ACCESS_KEY_ID=TU_ACCESS_KEY
R2_SECRET_ACCESS_KEY=TU_SECRET_KEY
R2_BUCKET_NAME=royal-clean-evidence
JOBS_SECRET=PEGA_AQUI_32_BYTES_ALEATORIOS
BUSINESS_TIMEZONE=America/Costa_RicaGenera los dos secretos:
openssl rand -base64 48openssl rand -hex 32`AUTH_SECRET` tiene un mínimo
Al menos 32 caracteres. La aplicación se niega a arrancar si es más corto, y lo dice con claridad en los registros en lugar de arrancar con una seguridad menor de la esperada.
El detalle de cada variable, qué pasa si falta y cuáles son opcionales está en Variables de entorno.
6 · Dominio y HTTPS
Domains → Add Domain
| Campo | Valor |
|---|---|
| Host | crm.royalcleancr.com |
| Port | 3000 |
| HTTPS | Activado |
| Certificate | Let's Encrypt |
Dokploy emite y renueva el certificado a través de Traefik.
Si el DNS todavía no apunta a la VPS, la validación falla y Let's Encrypt limita
los reintentos. Comprueba el dig de más arriba antes de pulsar.
7 · Healthcheck
Advanced → Health Check
| Campo | Valor |
|---|---|
| Path | /api/health |
| Interval | 30 s |
| Timeout | 5 s |
| Retries | 3 |
| Start period | 40 s |
El margen de arranque da tiempo a que se apliquen las migraciones antes de la primera comprobación. Sin él, el orquestador daría por muerto un contenedor que sólo estaba migrando.
La imagen ya trae su propio HEALTHCHECK; configurarlo también aquí es lo que
evita que Traefik enrute tráfico a un contenedor que aún no está listo.
8 · Primer despliegue
Pulsa Deploy y sigue los registros. Deberías ver, en este orden:
→ Aplicando migraciones pendientes…
All migrations have been successfully applied.
→ Iniciando Royal Clean CRM…
▲ Next.js 16.3.3
✓ ReadyComprueba desde tu equipo:
curl https://crm.royalcleancr.com/api/health{"status":"ok","database":"ok","latencyMs":12}Si responde `degraded`
La aplicación arrancó pero no alcanza la base. Casi siempre es DATABASE_URL:
revisa que el host sea el nombre interno del servicio —no localhost— y que
el puerto sea 5432, que es el interno del contenedor, no el 5433 que se usa en
desarrollo.
9 · Crear el primer administrador
La semilla de desarrollo se niega a ejecutarse en producción, así que el primer administrador se crea a mano, una sola vez.
Abre una terminal en el contenedor de la aplicación desde Dokploy (Application → Terminal) y ejecuta:
node scripts/create-admin.mjs --username admin.principal --email admin@tu-dominio.comTe pedirá la contraseña por entrada estándar, sin mostrarla, y la repetirá para confirmar. No se acepta por argumento a propósito: un argumento queda en el historial del intérprete de comandos y en la lista de procesos de la máquina.
La cuenta se crea obligada a cambiar la contraseña en el primer acceso, así que la que tecleas aquí no sobrevive al primer día.
La cuenta que administra la aplicación
Añade --super para dar además superadministración: gestionar las cuentas
de todos —crearlas, restablecer contraseñas, desactivar accesos y cerrar
sesiones—. Es una atribución para quien administra el sistema, no para quien
administra el negocio.
node scripts/create-admin.mjs --username admin.principal --email admin@tu-dominio.com --superA partir de aquí, desde la aplicación
El resto de las cuentas se crean desde el panel de administración. La cuenta con superadministración las gestiona todas: crearlas, restablecer contraseñas, desactivarlas y cerrar sesiones.
10 · Programar el mantenimiento
El trabajo de mantenimiento aplica la retención de las fotografías, limpia subidas abandonadas y marca los servicios que llevan demasiado tiempo abiertos.
Es idempotente: ejecutarlo dos veces seguidas no hace daño.
Es la opción recomendada: queda dentro del proyecto y se ve en el panel.
Create Service → Cron Job (o Schedules, según la versión).
Schedule: 0 3 * * * — todos los días a las 3:00.
Command:
curl -fsS -X POST -H "Authorization: Bearer EL_JOBS_SECRET" \
https://crm.royalcleancr.com/api/jobs/runEjecútalo una vez a mano para comprobarlo. Debe devolver un JSON con los
recuentos y "errors": [].
Sin el token responde 401, y sin JOBS_SECRET configurado responde 503
en lugar de quedar abierto.
11 · Respaldos
Una copia en la misma VPS no es un respaldo
Si se pierde el servidor, se pierde con él. El respaldo tiene que salir de ahí.
Configura una copia diaria de PostgreSQL hacia un destino externo —R2 sirve— y verifica una restauración cada cierto tiempo. Un respaldo que nunca se ha restaurado es una suposición, no un respaldo.
El procedimiento completo está en Operación diaria.
12 · La documentación
Este sitio se despliega como un segundo servicio del mismo proyecto, con su propio Dockerfile y su propio dominio. No comparte base de datos ni secretos con la aplicación.
Los pasos están en Desplegar la documentación.
Verificación final
No des el despliegue por terminado hasta que las diez líneas estén marcadas.
-
https://crm.royalcleancr.com/api/healthresponde"status":"ok" - El certificado HTTPS es válido y no da aviso en el navegador
- Se puede iniciar sesión con el administrador inicial
- Se fuerza el cambio de contraseña en ese primer ingreso
- PostgreSQL no responde desde fuera de la VPS
- Se puede crear un cliente, una propiedad y un servicio
- Una fotografía sube y se ve después (confirma que R2 está bien)
-
POST /api/jobs/runsin token responde 401 - El mantenimiento está programado y se ejecutó una vez a mano
- El respaldo diario está configurado y se probó una restauración
Para comprobar que la base no está expuesta, desde tu equipo:
nc -vz LA_IP_DE_LA_VPS 5432Debe fallar. Si conecta, la base está publicada y hay que corregirlo antes de seguir.