Tutorial

Migrar PostgreSQL 15 a 17 en una stack Docker

Bases de datos12 min de lectura5 pasos

PostgreSQL 15 alcanza su fin de vida oficial en noviembre de 2026. Supabase cambió silenciosamente a `postgres:17` en junio de 2026, rompiendo las stacks autoalojadas que no fijaban su versión. Si tu Compose aún corre PG 15, la ventana de migración está abierta — y se cierra. Esta guía desglosa las extensiones incompatibles, documenta los cambios de comportamiento reales entre PG 15 y PG 17, y proporciona un procedimiento de migración sin tiempo de inactividad aplicable a cualquier stack Compose de producción.

Contenido· Por qué migrar ahora — EOL de PG 15 y la señal Supabase1/9
  1. 01Por qué migrar ahora — EOL de PG 15 y la señal Supabase
  2. 02Qué cambia realmente entre PG 15 y PG 17
  3. 03Inventario de extensiones: qué pasa y qué se rompe
  4. 04Procedimiento de migración sin tiempo de inactividad: pg_dump/pg_restore
  5. 05El caso Supabase: postgres:17 sin advertencia
  6. 06Estrategias de migración: pg_dump/restore vs pg_upgrade vs replicación lógica
  7. 07Resolución de problemas: errores frecuentes y sus causas
  8. 08Checklist de conmutación — validar en orden antes de cada paso
  9. 09Gestionar la migración en un portfolio de clientes

Por qué migrar ahora — EOL de PG 15 y la señal Supabase

PostgreSQL 15 alcanza su fin de vida oficial en noviembre de 2026. Después de esa fecha, el PostgreSQL Global Development Group no publicará más correcciones de seguridad ni parches de errores.

