[{"data":1,"prerenderedAt":169},["ShallowReactive",2],{"seo-verification":3,"blog-migrating-postgresql-15-to-17-in-a-docker-stack-en":6},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"id":7,"slug":8,"slugs":9,"title":13,"excerpt":14,"readTime":15,"views":16,"isPinned":17,"publishedAt":18,"category":19,"categories":25,"featuredImage":27,"bgImage":28,"posterImage":29,"relatedSolution":27,"intro":30,"sections":31,"ctaTitle":110,"ctaBody":111,"ctaButton":112,"ctaUrl":113,"relatedPosts":114},315,"migrating-postgresql-15-to-17-in-a-docker-stack",{"fr":10,"en":8,"ar":11,"es":12},"postgresql-15-17-migration-docker-vps","ترقية-postgresql-من-الإصدار-15-إلى-17-في-docker","migrar-postgresql-15-a-17-en-una-stack-docker","Migrating PostgreSQL 15 to 17 in a Docker stack","Complete guide to migrating PostgreSQL 15 to 17 in Docker: pg_dump\u002Fpg_restore, incompatible extensions, zero-downtime checklist — and the Supabase case.",12,0,false,"2026-08-30T00:00:00+00:00",{"id":20,"name":21,"slug":22,"color":23,"icon":24},6,"Databases","bases-de-donnees","bg-teal-500\u002F10 text-teal-400","database",[26],{"id":20,"name":21,"slug":22,"color":23,"icon":24},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Fpostgresql-15-17-migration-docker-vps-poster.svg","PostgreSQL 15 reaches end of life in November 2026. Supabase silently switched to `postgres:17` in June 2026, breaking self-hosted stacks that did not pin their version. If your Compose stack is still running PG 15, the migration window is open — and closing. This guide breaks down incompatible extensions, documents real behavioral changes between PG 15 and PG 17, and provides a zero-downtime migration procedure applicable to any production Compose stack.",[32,36,47,50,69,72,75,95,98,107],{"type":33,"title":34,"body":35},"h2","Why migrate now — PG 15 EOL and the Supabase signal","PostgreSQL 15 officially reaches end of life in **November 2026**. After that date, the PostgreSQL Global Development Group will no longer publish security fixes or bug patches: any vulnerability discovered after the end-of-life date remains unpatched on your version.\n\nBut migration can no longer wait until November for a more immediate reason: **Supabase switched its reference image to `postgres:17` in June 2026** (public changelog discussion #46080). Stacks that referenced `image: supabase\u002Fpostgres` without a version tag received PG 17 on the next `docker compose pull`, with no visible warning in the startup logs. Some self-hosted deployments discovered the break on restart — an extension compiled for PG 15 refusing to load under PG 17.\n\nIf your stack already pins `postgres:15`, you are protected in the short term. If it points to `postgres:latest` or a floating Supabase tag, it may have already migrated without your knowledge. The verification takes one command:\n\n```bash\ndocker exec \u003Ccontainer> psql -U postgres -c 'SELECT version();'\n```\n\nThe result shows you the exact version of the running server. Do not assume — verify.",{"type":37,"title":38,"items":39},"ul","What actually changes between PG 15 and PG 17",[40,41,42,43,44,45,46],"**`search_path` hardened since PG 15.1** (ADV-2022-00007): the `public` schema is no longer in the default `search_path` for non-superuser roles. Any query that assumed `public.my_table` instead of an explicitly qualified name may silently return 0 rows instead of an error.","**`pg_dump` produces downward-incompatible dumps**: a PG 17 dump cannot be restored to PG 15. The reverse is supported, and that is the direction of this migration. Keep PG 15 dumps for at least 30 days after the switch.","**`wal_level` default raised to `logical` in PG 16+**: if your `postgresql.conf` forced `wal_level = minimal`, behavior changes after migration. Existing logical replication slots may become invalid.","**Deprecated function removals**: `lo_import`, `lo_export` and several `pg_catalog` functions were removed or renamed between PG 15 and PG 17 — check any functions used by your custom extensions.","**`pg_stat_statements` changes query normalization**: Grafana\u002FPgHero dashboards that aggregate by query fingerprint will see their series reset to zero after migration.","**`pg_partman`** (partition management) requires version ≥ 5.x for PG 17 — version 4.x is not compatible.","**`timescaledb`** requires version ≥ 2.13 for PG 17; earlier versions refuse to load and block container startup.",{"type":33,"title":48,"body":49},"Extension inventory: what passes and what breaks","Before any migration, extract the list of active extensions in each database:\n\n```bash\ndocker exec \u003Cpg15_container> psql -U postgres -c \\\n  \"SELECT datname, extname, extversion FROM pg_extension e JOIN pg_database d ON d.oid = e.extnamespace ORDER BY datname, extname;\"\n```\n\nExtensions to check first on PG 17:\n\n**Compatible without action**: `pgcrypto`, `uuid-ossp`, `hstore`, `ltree`, `citext`, `pg_trgm`, `unaccent`, `intarray`, `tablefunc`, `earthdistance`, `fuzzystrmatch`. These extensions ship in the standard `postgresql-contrib` package and have no breaking changes between PG 15 and PG 17.\n\n**Requiring an update**: `pgvector` (upgrade to ≥ 0.7.0 for PG 17), `pg_stat_statements` (built-in, but must be recreated if `shared_preload_libraries` changes), `PostGIS` (≥ 3.4 for PG 17), `TimescaleDB` (≥ 2.13 required), `pg_partman` (≥ 5.0 required).\n\n**Not ported or requiring recompilation**: any custom-compiled extension (`.so`) built against PG 15 headers must be recompiled against PG 17 headers. The binary is not backward-compatible. If your Docker image embeds such a `.so`, rebuild the image before migration.\n\nPost-migration verification command:\n\n```bash\ndocker exec \u003Cpg17_container> psql -U postgres -d mydb \\\n  -c 'SELECT extname, extversion FROM pg_extension ORDER BY extname;'\n```\n\nCompare the `installed_version` column with your PG 15 inventory — a missing extension (`NULL`) indicates a loading issue to fix before validating the migration.",{"type":51,"title":52,"steps":53},"steps","Zero-downtime migration procedure: pg_dump\u002Fpg_restore",[54,57,60,63,66],{"title":55,"body":56},"Pin the PG 15 image version","Before any operation, **pin the current image** in your `docker-compose.yml` with its exact tag. This allows you to return to the original state with one command:\n\n```bash\ndocker inspect \u003Cpg15_container> --format '{{.Config.Image}}'\n# e.g.: postgres:15.7\n# Update the Compose:\n# image: postgres:15.7\n```\n\nCommit this change to your VCS before proceeding. Migration cannot be done in place: PG 17 refuses to start on a PG 15 `PGDATA` (incompatible internal format).",{"title":58,"body":59},"Capture the global dump and per-database dumps","Export global objects (roles, tablespaces) first, then each database individually. The `--no-owner` and `--no-acl` flags avoid permission errors when restoring under a different superuser:\n\n```bash\n# Global objects (roles, tablespaces)\ndocker exec \u003Cpg15_container> pg_dumpall \\\n  -U postgres \\\n  --globals-only \\\n  > backup_globals.sql\n\n# Each application database\ndocker exec \u003Cpg15_container> pg_dump \\\n  -U postgres \\\n  --no-owner \\\n  --no-acl \\\n  --format=custom \\\n  --file=\u002Ftmp\u002Fmydb_pg15.dump \\\n  mydb\n\ndocker cp \u003Cpg15_container>:\u002Ftmp\u002Fmydb_pg15.dump .\u002Fmydb_pg15.dump\n```\n\nVerify dump integrity before proceeding:\n\n```bash\npg_restore --list mydb_pg15.dump | head -20\n```\n\nA corrupt or empty dump here means the migration stops — never proceed to the next step without validating the dump.",{"title":61,"body":62},"Start the PG 17 container in parallel on a different port","Add a second service in your `docker-compose.yml` for PG 17, **on a distinct port** (e.g. 5433), with a new data volume. The PG 15 service remains active throughout this step — no interruption for your applications:\n\n```yaml\n  postgres17:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: ${POSTGRES_DB}\n    volumes:\n      - pg17_data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    ports:\n      - \"5433:5432\"\n    shm_size: 256mb\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ${POSTGRES_USER}\"]\n      interval: 5s\n      timeout: 3s\n      retries: 5\n\nvolumes:\n  pg17_data:\n```\n\nStart only this service:\n\n```bash\ndocker compose up -d postgres17\ndocker compose exec postgres17 pg_isready\n```",{"title":64,"body":65},"Restore to PG 17 and verify extensions","Restore global objects first, then the database:\n\n```bash\n# Roles and tablespaces\ndocker exec -i \u003Cpg17_container> psql -U postgres \u003C backup_globals.sql\n\n# Database restore\ndocker cp mydb_pg15.dump \u003Cpg17_container>:\u002Ftmp\u002Fmydb_pg15.dump\n\ndocker exec \u003Cpg17_container> pg_restore \\\n  -U postgres \\\n  --no-owner \\\n  --no-acl \\\n  -d mydb \\\n  \u002Ftmp\u002Fmydb_pg15.dump\n```\n\nIf pg_restore reports errors on extensions, fix them before continuing:\n\n```bash\n# Create the missing extension on PG 17\ndocker exec \u003Cpg17_container> psql -U postgres -d mydb \\\n  -c 'CREATE EXTENSION IF NOT EXISTS pgvector;'\n\n# Verify all expected extensions are present\ndocker exec \u003Cpg17_container> psql -U postgres -d mydb \\\n  -c 'SELECT extname, extversion FROM pg_extension ORDER BY extname;'\n```\n\nErrors of the type `ERROR: function X does not exist` during restore indicate a missing or incompatible extension — do not ignore these messages.",{"title":67,"body":68},"Switch applications and validate","Put your applications into maintenance mode or read-only (depending on your architecture), take a final consistency dump from PG 15, then update the `DATABASE_URL` (or `POSTGRES_HOST`\u002F`POSTGRES_PORT`) variable of each application service to point to the PG 17 container on port 5432. Restart application services:\n\n```bash\n# Minimal application check\ndocker compose exec app php artisan db:monitor\n# or\ncurl -sf http:\u002F\u002Flocalhost\u002Fapi\u002Fhealth | jq .database\n```\n\nIf validation passes, stop the PG 15 container, remap port 5432 to `postgres17`, and remove the `postgres17` service by renaming it as the primary service. Delete the old PG 15 volume after 30 days of retention.",{"type":70,"body":71},"tip","**search_path is the most common silent change.** Since PG 15.1, the `public` schema is no longer included in the default `search_path` for non-superuser roles. If your Flyway, Liquibase, or Artisan seeds fail with `relation \"X\" does not exist` after migration, add `SET search_path TO public, \"$user\";` at session start or explicitly qualify your table names. Also check the parameter in `postgresql.conf`: `search_path = 'public'` (with single quotes) forces the expected behavior for all roles.",{"type":33,"title":73,"body":74},"The Supabase case: postgres:17 without warning","Supabase public changelog discussion #46080 (github.com\u002Forgs\u002Fsupabase\u002Fdiscussions\u002F46080) documents the switch of the reference image to `postgres:17` that occurred in June 2026. Self-hosted stacks that referenced `image: supabase\u002Fpostgres` without a version tag received PG 17 on the next `docker compose pull`, without automatic data migration.\n\nObserved behavior: the PG 17 container starts, refuses to read the PG 15 `PGDATA` (incompatible format), and exits immediately. Docker Compose attempts the configured restarts, then marks the service `unhealthy` or `exited`. The application goes down. The cause is not visible in application logs — it is in the postgres container logs:\n\n```bash\ndocker logs \u003Csupabase_db_container> 2>&1 | head -20\n# FATAL: database files are incompatible with server\n# DETAIL: The data directory was initialized by PostgreSQL version 15, which is not compatible with this version 17.\n```\n\nThe recommended approach for any self-hosted Supabase deployment is to pin the tag to a precise minor version in your `docker-compose.yml`:\n\n```yaml\n  db:\n    image: supabase\u002Fpostgres:15.8.1.040\n    # or the latest 17.x once migration is complete\n    # image: supabase\u002Fpostgres:17.4.1.016\n```\n\nCheck the Supabase GitHub releases page to identify the latest tag for each major branch. A floating tag (`latest`, `15`, `17`) delegates the upgrade decision to the publisher — with no control on your side over when it happens.",{"type":76,"title":77,"headers":78,"rows":82},"comparison","Migration strategies: pg_dump\u002Frestore vs pg_upgrade vs logical replication",[79,80,81],"Strategy","Downtime","Complexity",[83,87,91],[84,85,86],"pg_dump \u002F pg_restore (this guide)","5–30 min depending on data volume","Low — native tools, reproducible",[88,89,90],"In-place pg_upgrade","1–5 min (fast binary upgrade)","High — requires both PG 15 and PG 17 binaries simultaneously, difficult in Docker",[92,93,94],"Logical replication (true zero-downtime)","\u003C 1 min (online switchover)","Very high — requires `wal_level = logical`, replication slots, manual synchronization",{"type":33,"title":96,"body":97},"Troubleshooting: frequent errors and their causes","The most common errors during a PG 15 → 17 migration in a Docker context:\n\n**`FATAL: database files are incompatible with server`**\nCause: the PG 17 container started on the same volume as PG 15. The internal `PGDATA` format is not backward-compatible. Fix: always use a new volume for PG 17 and restore via `pg_restore`.\n\n**`ERROR: extension \"timescaledb\" is not available`** (or `pg_partman`, `pg_cron`)\nCause: the extension is not compiled for PG 17 in the official `postgres:17` image. Fix: use a derived image that includes the required extensions (e.g. `timescale\u002Ftimescaledb:latest-pg17`), or compile the extension in your own `Dockerfile`.\n\n**`ERROR: role \"X\" already exists`** when restoring globals\nCause: the `pg_dumpall --globals-only` dump includes `CREATE ROLE` for all roles, and the `postgres` superuser role already exists in the fresh PG 17 instance. Fix: use `--if-not-exists` or filter out `CREATE ROLE postgres` lines from `backup_globals.sql` before restoring.\n\n**`ERROR: relation \"public.X\" does not exist`** in applications\nCause: default `search_path` change since PG 15.1. Fix: add `options=-csearch_path=public` to the connection string, or run `ALTER ROLE app_user SET search_path = 'public';` after restore.\n\n**`pg_restore: error: could not execute query: ERROR: invalid byte sequence for encoding \"UTF8\"`**\nCause: data encoded as `LATIN1` in PG 15, and the PG 17 instance was initialized with `UTF8` (default). Fix: restore to a PG 17 instance initialized with `POSTGRES_INITDB_ARGS: --encoding=LATIN1 --lc-collate=fr_FR.UTF-8`, or migrate encoding in an intermediate script.",{"type":37,"title":99,"items":100},"Switchover checklist — validate in order before each step",[101,102,103,104,105,106],"**Before starting**: active extension list extracted (`pg_extension`), PG 15 version pinned in Compose, full dump validated (`pg_restore --list` without errors).","**After restore to PG 17**: all extensions present at the correct version, `search_path` verified via an application role (not superuser), row counts compared on the 5 critical tables between PG 15 and PG 17.","**Before application switchover**: maintenance window announced, read-only mode activated if possible, final consistency dump captured from PG 15.","**After application switchover**: `\u002Fhealth` endpoint returns 200 with `database: ok`, application logs without `relation does not exist` errors, `pg_stat_activity` metrics on PG 17 confirm active connections.","**PG 15 retention**: PG 15 volume kept for at least 30 days, dump kept for 90 days, rollback procedure documented (point Compose at PG 15 and restart).","**Post-migration**: `ANALYZE VERBOSE;` run on all databases to recalculate planner statistics, `autovacuum` confirmed active, monitoring updated for PG 17 (`pg_stat_statements`, `pg_stat_bgwriter`).",{"type":33,"title":108,"body":109},"Managing the migration across a client portfolio","For an agency managing multiple client environments, the PG 15 → 17 migration is not a single event — it is a workload to plan across the portfolio.\n\n**Inventory first.** The following command lists all running PostgreSQL versions across a VPS hosting multiple Compose projects:\n\n```bash\ndocker ps --format '{{.Names}}' | xargs -I{} sh -c \\\n  'docker exec {} psql -U postgres -qtAX -c \"SELECT current_setting(\\\"server_version\\\")\" 2>\u002Fdev\u002Fnull && echo \" \u003C- {}\"'\n```\n\nThe result identifies in one pass all containers still on PG 15 across the entire fleet.\n\n**Prioritize by risk.** Stacks with custom-compiled extensions or `timescaledb`\u002F`pg_partman` need a tested derived image before switching — they require more time. Stacks with only `pgcrypto`, `uuid-ossp`, and `hstore` switch in 15 minutes.\n\n**Standardize the image.** Define a shared image in an internal registry (`registry.yourcompany.com\u002Fpostgres:17-base`) that embeds the extensions validated by the agency. All portfolio Compose projects point to this image: updating an extension (e.g. a new version of `pgvector`) propagates by rebuilding one image.\n\nThe daily backups included on ServOrbit Agency plans provide a safety net for each migration: if a client stack shows unexpected behavior in the 24 hours following the switch, restoration starts from a previous day's backup rather than a manually prepared dump created under pressure.","Planned migration, portfolio under control","Agencies managing multiple client stacks cannot afford to discover an outage the evening of an automatic version upgrade. ServOrbit centralizes VPS management for client portfolios — controlled infrastructure, planned updates, daily backups included on Agency plans.","View agency plans","\u002Fsolutions\u002Fagences",[115,133,150],{"id":116,"slug":117,"slugs":118,"title":122,"excerpt":123,"readTime":124,"views":16,"isPinned":17,"publishedAt":125,"category":126,"categories":127,"featuredImage":27,"bgImage":28,"posterImage":129,"relatedSolution":130},60,"hosting-supabase-on-a-vps",{"fr":119,"en":117,"ar":120,"es":121},"heberger-supabase-vps","استضافة-supabase-على-خادم-vps","alojar-supabase-en-un-vps","Hosting Supabase on a VPS in 2026","Self-host Supabase on your Cloud VPS: Postgres, Auth, Storage, and REST API with Envoy Gateway. Kong→Envoy migration guide, S3 URL troubleshooting.",11,"2026-04-21T00:00:00+00:00",{"id":20,"name":21,"slug":22,"color":23,"icon":24},[128],{"id":20,"name":21,"slug":22,"color":23,"icon":24},"\u002Fblog\u002Fcovers\u002Fheberger-supabase-vps-poster.svg",{"categorySlug":131,"appSlug":132},"databases","supabase",{"id":134,"slug":135,"slugs":136,"title":140,"excerpt":141,"readTime":142,"views":16,"isPinned":17,"publishedAt":143,"category":144,"categories":145,"featuredImage":27,"bgImage":28,"posterImage":147,"relatedSolution":148},56,"postgresql-on-a-vps-a-reliable-and-controlled-database",{"fr":137,"en":135,"ar":138,"es":139},"heberger-postgresql-vps","postgresql-على-خادم-vps-قاعدة-بيانات-موثوقة-ومتحكم-بها","alojar-postgresql-en-un-vps","PostgreSQL on a VPS: a reliable and controlled database","Host PostgreSQL on a VPS: volumes, backups, restricted network access and sound configuration for your applications.",4,"2026-04-25T00:00:00+00:00",{"id":20,"name":21,"slug":22,"color":23,"icon":24},[146],{"id":20,"name":21,"slug":22,"color":23,"icon":24},"\u002Fblog\u002Fcovers\u002Fheberger-postgresql-vps-poster.svg",{"categorySlug":131,"appSlug":149},"postgresql-stack",{"id":151,"slug":152,"slugs":153,"title":157,"excerpt":158,"readTime":159,"views":16,"isPinned":17,"publishedAt":160,"category":161,"categories":166,"featuredImage":27,"bgImage":28,"posterImage":168,"relatedSolution":27},282,"depends-on-is-not-enough-postgresql-healthcheck-in-compose",{"fr":154,"en":152,"ar":155,"es":156},"docker-compose-depends-on-healthcheck","depends-on-لا-يكفي-healthcheck-لـ-postgresql-في-compose","docker-compose-healthcheck-postgresql","depends_on is not enough: PostgreSQL healthcheck in Compose","Why `depends_on` alone does not guarantee PostgreSQL is ready, and how to configure a reliable healthcheck with `service_healthy` to avoid race conditions.",8,"2026-08-19T00:00:00+00:00",{"id":162,"name":163,"slug":164,"color":165,"icon":164},3,"Deployment","deploiement","bg-success\u002F10 text-success",[167],{"id":162,"name":163,"slug":164,"color":165,"icon":164},"\u002Fblog\u002Fcovers\u002Fdocker-compose-depends-on-healthcheck-poster.svg",1788100067382]