Ir al contenido
Q
QuoteNode

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:

RutaCuándo usarlaMecanismo
A — CLI del hostPuedes ejecutar docker en el host (Ubuntu VPS, LAN Linux, Compose simple)scripts/migrate-postgres-major.sh
B — stack solo-UISolo 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 situimagen 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)

  1. Haz copia de .env. Contiene secretos que no se pueden regenerar, especialmente DB_ENCRYPTION_KEY.
  2. 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
  3. Pausa el auto-despliegue si tu plataforma redespliega con un tag móvil (p. ej. auto-deploy de Coolify, :latest de Portainer). Mira Orden de publicación — una imagen PG18 no debe llegar antes de ejecutar la migración.
  4. 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.

  1. Encuentra el nombre del volumen de datos:
    docker volume ls | grep postgres
  2. Detén el stack (conserva los volúmenes):
    docker compose down
  3. 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:
    DB_NAME=quotenode DB_USERNAME=quotenode DB_PASSWORD='...' \
    POSTGRES_VOLUME=<your_postgres_volume> \
    DB_MAJOR_MIGRATE_CONFIRM=yes \
      ./scripts/migrate-postgres-major.sh
    El script no hace nada destructivo sin DB_MAJOR_MIGRATE_CONFIRM=yes y aborta (dejando tus datos intactos) si encuentra roles/extensiones inesperados o espacio en disco insuficiente.
  4. Arranca el stack normalmente. Subirá en PostgreSQL 18 y aplicará las migraciones pendientes:
    docker compose up -d
  5. 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_ARGS coincidente (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 postgres entra 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 postgres para 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 contenedor postgres de Coolify; detenerlo no basta.
  • Tras migrar la base, recupera el stack con «Redeploy» — no «Restart». Como el contenedor postgres se eliminó, Coolify debe recrearlo. Pulsa Redeploy en el recurso; Coolify arranca entonces postgres:18-alpine sobre 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:

  1. cold-backup — un tar inmutable del volumen A (imagen de rollback), tomado antes de que nada lea A.
  2. dump — un preflight fail-closed del clúster en marcha + un dump lógico.
  3. restore — restauración en un volumen PostgreSQL 18 nuevo B.
  4. verify — comprueba la versión mayor, el esquema, las extensiones y la paridad de locale, y escribe REPORT.txt en el volumen de artefactos.

Pasos

  1. Detén el stack de la aplicación en la UI de tu plataforma (no borres volúmenes).

  2. 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.
  3. Despliega el stack de migración con estas variables de entorno:

    VariableValor
    SOURCE_VOLUME_NAMEel volumen existente A del paso 2
    DB_NAME DB_USERNAME DB_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
  4. Lee el informe. Cuando la fase verify termine, lee REPORT.txt desde el volumen de artefactos (quotenode_pg_upgrade_artifacts). Debe decir PASS. 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).

  5. 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 external por 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 por docker-compose.upgrade.yml dentro 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_upgrade es 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, el PGDATA=/var/lib/postgresql/data legacy, sin healthcheck y restart: no, y fija la imagen por digest.

Pasos

  1. Detén el stack y haz una copia cruda fuera del host del volumen A — es tu único rollback:
    docker run --rm -v <your_postgres_volume>:/d:ro -v "$PWD:/out" alpine:3.23 \
      tar czf /out/pg16-volume-backup.tar.gz -C /d .
    Copia pg16-volume-backup.tar.gz fuera del host.
  2. Ejecuta el archivo one-shot listo contra tu volumen en marcha (SOURCE_VOLUME_NAME = volumen A):
    SOURCE_VOLUME_NAME=<your_postgres_volume> \
    DB_NAME=quotenode DB_USERNAME=quotenode DB_PASSWORD='...' \
      docker compose -f infra/docker-compose.pgautoupgrade.unsafe.yml up
    Espera a que el contenedor termine con exit 0.
  3. 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
  4. 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).
  5. 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.yml en 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_NAME a B:
    POSTGRES_VOLUME_NAME=quotenode_postgres_upgraded \
      docker compose -f infra/docker-compose.coolify.yml \
                     -f infra/docker-compose.promote.yml up -d
    (usa docker-compose.prod.yml en lugar de coolify.yml para el archivo prod). Para revertir, quita -f docker-compose.promote.yml y arranca de nuevo — vuelves a A.
  • UI de Coolify / Portainer: en la UI no hay encadenamiento -f. En su lugar, remapea el almacenamiento del servicio postgres al volumen B en los ajustes de almacenamiento de la plataforma, o añade external: true + name: <B> para el volumen de postgres en el editor compose del stack.

Verificar

Una vez que el stack principal funciona sobre B:

  1. Abre /health — debe devolver UP.
  2. Inicia sesión y confirma que tus datos existentes (presupuestos, clientes, ajustes) están presentes.
  3. Crea un presupuesto de prueba y genera un PDF.
  4. 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:

  1. Pausa el auto-despliegue / fija el tag de imagen actual.
  2. Ejecuta la migración (ruta A o B) y promociona B.
  3. 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_NAME o 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.

Last reviewed: Recently