Pero la migración no puede esperar hasta noviembre por una razón más inmediata: Supabase cambió su imagen de referencia a postgres:17 en junio de 2026 (discusión #46080 del changelog público). Las stacks que referenciaban image: supabase/postgres sin tag de versión recibieron PG 17 en el siguiente docker compose pull, sin advertencia visible en los logs de inicio.

La verificación se hace con un comando:

docker exec <container> psql -U postgres -c 'SELECT version();'

No asumas — verifica.

Qué cambia realmente entre PG 15 y PG 17

  • search_path reforzado desde PG 15.1 (ADV-2022-00007): el esquema public ya no está en el search_path por defecto para roles no-superuser. Cualquier consulta que asumiera public.mi_tabla puede devolver silenciosamente 0 filas en lugar de un error.
  • pg_dump produce dumps incompatibles hacia abajo: un dump de PG 17 no puede restaurarse en PG 15. Lo inverso sí es posible. Conserva los dumps de PG 15 durante al menos 30 días después del cambio.
  • wal_level predeterminado elevado a logical en PG 16+: si tu postgresql.conf forzaba wal_level = minimal, el comportamiento cambia tras la migración.
  • Eliminación de funciones obsoletas: lo_import, lo_export y ciertas funciones de pg_catalog eliminadas o renombradas entre PG 15 y PG 17 — verificar las funciones usadas en extensiones propias.
  • pg_stat_statements modifica la normalización de consultas: los dashboards de Grafana/PgHero que agregan por query fingerprint verán sus series reiniciadas tras la migración.
  • pg_partman (gestión de particionamiento) requiere versión ≥ 5.x para PG 17 — la versión 4.x no es compatible.
  • timescaledb requiere versión ≥ 2.13 para PG 17; las versiones anteriores rechazan cargar y bloquean el inicio del contenedor.

Inventario de extensiones: qué pasa y qué se rompe

Antes de cualquier migración, extrae la lista de extensiones activas en cada base de datos:

docker exec <pg15_container> psql -U postgres -c \
  "SELECT datname, extname, extversion FROM pg_extension e JOIN pg_database d ON d.oid = e.extnamespace ORDER BY datname, extname;"

Compatibles sin acción: pgcrypto, uuid-ossp, hstore, ltree, citext, pg_trgm, unaccent.

Que requieren actualización: pgvector (≥ 0.7.0 para PG 17), PostGIS (≥ 3.4), TimescaleDB (≥ 2.13 obligatorio), pg_partman (≥ 5.0 obligatorio).

Comando de verificación post-migración:

docker exec <pg17_container> psql -U postgres -d mydb \
  -c 'SELECT extname, extversion FROM pg_extension ORDER BY extname;'

Procedimiento de migración sin tiempo de inactividad: pg_dump/pg_restore

  1. Fijar la versión de la imagen PG 15

    Antes de cualquier operación, fija la imagen actual en tu docker-compose.yml con su tag exacto:

    docker inspect <pg15_container> --format '{{.Config.Image}}'
    # ej: postgres:15.7

    Confirma este cambio en tu VCS antes de continuar. La migración no puede hacerse en el mismo volumen: PG 17 rechaza iniciar en un PGDATA de PG 15.

  2. Capturar el dump global y los dumps de bases de datos

    Exporta primero los objetos globales (roles, tablespaces), luego cada base de datos individualmente:

    docker exec <pg15_container> pg_dumpall \
      -U postgres \
      --globals-only \
      > backup_globals.sql
    
    docker exec <pg15_container> pg_dump \
      -U postgres \
      --no-owner \
      --no-acl \
      --format=custom \
      --file=/tmp/mydb_pg15.dump \
      mydb
    
    docker cp <pg15_container>:/tmp/mydb_pg15.dump ./mydb_pg15.dump

    Verifica la integridad del dump:

    pg_restore --list mydb_pg15.dump | head -20
  3. Iniciar el contenedor PG 17 en paralelo en un puerto diferente

    Añade un segundo servicio en docker-compose.yml para PG 17, en un puerto distinto (ej. 5433), con un volumen de datos nuevo:

      postgres17:
        image: postgres:17
        environment:
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: ${POSTGRES_DB}
        volumes:
          - pg17_data:/var/lib/postgresql/data
        ports:
          - "5433:5432"
        shm_size: 256mb
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
          interval: 5s
          timeout: 3s
          retries: 5
    
    volumes:
      pg17_data:

    Inicia solo este servicio:

    docker compose up -d postgres17
    docker compose exec postgres17 pg_isready
  4. Restaurar en PG 17 y verificar extensiones

    Restaura los objetos globales primero, luego la base de datos:

    docker exec -i <pg17_container> psql -U postgres < backup_globals.sql
    
    docker cp mydb_pg15.dump <pg17_container>:/tmp/mydb_pg15.dump
    
    docker exec <pg17_container> pg_restore \
      -U postgres \
      --no-owner \
      --no-acl \
      -d mydb \
      /tmp/mydb_pg15.dump

    Si pg_restore reporta errores en extensiones:

    docker exec <pg17_container> psql -U postgres -d mydb \
      -c 'CREATE EXTENSION IF NOT EXISTS pgvector;'
  5. Cambiar las aplicaciones y validar

    Pon tus aplicaciones en modo mantenimiento o solo-lectura, luego actualiza la variable DATABASE_URL de cada servicio para apuntar al contenedor PG 17 en el puerto 5432. Reinicia los servicios de aplicación:

    curl -sf http://localhost/api/health | jq .database

    Si la validación pasa, detén el contenedor PG 15 y remapea el puerto 5432 a postgres17.

search_path es el cambio silencioso más frecuente. Desde PG 15.1, el esquema public ya no está en el search_path predeterminado para roles no-superuser. Si tus migraciones Flyway, Liquibase o seeds Artisan fallan con relation "X" does not exist tras la migración, añade SET search_path TO public, "$user"; al inicio de sesión o cualifica explícitamente tus tablas.

El caso Supabase: postgres:17 sin advertencia

La discusión #46080 del changelog público de Supabase (github.com/orgs/supabase/discussions/46080) documenta el cambio de imagen de referencia a postgres:17 ocurrido en junio de 2026. Las stacks autoalojadas que referenciaban image: supabase/postgres sin tag de versión recibieron PG 17 en el siguiente docker compose pull, sin migración automática de datos.

Comportamiento observado: el contenedor PG 17 inicia, rechaza leer el PGDATA de PG 15 (formato incompatible), y se detiene inmediatamente. Los logs del contenedor muestran:

docker logs <supabase_db_container> 2>&1 | head -20
# FATAL: database files are incompatible with server
# DETAIL: The data directory was initialized by PostgreSQL version 15, which is not compatible with this version 17.

La mejor práctica para cualquier despliegue Supabase autoalojado es fijar el tag a una versión menor precisa en tu docker-compose.yml:

  db:
    image: supabase/postgres:15.8.1.040
    # o la última 17.x una vez completada la migración
    # image: supabase/postgres:17.4.1.016

Estrategias de migración: pg_dump/restore vs pg_upgrade vs replicación lógica

Desplace la tabla

EstrategiaTiempo de inactividadComplejidad
pg_dump / pg_restore (esta guía)5-30 min según el volumenBaja — herramientas nativas, reproducible
pg_upgrade en el lugar1-5 min (actualización binaria rápida)Alta — requiere PG 15 y PG 17 simultáneamente, difícil en Docker
Replicación lógica (zero-downtime real)< 1 min (conmutación en línea)Muy alta — requiere `wal_level = logical`, slots de replicación

Resolución de problemas: errores frecuentes y sus causas

Los errores más comunes durante una migración PG 15 → 17 en Docker:

FATAL: database files are incompatible with server
Causa: el contenedor PG 17 inició en el mismo volumen que PG 15. Solución: usa siempre un volumen nuevo para PG 17 y restaura via pg_restore.

ERROR: extension "timescaledb" is not available (o pg_partman, pg_cron)
Causa: la extensión no está compilada para PG 17 en la imagen oficial postgres:17. Solución: usa una imagen derivada que incluya las extensiones requeridas (ej. timescale/timescaledb:latest-pg17).

ERROR: role "X" already exists al restaurar globales
Solución: usa --if-not-exists o filtra las líneas CREATE ROLE postgres en backup_globals.sql antes de restaurar.

ERROR: relation "public.X" does not exist en aplicaciones
Solución: añade options=-csearch_path=public a la cadena de conexión, o ejecuta ALTER ROLE app_user SET search_path = 'public'; tras la restauración.

pg_restore: error: invalid byte sequence for encoding "UTF8"
Causa: datos codificados en LATIN1 en PG 15 y la instancia PG 17 fue inicializada en UTF8. Solución: restaura en una instancia PG 17 inicializada con POSTGRES_INITDB_ARGS: --encoding=LATIN1.

Checklist de conmutación — validar en orden antes de cada paso

  • Antes de comenzar: lista de extensiones activas extraída (pg_extension), versión PG 15 fijada en Compose, dump completo validado (pg_restore --list sin errores).
  • Tras restaurar en PG 17: todas las extensiones presentes en la versión correcta, search_path verificado via rol de aplicación (no superuser), recuentos de filas comparados en las 5 tablas críticas.
  • Antes del cambio de aplicación: ventana de mantenimiento anunciada, modo solo-lectura activado si es posible, último dump de coherencia capturado desde PG 15.
  • Tras el cambio de aplicación: endpoint /health devuelve 200 con database: ok, logs de aplicación sin errores relation does not exist.
  • Retención PG 15: volumen PG 15 conservado 30 días mínimo, dump conservado 90 días, procedimiento de rollback documentado.
  • Post-migración: ANALYZE VERBOSE; ejecutado en todas las bases, autovacuum confirmado activo, monitoreo actualizado para PG 17.

Gestionar la migración en un portfolio de clientes

Para una agencia que gestiona múltiples entornos de clientes, la migración PG 15 → 17 no es un evento único — es un trabajo a planificar por portfolio.

Inventariar primero. El siguiente comando lista todas las versiones de PostgreSQL en ejecución en un VPS que aloja múltiples proyectos Compose:

docker ps --format '{{.Names}}' | xargs -I{} sh -c \
  'docker exec {} psql -U postgres -qtAX -c "SELECT current_setting(\"server_version\")" 2>/dev/null && echo " <- {}"'

Priorizar por riesgo: las stacks con extensiones compiladas a medida o timescaledb/pg_partman necesitan una imagen derivada probada antes del cambio.

Estandarizar la imagen: definir una imagen común en un registry interno que incluya las extensiones validadas por la agencia. Todos los proyectos Compose del portfolio apuntan a esta imagen.

Las copias de seguridad diarias incluidas en los planes Agency de ServOrbit proporcionan una red de seguridad para cada migración: si una stack de cliente muestra comportamiento inesperado en las 24 horas siguientes al cambio, la restauración parte de la copia de seguridad del día anterior.

Migración planificada, portfolio bajo control

Las agencias que gestionan múltiples stacks de clientes no pueden permitirse descubrir una interrupción la noche de una actualización de versión automática. ServOrbit centraliza la gestión de VPS de portfolios de clientes — infraestructura controlada, actualizaciones planificadas, copias de seguridad diarias incluidas en los planes Agency.

¿Necesita ayuda?

Consulte nuestro centro de ayuda y nuestra FAQ, o contacte con nuestro equipo: llamada, WhatsApp o correo electrónico. Soporte en francés, inglés y árabe.

Escribir por WhatsAppse abre en una pestaña nueva