Installation
Migrar los datos a PostgreSQL 18
Procedimiento único y seguro para los datos para mover un despliegue PostgreSQL 16 existente a PostgreSQL 18 sin pérdida de datos.
Migrar los datos a PostgreSQL 18
Quién lo necesita: solo los despliegues que ya funcionan en PostgreSQL 16 y quieren pasar a una versión de QuoteNode que requiere PostgreSQL 18. Las instalaciones nuevas no necesitan nada — arrancan directamente en PostgreSQL 18.
Un servidor PostgreSQL 18 no puede arrancar sobre un directorio de datos de PostgreSQL 16. La aplicación se niega a arrancar sobre un directorio incompatible y nunca lo reescribe automáticamente, así que una instalación existente debe avanzar sus datos una vez. Es un procedimiento de operador deliberado y único.
La migración es segura para los datos por diseño: tu volumen de datos existente (llamémoslo A) nunca se borra, recrea ni sobrescribe. Los datos se copian a un nuevo volumen PostgreSQL 18 (B); A permanece intacto como rollback inmediato hasta que confirmes que B funciona.
Hay tres rutas (la ruta C es un último recurso desaconsejado). Elige según tengas o no shell en el host:
| Ruta | Cuándo usarla | Mecanismo |
|---|---|---|
| A — CLI del host | Puedes ejecutar docker en el host (Ubuntu VPS, LAN Linux, Compose simple) | scripts/migrate-postgres-major.sh |
| B — stack solo-UI | Solo tienes una UI (Coolify, Portainer, QNAP, Synology) | infra/docker-compose.upgrade.yml |
| C — auto-actualización in situ ⚠️ | Solo último recurso; ni A ni B funciona y aceptas el riesgo in situ | imagen pgautoupgrade (no recomendado) |
Las rutas A y B son seguras para los datos: copian a un volumen nuevo y dejan el original intacto. La ruta C actualiza in situ — no hay rollback intacto, solo tu copia de seguridad fuera del host. Prefiere A o B.
Antes de empezar (todas las rutas)
- Haz copia de
.env. Contiene secretos que no se pueden regenerar, especialmenteDB_ENCRYPTION_KEY. - Haz una copia de la base de datos (además de la copia de seguridad automática que hace la migración):
docker compose exec postgres pg_dump -U quotenode quotenode > backup-before-pg18.sql - Pausa el auto-despliegue si tu plataforma redespliega con un tag móvil (p. ej. auto-deploy de
Coolify,
:latestde Portainer). Mira Orden de publicación — una imagen PG18 no debe llegar antes de ejecutar la migración. - Detén el stack de la aplicación (sin borrar volúmenes). Nunca ejecutes
docker compose down -v.
Ruta A — CLI del host (recomendada si tienes shell)
Es la ruta más simple para Ubuntu VPS, LAN Linux y cualquier host Docker Compose simple.
- Encuentra el nombre del volumen de datos:
docker volume ls | grep postgres - Detén el stack (conserva los volúmenes):
docker compose down - Ejecuta la migración. Captura dos artefactos de seguridad (un dump lógico y un tarball crudo del
volumen) y verifica ambos antes de tocar nada, ejecuta un preflight fail-closed en el clúster en
marcha y luego recrea el volumen en PostgreSQL 18 y restaura:
El script no hace nada destructivo sinDB_NAME=quotenode DB_USERNAME=quotenode DB_PASSWORD='...' \ POSTGRES_VOLUME=<your_postgres_volume> \ DB_MAJOR_MIGRATE_CONFIRM=yes \ ./scripts/migrate-postgres-major.shDB_MAJOR_MIGRATE_CONFIRM=yesy aborta (dejando tus datos intactos) si encuentra roles/extensiones inesperados o espacio en disco insuficiente. - Arranca el stack normalmente. Subirá en PostgreSQL 18 y aplicará las migraciones pendientes:
docker compose up -d - Haz la Verificación y luego borra los artefactos de seguridad que imprime el script cuando tengas confianza.
Locale no predeterminado: si tu base de datos de origen usa un locale distinto al del predeterminado de la imagen, el script falla en fail-closed en la comprobación de encoding/collation (un cambio silencioso de collation puede corromper índices de texto). Reejecuta con un
POSTGRES_INITDB_ARGScoincidente (p. ej.--locale=...).
Notas para Coolify (hosts gestionados)
La ruta A ejecutada por SSH en el servidor Coolify es la vía más simple en un host Coolify. Algunas particularidades de Coolify, confirmadas en la práctica:
- Un despliegue «Failed» antes de migrar es lo esperado. Cuando Coolify despliega una versión que
requiere PostgreSQL 18 sobre un volumen PostgreSQL 16 existente, el contenedor
postgresentra en bucle de fallos (FATAL: database files are incompatible with server) y Coolify marca el despliegue como Failed. Tus datos no se tocan — PostgreSQL simplemente se niega a arrancar sobre un directorio de datos más antiguo. - Debes eliminar el contenedor
postgrespara migrar. La ruta A recrea el volumen de datos, y Docker se niega a eliminar un volumen mientras cualquier contenedor — incluso detenido — lo siga referenciando. Por eso el procedimiento elimina primero el contenedorpostgresde Coolify; detenerlo no basta. - Tras migrar la base, recupera el stack con «Redeploy» — no «Restart». Como el contenedor
postgresse eliminó, Coolify debe recrearlo. Pulsa Redeploy en el recurso; Coolify arranca entoncespostgres:18-alpinesobre el volumen migrado y recupera la app. - Coolify reutiliza el volumen migrado. Recrear el volumen pierde las etiquetas Compose de Coolify, pero Docker Compose (y por tanto el redeploy de Coolify) empareja volúmenes por nombre y reutiliza el existente — tus datos permanecen. Conserva los artefactos de seguridad hasta verificar la app redesplegada.
- Encuentra el nombre real del volumen en la pestaña Storages del recurso; tiene prefijo UUID, p. ej.
<uuid>_postgres-prod-data(fíjate en los guiones). Si ejecutas varios stacks (p. ej. una instalación dev y una main) en un host, migra cada uno por su propio nombre de volumen y no toques el otro.
Ruta B — stack de migración solo-UI (Coolify, Portainer, NAS)
Cuando no tienes shell en el host, usa el stack de migración dedicado y temporal
infra/docker-compose.upgrade.yml. No forma parte de ningún despliegue normal — levántalo solo para la
migración y elimínalo después. Usa imágenes PostgreSQL estándar, sin socket de Docker, y se controla por
completo mediante los entrypoints de los contenedores.
Ejecuta estas fases automáticamente y se detiene en un informe verificado — nunca promociona por sí mismo:
- cold-backup — un tar inmutable del volumen A (imagen de rollback), tomado antes de que nada lea A.
- dump — un preflight fail-closed del clúster en marcha + un dump lógico.
- restore — restauración en un volumen PostgreSQL 18 nuevo B.
- verify — comprueba la versión mayor, el esquema, las extensiones y la paridad de locale, y escribe
REPORT.txten el volumen de artefactos.
Pasos
-
Detén el stack de la aplicación en la UI de tu plataforma (no borres volúmenes).
-
Encuentra el nombre del volumen de datos existente (es A, usado como
SOURCE_VOLUME_NAME):- Coolify: el volumen lleva el prefijo del UUID del recurso, p. ej.
<uuid>_postgres_prod_data. - Portainer / QNAP / Synology: el prefijo es el nombre del stack, p. ej.
<stack>_postgres_data. - Predeterminado de Compose:
<project>_postgres_prod_data.
- Coolify: el volumen lleva el prefijo del UUID del recurso, p. ej.
-
Despliega el stack de migración con estas variables de entorno:
Variable Valor SOURCE_VOLUME_NAMEel volumen existente A del paso 2 DB_NAMEDB_USERNAMEDB_PASSWORDtus credenciales de base de datos existentes TARGET_VOLUME_NAMEopcional; por defecto quotenode_postgres_upgraded(este es B)# equivalente en Docker puro de lo que ejecuta la UI: docker compose -f infra/docker-compose.upgrade.yml up -
Lee el informe. Cuando la fase
verifytermine, leeREPORT.txtdesde el volumen de artefactos (quotenode_pg_upgrade_artifacts). Debe decirPASS. Si alguna fase falla, el stack se detiene de forma segura; A queda intacto — corrige la causa y reintenta (borra B primero, mira Recuperación). -
Elimina el stack de migración (conserva sus volúmenes) y luego Promover B.
Visibilidad de volúmenes según la plataforma (verifícalo en la tuya): la ruta B asume que el stack de migración puede montar el volumen A existente de la aplicación como volumen
externalpor nombre. La mayoría de hosts Docker lo permiten. Si tu plataforma aísla los volúmenes por stack/recurso y el stack de migración no ve A, usa la variante in situ: reemplaza temporalmente el compose de la aplicación pordocker-compose.upgrade.ymldentro del mismo recurso/proyecto para que herede el mismo namespace de volúmenes. Confírmalo en tu plataforma concreta antes de confiar en ello en producción.
Ruta C — imagen de auto-actualización in situ (parece la más sencilla, pero arriesgada — no recomendada) ⚠️
Solo último recurso. Úsalo solo cuando ni la ruta A ni la B sean viables y aceptes los riesgos de abajo. A diferencia de A y B, esta ruta actualiza in situ sobre tu volumen A existente, así que no hay volumen de rollback intacto — tu copia de seguridad fuera del host es la única red de seguridad.
La idea parece la más sencilla: deja que una imagen de auto-actualización
(pgautoupgrade) ejecute pg_upgrade una vez sobre
tu volumen existente y luego arranca el postgres:18-alpine normal sobre él. Pero no es un cambio de tag
de una línea en tu compose normal — el servicio postgres tiene allí un override command: (que suprime
por completo el entrypoint de actualización), restart: unless-stopped (que repetiría en bucle el one-shot
terminado) y un healthcheck (que marca el servidor en actualización como unhealthy). Los tres rompen una
actualización in situ.
El repo incluye un archivo one-shot listo e independiente que los evita todos:
infra/docker-compose.pgautoupgrade.unsafe.yml (el .unsafe es deliberado). Datos clave:
pg_upgradees destructivo in situ — si tiene éxito el clúster antiguo desaparece, y un fallo a medio camino puede dejar el directorio de datos medio convertido. Tu copia fuera del host es el único rollback.- El archivo mantiene
PGAUTO_ONESHOT=yes, elPGDATA=/var/lib/postgresql/datalegacy, sin healthcheck yrestart: no, y fija la imagen por digest.
Pasos
- Detén el stack y haz una copia cruda fuera del host del volumen A — es tu único rollback:
Copiadocker run --rm -v <your_postgres_volume>:/d:ro -v "$PWD:/out" alpine:3.23 \ tar czf /out/pg16-volume-backup.tar.gz -C /d .pg16-volume-backup.tar.gzfuera del host. - Ejecuta el archivo one-shot listo contra tu volumen en marcha (
SOURCE_VOLUME_NAME= volumen A):
Espera a que el contenedor termine con exit 0.SOURCE_VOLUME_NAME=<your_postgres_volume> \ DB_NAME=quotenode DB_USERNAME=quotenode DB_PASSWORD='...' \ docker compose -f infra/docker-compose.pgautoupgrade.unsafe.yml up - Verifica el layout — el volumen ahora debe ser PostgreSQL 18 en la ruta legacy:
docker run --rm -v <your_postgres_volume>:/d:ro alpine:3.23 cat /d/PG_VERSION # -> 18 - Arranca tu stack normal (
postgres:18-alpine). Usa el mismo volumen, ahora actualizado — no se necesita paso de promoción (la actualización fue in situ). - Haz la Verificación. Si algo va mal, restaura A desde
pg16-volume-backup.tar.gz.
Plataformas solo-UI (Coolify/Portainer): pega el contenido de
infra/docker-compose.pgautoupgrade.unsafe.ymlen un stack temporal nuevo, define las variables de arriba, despliega una vez, confirma que salió, elimina ese stack y luego apunta tu stack normal al mismo volumen.
Promover B
Tras la verificación, apunta el stack principal al volumen migrado B. Los archivos compose base nunca se editan (para que las instalaciones nuevas no se vean afectadas); la promoción es un override.
- Docker / Compose puro: añade el override de promoción y define
POSTGRES_VOLUME_NAMEa B:
(usaPOSTGRES_VOLUME_NAME=quotenode_postgres_upgraded \ docker compose -f infra/docker-compose.coolify.yml \ -f infra/docker-compose.promote.yml up -ddocker-compose.prod.ymlen lugar decoolify.ymlpara el archivo prod). Para revertir, quita-f docker-compose.promote.ymly arranca de nuevo — vuelves a A. - UI de Coolify / Portainer: en la UI no hay encadenamiento
-f. En su lugar, remapea el almacenamiento del serviciopostgresal volumen B en los ajustes de almacenamiento de la plataforma, o añadeexternal: true+name: <B>para el volumen de postgres en el editor compose del stack.
Verificar
Una vez que el stack principal funciona sobre B:
- Abre
/health— debe devolverUP. - Inicia sesión y confirma que tus datos existentes (presupuestos, clientes, ajustes) están presentes.
- Crea un presupuesto de prueba y genera un PDF.
- Abre un enlace público para confirmar el enrutamiento.
Solo después de esto debes borrar el volumen A y los artefactos de migración.
Orden de publicación (importante)
Si tu plataforma despliega automáticamente con un tag móvil, una imagen que requiere PG18 podría llegar antes de tu migración y romper el stack. Siempre:
- Pausa el auto-despliegue / fija el tag de imagen actual.
- Ejecuta la migración (ruta A o B) y promociona B.
- Solo entonces actualiza la aplicación a la versión PostgreSQL 18 y reactiva el auto-despliegue.
Recuperación
El volumen de origen A nunca se modifica, así que la recuperación es simple:
- Migración interrumpida antes de promover (timeout, redeploy, parada): borra el volumen B y el volumen de artefactos, y empieza de nuevo. No hay estado parcial que reanudar.
- Problema tras promover: revierte la promoción (quita el override / vacía
POSTGRES_VOLUME_NAMEo reapunta el almacenamiento a A) y arranca el stack — vuelves a PostgreSQL 16 con todos los datos. - Último recurso: restaura desde
backup-before-pg18.sql(mira Actualizar QuoteNode → Revertir).
Nunca ejecutes docker compose down -v durante este procedimiento — borra volúmenes.