惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
Simon Willison's Weblog
Simon Willison's Weblog
H
Help Net Security
F
Fortinet All Blogs
H
Heimdal Security Blog
S
Schneier on Security
L
LangChain Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
P
Palo Alto Networks Blog
J
Java Code Geeks
博客园 - 【当耐特】
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Vulnerabilities – Threatpost
I
InfoQ
Recorded Future
Recorded Future
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CERT Recently Published Vulnerability Notes
T
Tenable Blog
腾讯CDC
C
Check Point Blog
量子位
M
MIT News - Artificial intelligence
GbyAI
GbyAI
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
小众软件
小众软件
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
V2EX - 技术
V2EX - 技术
T
Threatpost
Engineering at Meta
Engineering at Meta
Attack and Defense Labs
Attack and Defense Labs
T
Tailwind CSS Blog
S
Securelist
The Cloudflare Blog
博客园 - 叶小钗
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
爱范儿
爱范儿

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Zero-Downtime PostgreSQL Major Version Upgrades in Containers: The Problem Nobody Talks About
Abhishek Sha · 2026-05-10 · via DEV Community

Running PostgreSQL in containers is one of the smartest infrastructure decisions a team can make — until the day you need to upgrade across a major version.

Then it becomes one of the most painful ones.

This post walks through why major PostgreSQL upgrades are uniquely hard in containerized environments, the common approaches teams reach for (and why they hurt), and how pg-upgrade — a Docker-native upgrade toolkit — turns a weekend-long migration into a reproducible, CI-validated three-step process.


Why Containerized PostgreSQL in the First Place?

Before diving into the upgrade problem, it's worth understanding why teams choose self-managed containerized PostgreSQL over managed services like Amazon RDS, Aurora, or Google Cloud SQL.

The cost difference is stark:

Setup Monthly cost (50 GB, 4 vCPU, 16 GB RAM)
Amazon RDS PostgreSQL (db.r6g.xlarge) ~$400–600/month
Aurora PostgreSQL (db.r6g.xlarge) ~$500–700/month
Self-managed PostgreSQL on EKS (m6g.xlarge node) ~$120–180/month

That's a 3–5x cost difference, which compounds quickly as your data grows. Beyond cost, containerized PostgreSQL gives you:

  • Full control over PostgreSQL configuration, extensions, and versions
  • Portability — the same docker-compose.yml works in dev, staging, and production
  • No vendor lock-in — you're not tied to a cloud provider's upgrade schedule or supported version matrix
  • Extension freedom — install PostGIS, TimescaleDB, pgvector, or any community extension without waiting for a managed service to support it

The tradeoff? You own the operational complexity. And nowhere does that complexity bite harder than major version upgrades.


The Problem: Major Version Upgrades Are Not Like Container Restarts

Upgrading a PostgreSQL minor version (e.g. 15.3 → 15.7) is trivial — swap the image tag and restart. The data directory format doesn't change.

Major version upgrades (e.g. 13 → 16) are a different beast entirely. PostgreSQL's internal data format changes between major versions. You cannot simply point a PostgreSQL 16 binary at a data directory written by PostgreSQL 13 and expect it to work. It will refuse to start.

The official tool for major version upgrades is pg_upgrade. It migrates the data directory in-place from the old format to the new one. But pg_upgrade has a constraint that makes it awkward in containerized environments:

Both the old and new PostgreSQL binaries must be present on the same machine, with access to the same data directories.

In a container world, this is an unnatural requirement. Your PostgreSQL 13 container has PG 13 binaries. Your PostgreSQL 16 image has PG 16 binaries. They don't share a filesystem, and they were never designed to.


What Teams Actually Do (And Why It Hurts)

Let's look at the common approaches teams reach for when they need to upgrade, and the hidden costs of each.

Option 1: pg_dumpall / pg_dump + Restore

The most common approach. Dump the entire database with pg_dumpall, spin up a new PostgreSQL 16 container, and restore.

# Dump from old container
docker exec pg13 pg_dumpall -U postgres > full_dump.sql

# Restore into new container
docker exec -i pg16 psql -U postgres < full_dump.sql

