Bases de données12 min de lecture

Migrer PostgreSQL 15 vers 17 dans une stack Docker

PostgreSQL 15 atteint sa fin de vie en novembre 2026. Supabase a basculé silencieusement sur `postgres:17` en juin 2026, cassant les stacks auto-hébergées qui ne fixaient pas leur version. Si votre Compose tourne encore sur PG 15, la fenêtre de migration est ouverte — et elle se referme. Ce guide démonte les extensions incompatibles, documente les changements de comportement réels entre PG 15 et PG 17, et donne une procédure de bascule à zéro interruption applicable à n'importe quel Compose de production.

Pourquoi migrer maintenant — PG 15 EOL et le signal Supabase

PostgreSQL 15 entre en fin de vie officielle en novembre 2026. Passé cette date, la PostgreSQL Global Development Group ne publie plus de correctifs de sécurité ni de corrections de bogues : toute vulnérabilité découverte après la date de fin de vie reste sans patch sur votre version.

Mais la migration ne peut plus attendre novembre pour une raison plus immédiate : Supabase a basculé son image de référence sur postgres:17 en juin 2026 (discussion #46080 du changelog public). Les stacks qui référençaient image: supabase/postgres sans tag de version ont reçu PG 17 lors d'un simple docker compose pull, sans avertissement visible dans les logs de démarrage. Certains déploiements auto-hébergés ont découvert la rupture au redémarrage — une extension compilée pour PG 15 refusant de charger sous PG 17.

Si votre stack épingle déjà postgres:15, vous êtes protégé à court terme. Si elle pointe postgres:latest ou un tag flottant de Supabase, elle a peut-être déjà migré sans que vous le sachiez. La vérification se fait en une commande :

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

Le résultat vous indique la version exacte du serveur en cours d'exécution. Ne supposez pas — vérifiez.

Ce qui change réellement entre PG 15 et PG 17

  • search_path durci depuis PG 15.1 (ADV-2022-00007) : le schéma public n'est plus dans le search_path par défaut pour les rôles non-superuser. Toute requête qui supposait public.ma_table au lieu de ma_table qualifié peut échouer silencieusement en renvoyant 0 résultats au lieu d'une erreur.
  • pg_dump produit des dumps incompatibles vers le bas : un dump PG 17 ne se restaure pas sur PG 15. L'inverse est possible, et c'est le sens de la migration. Conserver les dumps PG 15 pendant 30 jours minimum après la bascule.
  • Paramètre wal_level par défaut élevé à logical en PG 16+ : si votre postgresql.conf forçait wal_level = minimal, le comportement change après migration. Les slots de réplication logique existants peuvent devenir invalides.
  • Suppressions de fonctions dépréciées : lo_import, lo_export et certaines fonctions pg_catalog retirées ou rebaptisées entre PG 15 et PG 17 — vérifier les fonctions utilisées dans vos extensions maison.
  • pg_stat_statements modifie la normalisation des requêtes : les dashboards Grafana/PgHero qui agrègent par query fingerprint verront leurs séries repartir de zéro après la migration.
  • L'extension pg_partman (gestion de partitionnement) demande une version ≥ 5.x pour PG 17 — la version 4.x n'est pas compatible.
  • timescaledb exige une version ≥ 2.13 pour PG 17 ; les versions antérieures refusent de charger et bloquent le démarrage du conteneur.

Inventaire des extensions : ce qui passe et ce qui casse

Avant toute migration, extraire la liste des extensions actives dans chaque base :

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;"

Les extensions à vérifier en priorité sur PG 17 :

Extensions compatibles sans action : pgcrypto, uuid-ossp, hstore, ltree, citext, pg_trgm, unaccent, intarray, tablefunc, earthdistance, fuzzystrmatch. Ces extensions sont fournies dans le package postgresql-contrib standard et n'ont pas subi de rupture entre PG 15 et PG 17.

Extensions nécessitant une mise à jour : pgvector (passer à ≥ 0.7.0 pour PG 17), pg_stat_statements (intégré, mais l'extension doit être recréée si shared_preload_libraries change), PostGIS (≥ 3.4 pour PG 17), TimescaleDB (≥ 2.13 obligatoire), pg_partman (≥ 5.0 obligatoire).

Extensions non portées ou à compiler : toute extension compilée maison (.so) contre les headers PG 15 doit être recompilée contre les headers PG 17. Le binaire n'est pas rétrocompatible. Si votre image Docker embarque un tel .so, reconstruire l'image avant la migration.

La commande de vérification post-migration :

docker exec <pg17_container> psql -U postgres -c 'SELECT * FROM pg_available_extensions ORDER BY name;'

Comparez la colonne installed_version avec votre inventaire PG 15 — une extension manquante (NULL) indique un problème de chargement à corriger avant de valider la migration.

Procédure de migration sans interruption : pg_dump/pg_restore

01

Figer la version de l'image PG 15

Avant toute opération, épinglez l'image actuelle dans votre docker-compose.yml avec son digest exact. Cela vous permet de revenir à l'état d'origine en une commande :

docker inspect <pg15_container> --format '{{.Config.Image}}'
# ex : postgres:15.7
# Mettre à jour le Compose :
# image: postgres:15.7

Committez ce changement dans votre VCS avant de poursuivre. La migration ne peut se faire en place : PG 17 refuse de démarrer sur un PGDATA PG 15 (format interne incompatible).

02

Capturer le dump global et les dumps de bases

Exportez d'abord les objets globaux (rôles, tablespaces), puis chaque base individuellement. Les flags --no-owner et --no-acl évitent les erreurs de droits lors de la restauration sur un superuser différent :

# Objets globaux (rôles, tablespaces)
docker exec <pg15_container> pg_dumpall \
  -U postgres \
  --globals-only \
  > backup_globals.sql

# Chaque base applicative
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

Vérifiez l'intégrité du dump avant d'aller plus loin :

pg_restore --list mydb_pg15.dump | head -20

Un dump corrompu ou vide ici signifie que la migration s'arrête — ne passez jamais à l'étape suivante sans avoir validé le dump.

03

Démarrer le conteneur PG 17 en parallèle sur un port différent

Ajoutez un second service dans votre docker-compose.yml pour PG 17, sur un port distinct (ex : 5433), avec un volume de données neuf. Le service PG 15 reste actif pendant toute cette étape — aucune interruption pour vos applications :

  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:

Lancez uniquement ce service :

docker compose up -d postgres17
docker compose exec postgres17 pg_isready
04

Restaurer sur PG 17 et vérifier les extensions

Restaurez les objets globaux, puis la base :

# Rôles et tablespaces
docker exec -i <pg17_container> psql -U postgres < backup_globals.sql

# Restauration de la base
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 signale des erreurs sur des extensions, corrigez-les avant de continuer :

# Créer l'extension manquante sur PG 17
docker exec <pg17_container> psql -U postgres -d mydb \
  -c 'CREATE EXTENSION IF NOT EXISTS pgvector;'

# Vérifier que toutes les extensions attendues sont là
docker exec <pg17_container> psql -U postgres -d mydb \
  -c 'SELECT extname, extversion FROM pg_extension ORDER BY extname;'

Les erreurs de type ERROR: function X does not exist lors de la restauration indiquent une extension absente ou incompatible — ne pas ignorer ces messages.

05

Basculer les applications et valider

Mettez vos applications en mode maintenance ou lecture seule (selon votre architecture), effectuez un dernier dump de cohérence depuis PG 15, puis mettez à jour la variable DATABASE_URL (ou POSTGRES_HOST/POSTGRES_PORT) de chaque service applicatif pour pointer vers le conteneur PG 17 sur le port 5432. Redémarrez les services applicatifs :

# Vérification applicative minimale
docker compose exec app php artisan db:monitor
# ou
curl -sf http://localhost/api/health | jq .database

Si la validation passe, arrêtez le conteneur PG 15, remappez le port 5432 sur postgres17, et supprimez le service postgres17 en le renommant en service principal. Supprimez l'ancien volume PG 15 après 30 jours de rétention.

Le search_path est le changement silencieux le plus fréquent. Depuis PG 15.1, le schéma public n'est plus inclus dans le search_path par défaut des rôles non-superuser. Si vos migrations Flyway, Liquibase ou vos seeds Artisan échouent avec relation "X" does not exist après la migration, ajoutez SET search_path TO public, "$user"; en tête de session ou qualifiez explicitement vos tables. Vérifiez aussi le paramètre dans postgresql.conf : search_path = 'public' (avec guillemets simples) force le comportement attendu pour tous les rôles.

Le cas Supabase : postgres:17 sans avertissement

La discussion #46080 du changelog public Supabase (github.com/orgs/supabase/discussions/46080) documente le basculement de l'image de référence vers postgres:17 intervenu en juin 2026. Les stacks auto-hébergées qui référençaient image: supabase/postgres sans tag de version ont reçu PG 17 lors du prochain docker compose pull, sans migration automatique des données.

Le comportement observé : le conteneur PG 17 démarre, refuse de lire le PGDATA PG 15 (format incompatible), et s'arrête immédiatement. Docker Compose tente les redémarrages configurés, puis marque le service unhealthy ou exited. L'application tombe. La cause n'est pas visible dans les logs applicatifs — elle est dans les logs du conteneur postgres :

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 bonne pratique pour tout déploiement Supabase auto-hébergé est de fixer le tag à une version mineure précise dans votre docker-compose.yml :

  db:
    image: supabase/postgres:15.8.1.040
    # ou la dernière 17.x une fois la migration effectuée
    # image: supabase/postgres:17.4.1.016

Consultez la page des releases GitHub de Supabase pour identifier le dernier tag de chaque branche majeure. Un tag flottant (latest, 15, 17) délègue la décision de mise à jour à l'éditeur — sans contrôle de votre côté sur le moment où elle intervient.

Stratégies de migration : pg_dump/restore vs pg_upgrade vs réplication logique

StratégieDurée de coupureComplexité
pg_dump / pg_restore (ce guide)5-30 min selon le volumeFaible — outils natifs, reproductible
pg_upgrade en place1-5 min (binaire rapide)Élevée — exige accès au binaire PG 15 et PG 17 simultanément, difficile en Docker
Réplication logique (zero-downtime réel)< 1 min (bascule en ligne)Très élevée — nécessite `wal_level = logical`, slots de réplication, synchronisation manuelle

Dépannage : erreurs fréquentes et leurs causes

Les erreurs qui reviennent le plus souvent lors d'une migration PG 15 → 17 dans un contexte Docker :

FATAL: database files are incompatible with server
Cause : le conteneur PG 17 a démarré sur le même volume que PG 15. Le format interne de PGDATA n'est pas rétrocompatible. Solution : toujours utiliser un volume neuf pour PG 17 et restaurer via pg_restore.

ERROR: extension "timescaledb" is not available (ou pg_partman, pg_cron)
Cause : l'extension n'est pas compilée pour PG 17 dans l'image officielle postgres:17. Solution : utiliser une image dérivée qui embarque les extensions voulues (ex : timescale/timescaledb:latest-pg17), ou compiler l'extension dans votre propre Dockerfile.

ERROR: role "X" already exists lors de la restauration des globals
Cause : le dump pg_dumpall --globals-only inclut CREATE ROLE pour tous les rôles, et le rôle postgres superuser existe déjà dans l'instance PG 17 vierge. Solution : utiliser --if-not-exists ou filtrer les lignes CREATE ROLE postgres dans backup_globals.sql avant restauration.

ERROR: relation "public.X" does not exist dans les applications
Cause : changement du search_path par défaut depuis PG 15.1. Solution : ajouter options=-csearch_path=public à la chaîne de connexion, ou poser ALTER ROLE app_user SET search_path = 'public'; après la restauration.

pg_restore: error: could not execute query: ERROR: invalid byte sequence for encoding "UTF8"
Cause : données encodées en LATIN1 dans PG 15, et l'instance PG 17 a été initialisée en UTF8 (par défaut). Solution : restaurer d'abord sur une instance PG 17 initialisée avec POSTGRES_INITDB_ARGS: --encoding=LATIN1 --lc-collate=fr_FR.UTF-8, ou migrer l'encodage des données dans un script intermédiaire.

Checklist de bascule — à valider dans l'ordre avant chaque étape

  • Avant de commencer : liste des extensions actives extraite (pg_extension), version PG 15 épinglée dans le Compose, dump complet validé (pg_restore --list sans erreur).
  • Après restauration sur PG 17 : toutes les extensions présentes et à la bonne version, search_path vérifié par un rôle applicatif (pas superuser), comptes de lignes comparés sur les 5 tables critiques entre PG 15 et PG 17.
  • Avant bascule applicative : fenêtre de maintenance annoncée, mode lecture seule activé si possible, dernier dump de cohérence capturé depuis PG 15.
  • Après bascule applicative : endpoint /health retourne 200 avec database: ok, logs applicatifs sans erreur relation does not exist, métriques pg_stat_activity sur PG 17 confirmant des connexions actives.
  • Rétention PG 15 : volume PG 15 conservé 30 jours minimum, dump conservé 90 jours, procédure de rollback documentée (pointer le Compose vers PG 15 et redémarrer).
  • Post-migration : ANALYZE VERBOSE; lancé sur toutes les bases pour recalculer les statistiques du planificateur, autovacuum validé actif, monitoring adapté à PG 17 (pg_stat_statements, pg_stat_bgwriter).

Gérer la migration dans un portefeuille de stacks clients

Pour une agence qui gère plusieurs environnements clients, la migration PG 15 → 17 n'est pas un événement unique — c'est un chantier à planifier par portefeuille.

Inventorier d'abord. La commande suivante liste toutes les versions PostgreSQL en cours sur un VPS qui héberge plusieurs projets 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 " <- {}"'

Le résultat identifie en une passe les conteneurs encore sur PG 15 à travers tout le parc.

Prioriser par risque. Les stacks avec des extensions compilées maison ou timescaledb/pg_partman ont besoin d'une image dérivée testée avant la bascule — elles demandent plus de temps. Les stacks avec uniquement pgcrypto, uuid-ossp et hstore basculent en 15 minutes.

Standardiser l'image. Définir une image commune dans un registry interne (registry.yourcompany.com/postgres:17-base) qui embarque les extensions validées par l'agence. Tous les projets Compose du portefeuille pointent cette image : la mise à jour de l'extension (ex : nouvelle version de pgvector) se propage en reconstruisant une seule image.

Les sauvegardes quotidiennes incluses sur les offres Agency de ServOrbit donnent un filet de sécurité pour chaque migration : si une stack client présente un comportement inattendu dans les 24 heures suivant la bascule, la restauration repart d'un backup de la veille plutôt que d'un dump manuel préparé dans l'urgence.

Migration planifiée, portefeuille sous contrôle

Les agences qui gèrent plusieurs stacks clients n'ont pas le luxe de découvrir une coupure le soir d'une montée de version automatique. ServOrbit centralise la gestion des VPS des portefeuilles clients — infrastructure maîtrisée, mises à jour planifiées, sauvegardes quotidiennes incluses sur les offres Agency.

Besoin d'aide ?

Parcourez notre centre d'aide et notre FAQ, ou contactez notre équipe — rappel, WhatsApp ou e-mail. Support en français, anglais et arabe.