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_pathreforzado desde PG 15.1 (ADV-2022-00007): el esquemapublicya no está en elsearch_pathpor defecto para roles no-superuser. Cualquier consulta que asumierapublic.mi_tablapuede devolver silenciosamente 0 filas en lugar de un error.pg_dumpproduce 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_levelpredeterminado elevado alogicalen PG 16+: si tupostgresql.confforzabawal_level = minimal, el comportamiento cambia tras la migración.- Eliminación de funciones obsoletas:
lo_import,lo_exporty ciertas funciones depg_catalogeliminadas o renombradas entre PG 15 y PG 17 — verificar las funciones usadas en extensiones propias. pg_stat_statementsmodifica 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.timescaledbrequiere 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
Fijar la versión de la imagen PG 15
Antes de cualquier operación, fija la imagen actual en tu
docker-compose.ymlcon su tag exacto:docker inspect <pg15_container> --format '{{.Config.Image}}' # ej: postgres:15.7Confirma este cambio en tu VCS antes de continuar. La migración no puede hacerse en el mismo volumen: PG 17 rechaza iniciar en un
PGDATAde PG 15.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.dumpVerifica la integridad del dump:
pg_restore --list mydb_pg15.dump | head -20Iniciar el contenedor PG 17 en paralelo en un puerto diferente
Añade un segundo servicio en
docker-compose.ymlpara 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_isreadyRestaurar 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.dumpSi pg_restore reporta errores en extensiones:
docker exec <pg17_container> psql -U postgres -d mydb \ -c 'CREATE EXTENSION IF NOT EXISTS pgvector;'Cambiar las aplicaciones y validar
Pon tus aplicaciones en modo mantenimiento o solo-lectura, luego actualiza la variable
DATABASE_URLde 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 .databaseSi 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.016Estrategias de migración: pg_dump/restore vs pg_upgrade vs replicación lógica
Desplace la tabla
| Estrategia | Tiempo de inactividad | Complejidad |
|---|---|---|
| pg_dump / pg_restore (esta guía) | 5-30 min según el volumen | Baja — herramientas nativas, reproducible |
| pg_upgrade en el lugar | 1-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 --listsin errores). - Tras restaurar en PG 17: todas las extensiones presentes en la versión correcta,
search_pathverificado 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
/healthdevuelve 200 condatabase: ok, logs de aplicación sin erroresrelation 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,autovacuumconfirmado 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.