Enter fullscreen mode Exit fullscreen mode

The problem:

  • Downtime scales with database size. A 100 GB database takes 1–3 hours to dump and another 2–5 hours to restore (indexes are rebuilt from scratch). Your application is down for all of it.
  • No rollback. Once you've promoted the new cluster and your application is writing to it, you can't go back to the dump — it's already stale.
  • No integrity proof. Did every row make it? Did all sequences reset correctly? Did materialized views survive? You're trusting the process.
  • Memory and disk pressure. The dump file itself can be enormous. A 500 GB database produces a 200–400 GB SQL file that needs to live somewhere during the migration.

For a production database that your business depends on, multi-hour downtime windows are often simply not acceptable.


Option 2: Logical Replication with pglogical or Built-in Slots

A more sophisticated approach: set up logical replication from the old cluster to a new PG 16 cluster, let it catch up, then cut over.

PG 13 (primary) ──logical replication──► PG 16 (replica)
                                            │
                              [catch up, then promote]

Enter fullscreen mode Exit fullscreen mode

The problems:

  • Replication lag during cutover. Logical replication does not replicate DDL (schema changes). Any schema migrations running during the replication period must be manually applied to both clusters.
  • Setup complexity is high. You need to configure wal_level = logical, create replication slots, manage slot lag, handle large objects (which logical replication doesn't support), and deal with sequences (which are not replicated).
  • Slots can accumulate WAL indefinitely. If the replica falls behind, the primary holds WAL segments in reserve. On a busy write-heavy database, this can fill your disk within hours.
  • Not all data types replicate cleanly. pg_largeobject (lo/bytea blobs stored via lo functions), unlogged tables, and some partitioning configurations don't replicate through logical slots without extra work.
  • Still requires a cutover window. Even with replication in place, you need a moment where writes stop on the old cluster, you confirm the replica is caught up, and you flip the application over. That window is shorter than a dump/restore — but it's still a window.

Option 3: Snapshot + Restore on a New Node

Cloud-native teams sometimes use volume snapshots: snapshot the EBS/PD volume backing the old cluster, mount it on a new instance with PostgreSQL 16 installed, and run pg_upgrade there.

The problems:

  • PostgreSQL 16 isn't installed on the new node. You're back to the original problem: pg_upgrade needs both binary sets on the same machine. You either have to install both versions manually or find a way to get them there.
  • No reproducibility. The upgrade is a manual, unrepeatable operation. If something goes wrong, you start over from the snapshot — but you have no record of what commands were run, in what order, or what state the system was in when they failed.
  • No integrity check. After pg_upgrade finishes, how do you know the data is intact? You might spot-check a few tables manually, but there's no systematic verification.
  • Hard to test in staging. The snapshot approach is environment-specific. The commands that worked on production won't necessarily work in your staging environment because the volume setup is different.

Option 4: Manual pg_upgrade Inside a Container

Some teams install both PostgreSQL versions inside a single container and run pg_upgrade manually:

FROM ubuntu:22.04
RUN apt-get install postgresql-13 postgresql-16

Enter fullscreen mode Exit fullscreen mode

The problems:

  • No standard image for this. Every team builds their own one-off image, with different assumptions about data directory paths, binary locations, and user permissions.
  • Permissions are a minefield. pg_upgrade must be run as the postgres OS user, not root. The data directories must be owned by postgres. Getting this right inside a custom container is non-trivial.
  • No CI validation. The upgrade is run once, manually, against production. There's no prior test run to prove it would work.
  • Old Debian/Ubuntu base images hit EOL. PostgreSQL 9.6 was only available on Debian Stretch and Buster, both of which reached end-of-life. Their apt mirrors were moved off the main servers, so apt-get install postgresql-9.6 fails on a fresh install — producing cryptic 404 errors with no obvious fix.

How pg-upgrade Solves This

pg-upgrade is a set of Docker images that package both the old and new PostgreSQL binaries, along with three coordinated container steps — init-old, upgrade, and verify — connected by Docker volumes.

The core insight: instead of trying to install two PostgreSQL versions in a running container at migration time, bake both binary sets into a purpose-built upgrade image at build time. Then the upgrade is just docker run.

The Three-Step Pipeline

┌──────────────────────────────────────────────────────────────────┐
│  Step 1 — init-old                                               │
│  Seeds the old cluster with real-world schema: tables, indexes,  │
│  views, sequences, materialized views, FK constraints, sample    │
│  data across multiple databases.                                 │
└──────────────────────┬───────────────────────────────────────────┘
                       │  pg-old-data volume
┌──────────────────────▼───────────────────────────────────────────┐
│  Step 2 — upgrade                                                │
│  Runs pg_upgrade --check (dry run first), then the real upgrade. │
│  Prints before/after directory snapshots, file sizes, structural │
│  renames, and wall-clock duration.                               │
└──────────────────────┬───────────────────────────────────────────┘
                       │  pg-new-data volume
┌──────────────────────▼───────────────────────────────────────────┐
│  Step 3 — verify                                                 │
│  Starts the upgraded cluster and asserts: databases exist, row   │
│  counts match, indexes are intact, views work, sequences are     │
│  preserved, foreign keys survive.                                │
└──────────────────────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

For a production upgrade, you skip init-old and mount your existing data PVC directly into the upgrade step. The old cluster must be scaled to zero first (your downtime window) — but the upgrade itself runs in seconds to minutes, not hours.

Downtime Comparison

Approach 10 GB DB 100 GB DB 1 TB DB
pg_dump + restore 30–90 min 3–8 hours 30+ hours
Logical replication cutover 5–30 min 5–30 min 5–30 min
pg-upgrade (copy mode) ~45 sec ~7 min ~70 min
pg-upgrade (link mode -k) < 5 sec < 5 sec < 5 sec

Link mode (-k) uses hard links instead of copying data files. The upgrade completes in seconds regardless of cluster size, because no bytes are moved — the old and new clusters share the same underlying files. The tradeoff is that the old data directory is no longer independently valid after the upgrade, so you delete it only after confirming the new cluster is healthy in production.

What the Upgrade Output Looks Like

After the upgrade step, you get a structured report directly in your CI log:

──────────────────────────────────────────────────────────────────────
  Old cluster — PostgreSQL 9.6
──────────────────────────────────────────────────────────────────────
  Path:                /var/lib/postgresql/9.6/main
  Total size:          47M

  Notable structural changes applied during this upgrade:
    pg_xlog/    → pg_wal/    (WAL directory, renamed in PG 10)
    pg_clog/    → pg_xact/   (transaction status, renamed in PG 10)
    pg_log/     → log/       (server log directory, renamed in PG 10)

──────────────────────────────────────────────────────────────────────
  Upgrade complete
──────────────────────────────────────────────────────────────────────
  Cluster size:        47M → 49M  (+4%)
  Upgrade duration:    8s
  PostgreSQL version:  9.6 → 16
──────────────────────────────────────────────────────────────────────

Enter fullscreen mode Exit fullscreen mode

And after the verify step:

──────────────────────────────────────────────────────────────────────
  Verification result — PostgreSQL 16
──────────────────────────────────────────────────────────────────────
  Passed:    9
  Failed:    0
──────────────────────────────────────────────────────────────────────

Enter fullscreen mode Exit fullscreen mode

This is the piece every other approach lacks: a systematic, scripted assertion that the data survived intact.

No Credentials Required

One of the underrated advantages of pg_upgrade over dump/restore or logical replication: it never connects to a running database server. It reads and writes data files directly on disk as the postgres OS user. No database password is passed around, no pg_hba.conf changes are needed, and application credentials (stored in pg_authid) are migrated automatically along with everything else.

CI-Validated Upgrade Paths

Every supported upgrade path — from 9.6→12 all the way through 15→16 — is validated in GitHub Actions on every commit. The matrix runs the full three-step pipeline (init → upgrade → verify) and fails the build if any integrity check doesn't pass.

This means when you pull abhsss/pg-upgrade:13-to-16 and run it against your production data, you're not running an untested script. You're running the same pipeline that passed CI.

Kubernetes Ready, No Changes Required

The same image runs on Kubernetes without modification. Replace Docker volumes with PersistentVolumeClaims and docker run with Jobs:

# Apply the upgrade Job
kubectl apply -f pg-upgrade-job.yaml

# Wait for it to complete
kubectl wait --for=condition=complete job/pg-upgrade-run --timeout=30m

# Check the output
kubectl logs job/pg-upgrade-run

Enter fullscreen mode Exit fullscreen mode

For a production cluster running as a StatefulSet, the workflow is:

  1. Scale the StatefulSet to zero (kubectl scale statefulset postgres --replicas=0)
  2. Run the upgrade Job, mounting the existing PVC
  3. Update the StatefulSet's image tag to PostgreSQL 16
  4. Run the verify Job
  5. Scale the StatefulSet back up

The downtime window is steps 1–5. With link mode, steps 2–4 take under a minute for any database size.


Quick Start

# Pull the image for your upgrade path
docker pull abhsss/pg-upgrade:13-to-16

# Create volumes
docker volume create pg-old-data
docker volume create pg-new-data

# Step 1 — seed test data (skip this in production; mount your existing volume)
docker run --rm \
  -v pg-old-data:/var/lib/postgresql/13/main \
  abhsss/pg-upgrade:13-to-16 init-old

# Step 2 — upgrade
docker run --rm \
  -v pg-old-data:/var/lib/postgresql/13/main \
  -v pg-new-data:/var/lib/postgresql/16/main \
  abhsss/pg-upgrade:13-to-16 upgrade

# Step 3 — verify
docker run --rm \
  -v pg-new-data:/var/lib/postgresql/16/main \
  abhsss/pg-upgrade:13-to-16 verify

# Cleanup
docker volume rm pg-old-data pg-new-data

Enter fullscreen mode Exit fullscreen mode

Supported upgrade paths are published on DockerHub. Images exist for every common path from PG 9.6 to PG 16.


When to Use What

Situation Recommended approach
Small DB, extended downtime window acceptable pg_dump / restore — simple, no extra tooling
Zero downtime requirement, complex schema with DDL changes during cutover Logical replication + careful cutover scripting
Containerized PostgreSQL, predictable downtime window pg-upgrade — reproducible, CI-validated, fast
Containerized PostgreSQL, absolute minimum downtime pg-upgrade with link mode -k

Want to Contribute?

The project is open source and there are good entry points for contributors at every level. If something above resonated with you, the easiest way to get involved is to pick up one of the open issues:

Good first issues — low scope, well-defined:

Larger contributions — help wanted:

  • #2 — CI coverage for pg_partman, pg_cron, and pgaudit
    Extensions that touch background workers and cron scheduling have different upgrade behavior than data extensions. This adds matrix entries and verify assertions for each.

  • #3 — Add PG 17 as an upgrade target
    PG 17 ships on Debian Bookworm (OpenSSL 3), while older source versions were compiled against OpenSSL 1.1. Getting both to coexist in one image requires a careful COPY --from and ldconfig dance.

  • #5 — Support --link and --clone upgrade modes
    Link mode (-k) reduces upgrade time to under 5 seconds regardless of cluster size. Clone mode offers a middle ground. This wires both through as options without breaking the default copy-mode behavior.

No contribution is too small. Open an issue first if you want to discuss scope before sending a pull request.


Conclusion

Containerized PostgreSQL is a compelling alternative to managed cloud databases — significant cost savings, full control, and no vendor lock-in. But that control comes with responsibility, and major version upgrades are where that responsibility shows up most clearly.

The conventional approaches — dump/restore, logical replication, manual pg_upgrade — all work, but they carry hidden costs: hours of downtime, unrepeatable procedures, no systematic integrity verification, and no way to prove the upgrade would succeed before running it against production.

pg-upgrade addresses each of these. It's a reproducible, Docker-native upgrade pipeline that runs the same three steps in CI that you'll run in production. The upgrade is fast (seconds to minutes, not hours), the output is structured and machine-readable, and the verify step gives you a signed-off assertion that the data survived intact — before you promote the new cluster and scale your application back up.