<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Viacheslav(Slava) Sarzhan</title>
    <description>The latest articles on DEV Community by Viacheslav(Slava) Sarzhan (@vyacheslav_sarzhan_3c1767).</description>
    <link>https://dev.to/vyacheslav_sarzhan_3c1767</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951251%2Ffb0304d0-d0a6-4ea8-824d-679d79b8a4a0.jpg</url>
      <title>DEV Community: Viacheslav(Slava) Sarzhan</title>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vyacheslav_sarzhan_3c1767"/>
    <language>en</language>
    <item>
      <title>Community Docker Images: keeping the operator open without a vendor registry lock-in</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:34:34 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/community-docker-images-keeping-the-operator-open-without-a-vendor-registry-lock-in-532g</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/community-docker-images-keeping-the-operator-open-without-a-vendor-registry-lock-in-532g</guid>
      <description>&lt;p&gt;&lt;strong&gt;PostgreSQL community images&lt;/strong&gt; address a real gap in how a Kubernetes database operator earns your trust. Running a database operator on Kubernetes means trusting two things: the code, and the container images the operator pulls. The code is on GitHub, easy to inspect, easy to fork. The container images, the registry that hosts them, and the license that governs them all sit with the vendor, and any of those three can change without the source repository changing at all. Starting with Percona Operator for PostgreSQL 3.0.0, you can run the operator against community images you build yourself from the official PostgreSQL packages on download.postgresql.org, in a registry you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Community Docker Images: tech preview in PGO 3.0.0, official in 3.1.0.&lt;/strong&gt; Point the operator at upstream-built PostgreSQL images instead of the Percona Distribution images, in a registry you control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You build them yourself from the official PostgreSQL source.&lt;/strong&gt; The Dockerfiles pull packages from download.postgresql.org (the PGDG repositories), so a security-conscious team can audit the chain end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs are intentional.&lt;/strong&gt; Distribution-only features like TDE do not exist in a community-built image; if you depend on those, run the distribution image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How open source gets diluted in practice&lt;/li&gt;
&lt;li&gt;Why distributions exist anyway, honestly&lt;/li&gt;
&lt;li&gt;How Community Docker Images work&lt;/li&gt;
&lt;li&gt;Limits of the upstream path&lt;/li&gt;
&lt;li&gt;What to try, what to tell us&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How open source gets diluted
&lt;/h2&gt;

&lt;p&gt;Open source has changed in the last few years, and not always for the better. Companies have learned that you can keep a project's source code fully open and still capture most of the lock-in by quietly closing the parts that matter in production: the release artifacts, the container images, the supported OS list, the certified Kubernetes distributions, the marketplace listings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same project, closed artifacts
&lt;/h3&gt;

&lt;p&gt;You can have a fully community CNCF project that does not appear on the Red Hat Marketplace except as a paid Enterprise edition. Similarly, you can have a vendor that ships one packaging in the community and a richer one in Enterprise with the features you actually need in production. The license still says "open source." The practical experience says "you depend on us." And the source repository's license is not the only license that matters here: a vendor can change the license, the trademark policy, or the distribution terms on the container images alone, while leaving the source repository untouched. That has happened in the PostgreSQL operator space recently, and the community noticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the community is right to be wary
&lt;/h3&gt;

&lt;p&gt;Nobody outside the vendor can predict when a license will change, when a feature will move behind a paywall, or when an external contribution will get rejected because it competes with an Enterprise feature. Recent history has plenty of examples and the PostgreSQL community has been paying attention. When this community resists vendor-controlled distributions, it is not nostalgia. It is a rational read of where things have gone before.&lt;/p&gt;

&lt;p&gt;I work on Percona's PostgreSQL operator, so I see this conversation from the vendor side. The skepticism is fair. The honest question for us is what to do about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why distributions exist anyway
&lt;/h2&gt;

&lt;p&gt;Acknowledging the community's concerns does not mean distributions are pointless. There are real reasons to ship one, and pretending otherwise makes for bad blog posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a distribution buys you
&lt;/h3&gt;

&lt;p&gt;A vendor-built distribution lets the vendor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Control the build process, dependencies, and defaults so they fit a specific user shape.&lt;/li&gt;
&lt;li&gt;Ship hotfixes faster, because the whole release path sits in one place.&lt;/li&gt;
&lt;li&gt;Fork PostgreSQL itself when something the upstream community will not accept, or can take years to accept, matters to customers, such as Transparent Data Encryption.&lt;/li&gt;
&lt;li&gt;For a Kubernetes operator, ship images with exactly the tools and extensions the operator supports, and skip everything else. The CVE surface stays smaller.&lt;/li&gt;
&lt;li&gt;Give QA and Service teams a predictable environment. "We support extensions A, B, C and not D, X, Z" is only honest if QA actually exercises A, B, C and the Service team can work with them in production env.&lt;/li&gt;
&lt;li&gt;Give customers one accountable party for the full release cycle, from hotfix through package availability. Some teams explicitly need that contract for compliance and audit reasons.&lt;/li&gt;
&lt;li&gt;And yes, less positive reasons that we covered above also apply, which is exactly the part the community keeps pointing at.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The trade-off you accept
&lt;/h3&gt;

&lt;p&gt;If you run the vendor distribution, you accept that the vendor's registry, image policy, and supported-extension matrix become part of your stack. If the vendor changes any of that, your operator deployment changes with it. That is not hypothetical for users who have lived through it on other products.&lt;/p&gt;

&lt;p&gt;So the real question is whether you can keep the benefits a distribution provides for the users who want them, while leaving an honest, supported door open for users who do not. That is the door PGO 3.0.0 opens as a tech preview, with PGO 3.1.0 making it part of the regular release cycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Community Docker Images in PGO
&lt;/h2&gt;

&lt;p&gt;Starting with Percona Operator for PostgreSQL 3.0.0, the operator can run against images built from upstream PostgreSQL packages, not just the Percona Distribution images. This is what we are calling &lt;strong&gt;Community Docker Images&lt;/strong&gt;. In 3.0.0, the feature ships as a tech preview. In 3.1.0, these images become part of our official release cycle and are fully documented.&lt;/p&gt;

&lt;p&gt;One of the main advantages of Community Docker Images is that the community can request or contribute any extension that does not exist in the official Percona PostgreSQL distribution. TimescaleDB and Citus are the first examples: the community asked for them, and we shipped both in the Community Images set from day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;The operator does not care where the image came from, as long as the image meets the operator's runtime expectations (&lt;code&gt;pg_ctl&lt;/code&gt;, the expected binary layout, the directory structure the operator manages). Therefore, PGO 3.0.0 publishes the build manifests and Dockerfiles for community images so anyone can produce a working image set from upstream packages alone.&lt;/p&gt;

&lt;p&gt;A typical CR using a community image looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgv2.percona.com/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PerconaPGCluster&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/postgresql-community:17&lt;/span&gt;
  &lt;span class="na"&gt;postgresVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;17&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pgBouncer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/pgbouncer-community:1.23&lt;/span&gt;
  &lt;span class="na"&gt;backups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pgbackrest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/pgbackrest-community:2.51&lt;/span&gt;
  &lt;span class="c1"&gt;# other spec fields unchanged from a normal CR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields that change are &lt;code&gt;spec.image&lt;/code&gt;, &lt;code&gt;spec.proxy.pgBouncer.image&lt;/code&gt;, and &lt;code&gt;spec.backups.pgbackrest.image&lt;/code&gt;. You can build and publish all three images under your own registry, with your own tags if that helps you track versions. The operator drives the rest of the deployment the same way it always has: instances, backups, replication, monitoring, all of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What ships in each image
&lt;/h3&gt;

&lt;p&gt;Each Community Docker Image is a thin layer over the chosen base (UBI9 or UBI8) plus the packages the operator needs for that role. Where you see &lt;code&gt;{N}&lt;/code&gt;, substitute the PostgreSQL major you build for (17, 18, and so on).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;postgres&lt;/code&gt; image&lt;/strong&gt; (e.g. &lt;code&gt;postgres17&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgresql{N}-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgresql{N}-contrib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;contrib modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_repack_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;online table/index reorganization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgaudit_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;audit logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set_user_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;privilege escalation control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgvector_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vector similarity search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wal2json_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WAL to JSON logical decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_cron_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;in-database cron scheduler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgbackrest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;backup/restore tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patroni&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HA cluster manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timescaledb-2-postgresql-{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;time-series extension (x86_64 only; EL9 only for PG18)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;citus_{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;distributed PostgreSQL (PG16+ only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pgbackrest&lt;/code&gt; image&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgbackrest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;backup/restore tool only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pgbouncer&lt;/code&gt; image&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgbouncer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;connection pooler only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The split is intentional. The postgres image ships the full operator-aware runtime; the backup and proxy images stay minimal. As a result, the operator's components stay in separate failure domains and shrinks the attack surface of each container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building your own image
&lt;/h3&gt;

&lt;p&gt;The published build flow is intentionally short. Specifically, the goal is for a security-conscious team to read it end to end in one sitting, audit the dependencies, and reproduce the build on their own infrastructure. We are not asking anyone to trust a black box. In fact, the build pulls PostgreSQL from download.postgresql.org, the official package repositories maintained by the PostgreSQL Global Development Group (PGDG), so the trust chain runs from PGDG to your build to your registry to your operator, with no vendor in the middle. The Dockerfile, the package list, and a sample CI job for keeping the image current are available with the 3.0.0 tech preview, and become part of the official documentation in 3.1.0.&lt;/p&gt;

&lt;p&gt;Once your image is in your registry, you point the operator at it. Day-to-day operations look identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to build the images
&lt;/h3&gt;

&lt;p&gt;The Dockerfile, the package list, and a sample CI job ship in &lt;a href="https://github.com/percona/percona-docker" rel="noopener noreferrer"&gt;percona-docker/postgresql-containers/community&lt;/a&gt;. The build is a regular &lt;code&gt;make&lt;/code&gt; target on top of &lt;code&gt;docker buildx&lt;/code&gt;, so you can run it on any multi-platform builder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Prerequisites: docker buildx with a multi-platform builder&lt;/span&gt;
docker buildx create &lt;span class="nt"&gt;--use&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; multiarch

&lt;span class="c"&gt;# Build and push all PostgreSQL community images (UBI9 / EL9)&lt;/span&gt;
git clone https://github.com/percona/percona-docker
&lt;span class="nb"&gt;cd &lt;/span&gt;percona-docker/postgresql-containers/community
make all &lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.0.0 &lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myrepo/percona-postgresql-operator

&lt;span class="c"&gt;# Or a single image&lt;/span&gt;
make postgres17 &lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.0.0 &lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myrepo/percona-postgresql-operator

&lt;span class="c"&gt;# UBI8 / EL8 variants&lt;/span&gt;
make all-ubi8 &lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.0.0-ubi8 &lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myrepo/percona-postgresql-operator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;make all&lt;/code&gt; builds all three images (postgres, pgBouncer, pgBackRest) so they stay version-aligned. Override &lt;code&gt;REGISTRY&lt;/code&gt; and &lt;code&gt;TAG&lt;/code&gt; to point at your own namespace and tagging scheme. Once the images are in your registry, plug them into the CR fields shown earlier and the operator picks them up.&lt;/p&gt;

&lt;p&gt;Full build documentation: &lt;a href="https://github.com/percona/percona-docker/blob/main/postgresql-containers/community/README.md" rel="noopener noreferrer"&gt;percona-docker/postgresql-containers/community/README.md&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to contribute
&lt;/h3&gt;

&lt;p&gt;Community images live in &lt;a href="https://github.com/percona/percona-docker" rel="noopener noreferrer"&gt;percona/percona-docker&lt;/a&gt;, and the build is driven by a &lt;code&gt;transform.py&lt;/code&gt; generator that produces the Dockerfiles under &lt;code&gt;build/&lt;/code&gt;. The files under &lt;code&gt;build/&lt;/code&gt; are regenerated on every sync, so contributions go through the generator, never through the generated files.&lt;/p&gt;

&lt;p&gt;The contribution flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edit &lt;code&gt;transform.py&lt;/code&gt; (or the package mapping or extension injection it drives).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;make test&lt;/code&gt; (or &lt;code&gt;python3 -m pytest tests/ -v&lt;/code&gt;). The suite covers package mapping, transforms, extension injection, and pgaudit legacy naming. Version-specific behavior needs new tests in &lt;code&gt;tests/test_transform.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Regenerate the Dockerfiles with &lt;code&gt;./sync.sh --force --apply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify locally: &lt;code&gt;make postgres17 PLATFORMS=linux/amd64 OUTPUT=--load&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Commit &lt;code&gt;transform.py&lt;/code&gt;, the regenerated &lt;code&gt;build/&lt;/code&gt; files, and &lt;code&gt;.source-hashes&lt;/code&gt; together so the chain stays consistent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Adding a new PostgreSQL major follows the same flow plus a &lt;code&gt;TARGETS&lt;/code&gt; update in &lt;code&gt;sync.sh&lt;/code&gt; and matching variables and targets in the &lt;code&gt;Makefile&lt;/code&gt; (for both &lt;code&gt;all&lt;/code&gt; and &lt;code&gt;all-ubi8&lt;/code&gt;). Then &lt;code&gt;./sync.sh --apply&lt;/code&gt; produces the Dockerfiles.&lt;/p&gt;

&lt;p&gt;Full contribution guide: &lt;a href="https://github.com/percona/percona-docker/blob/main/postgresql-containers/community/CONTRIBUTING.md" rel="noopener noreferrer"&gt;community/CONTRIBUTING.md&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to provide feedback
&lt;/h3&gt;

&lt;p&gt;Two channels, depending on the shape of the feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub issue&lt;/strong&gt; on &lt;a href="https://github.com/percona/percona-postgresql-operator" rel="noopener noreferrer"&gt;percona/percona-postgresql-operator&lt;/a&gt; with the &lt;code&gt;community-images&lt;/code&gt; label. Use this for bug reports, missing extensions, build problems, and concrete requests. The label keeps all community-image reports in one filter the team watches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Forum&lt;/strong&gt; at &lt;a href="https://forums.percona.com" rel="noopener noreferrer"&gt;forums.percona.com&lt;/a&gt;. Use this for open-ended discussion, questions, and shape-of-thing feedback ("would this also work for X workload?").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful things to include in a GitHub issue: PostgreSQL version, the operator version (3.0.0 or 3.1.0), the image tag you tried, and either the error or the unexpected behavior. If the issue is a build problem, paste the &lt;code&gt;make&lt;/code&gt; output. If it is runtime, the operator and database logs help.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limits worth being honest about
&lt;/h3&gt;

&lt;p&gt;A community image is not a Percona Distribution image. Two practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distribution-only features will not work.&lt;/strong&gt; Transparent Data Encryption, for example, lives in the Percona Distribution build. A community image built from upstream PostgreSQL does not include it. If you depend on TDE, run the distribution image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support boundaries are different.&lt;/strong&gt; Percona Support is responsible for the Percona Distribution images and the operator code. A community image you built yourself is your image. We will help debug operator behavior. We will not own the contents of an image we did not produce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, these are the right trade-offs. The point of community images is to give you transparency and control. Taking on the maintenance of your own image is part of that deal. At the same time, we publish all three images under &lt;code&gt;perconalab/percona-postgresql-operator&lt;/code&gt; on Docker Hub so you can evaluate the tech preview without standing up your own build pipeline first. &lt;code&gt;perconalab&lt;/code&gt; is Percona's non-production namespace, so use those images for testing; for production, build and sign your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The first step was taking full engineering ownership of Percona Operator for PostgreSQL as an independent project, so the roadmap, the release cadence, and the governance live with one team that the community can talk to directly. Community Docker Images are the next step in that same commitment. If the community adopts this path, we have ideas for what to invest in next.&lt;/p&gt;

&lt;p&gt;We will let the community tell us. If this is useful, we keep investing here. We are ready to add more features to the operator around Community Images. Conversely, if nobody adopts it, that is also a signal, and an honest one.&lt;/p&gt;

&lt;p&gt;Try the tech preview in 3.0.0. Open an issue if the build flow is rougher than it should be. Tell us what you want next on the forum or directly on GitHub.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Percona Operator for PostgreSQL 3.0.0: Hard Fork, OLM Scoping, Major Upgrades</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Wed, 03 Jun 2026 17:37:09 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/percona-operator-for-postgresql-300-hard-fork-olm-scoping-major-upgrades-32ac</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/percona-operator-for-postgresql-300-hard-fork-olm-scoping-major-upgrades-32ac</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2mz0zcyoxqexdaje7ey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2mz0zcyoxqexdaje7ey.png" alt="Percona Operator for PostgreSQL 3.0.0" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Percona Operator for PostgreSQL 3.0.0 is here. This is the release that completes the hard fork of the operator from the Crunchy Data PostgreSQL Operator into a fully independent project, with a dedicated &lt;code&gt;upstream.pgv2.percona.com&lt;/code&gt; API group for the inherited CRDs, an automatic CRD-rename rollout for existing 2.x installs on upgrade, and a &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;public roadmap&lt;/a&gt; that drives what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard fork complete: CRDs renamed under &lt;code&gt;upstream.pgv2.percona.com&lt;/code&gt;.&lt;/strong&gt; The Crunchy operator and the Percona operator can now coexist in the same Kubernetes cluster, in different namespaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved OLM namespace scoping for OpenShift.&lt;/strong&gt; All-namespaces and single-namespace modes now honor the &lt;code&gt;OperatorGroup&lt;/code&gt; correctly. Certified bundle users on the &lt;code&gt;stable-cw&lt;/code&gt; channel need to switch to &lt;code&gt;stable&lt;/code&gt; to receive the upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Major-version upgrades now use the official Percona Distribution image.&lt;/strong&gt; The same &lt;code&gt;percona-distribution-postgresql&lt;/code&gt; build family that runs in your clusters now handles &lt;code&gt;pg_upgrade&lt;/code&gt; too, so image-signature and CVE-scanning policies apply uniformly across runtime and upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This release ships three headline changes that matter for production teams. The &lt;strong&gt;CRD renaming under a Percona-owned API group&lt;/strong&gt;, which finally lets the Crunchy operator and the Percona operator coexist in the same Kubernetes cluster. &lt;strong&gt;Proper OLM namespace scoping&lt;/strong&gt; for OpenShift installations. And the &lt;strong&gt;move to the official Percona Distribution image for major PostgreSQL version upgrades&lt;/strong&gt;, aligning the upgrade path with the same binaries that run in your clusters.&lt;/p&gt;

&lt;p&gt;All three land in service of the same goal: making 3.0.0 a clean, durable operational baseline for the operator's next several years as an independent project. Future releases will be shaped by what the community asks for and contributes back. The public roadmap is the durable signal of that commitment.&lt;/p&gt;

&lt;p&gt;In this post, you will learn about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hard fork and how the CRD rename unlocks coexistence with the Crunchy operator&lt;/li&gt;
&lt;li&gt;OLM namespace-scoping improvements for OpenShift installations&lt;/li&gt;
&lt;li&gt;The move to the official Percona Distribution image for major PostgreSQL version upgrades&lt;/li&gt;
&lt;li&gt;Other improvements and the 2.7.0 deprecation&lt;/li&gt;
&lt;li&gt;Supported PostgreSQL versions and platforms&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hard fork: CRDs renamed under &lt;code&gt;upstream.pgv2.percona.com&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjm2zks8bq5c0rhj915o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjm2zks8bq5c0rhj915o.png" alt="Hard fork and CRD rename" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Percona Operator for PostgreSQL has, until now, been a soft fork. Custom Resources inherited from Crunchy PGO used the upstream &lt;code&gt;postgres-operator.crunchydata.com&lt;/code&gt; API group. The two operators shared CRDs, which meant you could only run one of them in a given Kubernetes cluster. Installing both would lead to overlapping CRDs, conflicting webhooks, and finalizer collisions, so platform teams had to pick a side before they had finished evaluating.&lt;/p&gt;

&lt;p&gt;Starting with 3.0.0, every inherited CRD is renamed into a new dedicated &lt;code&gt;upstream.pgv2.percona.com&lt;/code&gt; API group (&lt;a href="https://perconadev.atlassian.net/browse/K8SPG-1007" rel="noopener noreferrer"&gt;K8SPG-1007&lt;/a&gt;). Percona's own native CRDs (such as &lt;code&gt;PerconaPGCluster&lt;/code&gt; under &lt;code&gt;pgv2.percona.com/v2&lt;/code&gt;) are unchanged. The change applies to the inherited resources: &lt;code&gt;PostgresCluster&lt;/code&gt;, &lt;code&gt;PGUpgrade&lt;/code&gt;, &lt;code&gt;PGAdmin&lt;/code&gt;, and the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coexistence: running both operators in the same cluster
&lt;/h3&gt;

&lt;p&gt;The practical effect is that &lt;strong&gt;the Crunchy Data PostgreSQL Operator and the Percona Operator for PostgreSQL can now run on the same Kubernetes cluster at the same time&lt;/strong&gt;, in different namespaces, with no CRD or webhook conflict. That unlocks a few real workflows: evaluating both operators on the same staging cluster without spinning up a second cluster, running existing Crunchy-managed clusters in some namespaces while bringing up new Percona-managed clusters in others, or testing a new database version on the Percona side while production stays on Crunchy until you are confident. The choice between the two operators stops being all-or-nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrade behavior for existing 2.x installs
&lt;/h3&gt;

&lt;p&gt;For an existing install, the upgrade to 3.0.0 is mechanically simple. The operator creates the new-API-group CRDs alongside the legacy ones, then runs a one-time migration that updates dependent objects (Secrets, certificates, finalizer references) to point at the new CRD instances. Existing custom resources keep working through the legacy CRDs during the transition, and once migration completes, all reconciliation moves to the new group.&lt;/p&gt;

&lt;p&gt;Old &lt;code&gt;PostgresCluster&lt;/code&gt; reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-operator.crunchydata.com/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresCluster&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New (after upgrade to 3.0.0):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upstream.pgv2.percona.com/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresCluster&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Day-to-day, your &lt;code&gt;PerconaPGCluster&lt;/code&gt; Custom Resource (the one most teams interact with directly) is unchanged. The rename mostly matters in three situations: when a kubectl filter or a GitOps repository hard-codes the old API group, when a CI pipeline references the legacy CRD by name, and when you run the Percona and Crunchy operators side by side and need them not to collide.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: During the CRD migration on upgrade, the release notes report brief disruptions to pgBackRest operations (typically 1 to 2 minutes) while Kubernetes propagates certificate changes. Plan the upgrade during a maintenance window if backup continuity is critical, or pause scheduled backups during the upgrade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full details on the API-group change are in the &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/" rel="noopener noreferrer"&gt;Percona PostgreSQL operator documentation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Improved OLM namespace scoping for OpenShift
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zk39n30y6svzwju2poj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zk39n30y6svzwju2poj.png" alt="OLM namespace scoping" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenShift users install operators through the OpenShift Lifecycle Manager (OLM), and OLM enforces an &lt;code&gt;OperatorGroup&lt;/code&gt; to scope which namespaces an operator watches. In practice, 2.x had quirks: teams that selected "Single namespace" mode would sometimes see the operator reconciling CRs in other namespaces, and teams in "All namespaces" mode would sometimes see incomplete coverage when CRs were created in newly-added namespaces.&lt;/p&gt;

&lt;p&gt;3.0.0 fixes this by aligning the operator's namespace watch list with the &lt;code&gt;OperatorGroup&lt;/code&gt; that OLM applies. All-namespaces installs watch all namespaces. Single-namespace installs respect the &lt;code&gt;targetNamespaces&lt;/code&gt; set on the &lt;code&gt;OperatorGroup&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters in shared infrastructure
&lt;/h3&gt;

&lt;p&gt;For an OpenShift platform team running shared infrastructure, this distinction matters operationally. A typical setup has the database operator installed once in a platform namespace (such as &lt;code&gt;openshift-operators&lt;/code&gt;) but expected to serve &lt;code&gt;PerconaPGCluster&lt;/code&gt; resources owned by individual application teams in their own namespaces. If the operator over-reaches into namespaces it should not watch, RBAC noise multiplies. If it under-reaches, application teams file tickets about clusters that never reconcile. The 3.0.0 alignment with &lt;code&gt;OperatorGroup&lt;/code&gt; semantics removes both failure modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  OperatorGroup wiring
&lt;/h3&gt;

&lt;p&gt;For users installing through OLM via the OpenShift web console, the install flow is unchanged. The fix is in how the operator's reconciler interprets the OLM-supplied namespace scope after install. For users who manage OperatorGroups directly, a single-namespace install looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;operators.coreos.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OperatorGroup&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;percona-pg-operator-group&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetNamespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres-prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an all-namespaces install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;operators.coreos.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OperatorGroup&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;percona-pg-operator-group&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openshift-operators&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty &lt;code&gt;spec: {}&lt;/code&gt; (or an &lt;code&gt;OperatorGroup&lt;/code&gt; with no &lt;code&gt;targetNamespaces&lt;/code&gt;) means "watch all namespaces" by OLM convention. The 3.0.0 operator now honors that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: After you upgrade an existing 2.x install to 3.0.0, the operator may begin reconciling &lt;code&gt;PerconaPGCluster&lt;/code&gt; resources in namespaces it had previously ignored due to the prior scoping bug. Audit existing CRs across your cluster before upgrading, especially if you have stale test clusters in unintended namespaces. The release notes call this out explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note for community vs certified bundle users&lt;/strong&gt;: Community OLM bundles did not support cluster-wide (all-namespaces) mode in earlier versions. The 3.0.0 adds it. Certified bundles already supported cluster-wide mode, but they used a separate &lt;code&gt;stable-cw&lt;/code&gt; channel for it. With 3.0.0 the channels are unified, so subscriptions on the certified &lt;code&gt;stable-cw&lt;/code&gt; channel need to switch to &lt;code&gt;stable&lt;/code&gt; to receive the upgrade. A few OLM rules to keep in mind: you cannot install the same bundle twice in one cluster (no two community installs, no two certified installs, even if they target different namespaces), but community and certified bundles can coexist on the same cluster as long as neither is in cluster-wide mode. The catalog blocks two cluster-wide installs of the same operator regardless of source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the full install workflow on OpenShift, see the &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/openshift.html" rel="noopener noreferrer"&gt;OpenShift installation documentation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Major PostgreSQL version upgrades now use the official Percona Distribution image
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbuwfu6tmt19z90w20nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbuwfu6tmt19z90w20nm.png" alt="Major-version upgrade image" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Major-version upgrades (for example, PostgreSQL 17 to 18) require running &lt;code&gt;pg_upgrade&lt;/code&gt;, which needs binaries for both the source and target versions in the same environment. The operator has supported major-version upgrades since 2.x, but it shipped its own dedicated upgrade image to do so. That worked, but it meant a Percona-specific image lived in the upgrade path, separate from the same Percona Distribution for PostgreSQL build that runs in your clusters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching to the official Percona Distribution image
&lt;/h3&gt;

&lt;p&gt;In 3.0.0, the operator switches to using the &lt;strong&gt;official Percona Distribution for PostgreSQL image&lt;/strong&gt; for major-version upgrades: &lt;code&gt;percona/percona-distribution-postgresql-upgrade&lt;/code&gt; (current tag: &lt;code&gt;18.4-17.10-16.14-15.18-14.23-1&lt;/code&gt;, which encodes the bundled major versions). The benefit is alignment: the binaries that run &lt;code&gt;pg_upgrade&lt;/code&gt; are the same binaries that ship in the corresponding &lt;code&gt;percona-distribution-postgresql&lt;/code&gt; image you already run in production, built from the same source, signed the same way, and patched on the same schedule. The operator orchestrates the upgrade through the &lt;code&gt;PerconaPGUpgrade&lt;/code&gt; Custom Resource that names the source and target versions, the upgrade image, and the target component images (PostgreSQL, pgBouncer, pgBackRest).&lt;/p&gt;

&lt;h3&gt;
  
  
  Running an upgrade through the PerconaPGUpgrade CR
&lt;/h3&gt;

&lt;p&gt;A PostgreSQL 17 to 18 upgrade looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgv2.percona.com/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PerconaPGUpgrade&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1-17-to-18&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgresClusterName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/percona/percona-distribution-postgresql-upgrade:18.4-17.10-16.14-15.18-14.23-1&lt;/span&gt;
  &lt;span class="na"&gt;fromPostgresVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;17&lt;/span&gt;
  &lt;span class="na"&gt;toPostgresVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;
  &lt;span class="na"&gt;toPostgresImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/percona/percona-distribution-postgresql:18.4-1&lt;/span&gt;
  &lt;span class="na"&gt;toPgBouncerImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/percona/percona-pgbouncer:1.25.2-1&lt;/span&gt;
  &lt;span class="na"&gt;toPgBackRestImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/percona/percona-pgbackrest:2.58.0-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it with &lt;code&gt;kubectl apply -f upgrade.yaml -n &amp;lt;namespace&amp;gt;&lt;/code&gt;. The operator reconciles the upgrade as a controlled, observable process: it brings the cluster down for the upgrade window, runs &lt;code&gt;pg_upgrade&lt;/code&gt; from the bundled image, brings the cluster back up on the target version, and updates pgBouncer and pgBackRest images in the same step.&lt;/p&gt;

&lt;p&gt;Operationally, this matters for teams running on PostgreSQL's annual major-version cadence. Every September brings a new major release. Staying on a supported version means executing one major upgrade per cluster per year. Pulling the upgrade image from the same &lt;code&gt;percona-distribution-postgresql&lt;/code&gt; registry path as the runtime image means image-signature verification, mirror-to-private-registry rules, and CVE-scanning policies you already have in place apply to the upgrade flow without any per-image exception.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;pgaudit&lt;/code&gt; extension is not upgraded automatically. After the operator completes the major version upgrade, drop and recreate &lt;code&gt;pgaudit&lt;/code&gt; manually in each database that uses it: &lt;code&gt;DROP EXTENSION pgaudit;&lt;/code&gt; followed by &lt;code&gt;CREATE EXTENSION pgaudit;&lt;/code&gt;. The release notes call this out as a required step (&lt;a href="https://perconadev.atlassian.net/browse/K8SPG-1022" rel="noopener noreferrer"&gt;K8SPG-1022&lt;/a&gt;). Also worth scanning for collation-dependent indexes after the upgrade and refreshing collation metadata with &lt;code&gt;ALTER DATABASE &amp;lt;name&amp;gt; REFRESH COLLATION VERSION;&lt;/code&gt; per the upstream PostgreSQL 18 release notes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full procedure, prerequisites, and rollback notes are in the &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/update-db-major.html" rel="noopener noreferrer"&gt;major version upgrade documentation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Other Improvements
&lt;/h2&gt;

&lt;p&gt;Operational polish landed alongside the headline changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go 1.26 update&lt;/strong&gt; (&lt;a href="https://perconadev.atlassian.net/browse/K8SPG-1019" rel="noopener noreferrer"&gt;K8SPG-1019&lt;/a&gt;): the operator binary is now built with Go 1.26, picking up performance optimizations, tooling improvements, and the security fixes that landed in the Go runtime since the previous release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgaudit upgrade documentation&lt;/strong&gt; (&lt;a href="https://perconadev.atlassian.net/browse/K8SPG-1022" rel="noopener noreferrer"&gt;K8SPG-1022&lt;/a&gt;): the major-version upgrade docs now include an explicit pgaudit drop-and-recreate procedure, surfacing the gotcha that previously caught users mid-upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release also defaults the cluster-upgrade documentation to PostgreSQL 18 across all examples and tutorials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Supported software and platforms
&lt;/h2&gt;

&lt;p&gt;The Percona Operator for PostgreSQL 3.0.0 is developed and tested on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;: 14.23-1, 15.18-1, 16.14-1, 17.10-1, 18.4-1 (PostgreSQL 18 is the new default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgBackRest&lt;/strong&gt;: 2.58.0-2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgBouncer&lt;/strong&gt;: 1.25.2-1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patroni&lt;/strong&gt;: 4.1.3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostGIS&lt;/strong&gt;: 3.5.6&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PMM Client&lt;/strong&gt;: 2.44.1-1 and 3.7.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supported Kubernetes platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Kubernetes Engine (GKE) 1.33 to 1.35&lt;/li&gt;
&lt;li&gt;Amazon Elastic Kubernetes Service (EKS) 1.33 to 1.35&lt;/li&gt;
&lt;li&gt;OpenShift 4.18 to 4.21&lt;/li&gt;
&lt;li&gt;Azure Kubernetes Service (AKS) 1.33 to 1.35&lt;/li&gt;
&lt;li&gt;Minikube 1.38.1 (Kubernetes v1.35.1) for local development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deprecation: 2.7.0 support dropped
&lt;/h3&gt;

&lt;p&gt;Support for Custom Resource Definitions from operator version 2.7.0 has been removed. If you are still on 2.7.0, upgrade to 2.8.x or 2.9.x first, then upgrade to 3.0.0. The CRD migration described above only handles 2.8.x and 2.9.x to 3.0.0 transitions cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;3.0.0 is the release where the Percona Operator for PostgreSQL becomes a fully independent project. The CRD rename removes the last upstream coupling that mattered operationally. The OLM scoping fix removes a long-standing OpenShift quirk. The official major-version upgrade image removes one of the more painful operational gaps in earlier versions.&lt;/p&gt;

&lt;p&gt;Beyond the technical work, 3.0.0 is also where Percona's commitment to community-driven development moves from intent to mechanism. The &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;public roadmap&lt;/a&gt; is open. The &lt;a href="https://github.com/percona/percona-postgresql-operator/issues" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt; is open. The images are freely redistributable. Future releases will be shaped by what the community asks for, files, and contributes back. If there is a feature you want to see in 3.1.0 or 3.2.0, open an issue or a PR. That is where the work happens now.&lt;/p&gt;







&lt;p&gt;&lt;em&gt;This post was originally published on the &lt;a href="https://www.percona.com/blog/percona-operator-for-postgresql-3-0-0-hard-fork-olm-scoping-major-upgrades/" rel="noopener noreferrer"&gt;Percona blog&lt;/a&gt;. For the full Try It Out links (release notes, documentation, GitHub, public roadmap, issue tracker, community forum), see the bottom of the canonical post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Not All Open Source Is Equal: Choosing a PostgreSQL Operator for Kubernetes in 2026</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Fri, 29 May 2026 16:11:36 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/not-all-open-source-is-equal-choosing-a-postgresql-operator-for-kubernetes-in-2026-1l6c</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/not-all-open-source-is-equal-choosing-a-postgresql-operator-for-kubernetes-in-2026-1l6c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmbtqazgu6xpy1tlnnbl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmbtqazgu6xpy1tlnnbl.png" alt="Not All Open Source Is Equal: Choosing a PostgreSQL Operator for Kubernetes in 2026" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choosing an open source PostgreSQL operator for Kubernetes used to be a question about features and community size. In 2026, it has become a question about licensing posture, image distribution, and whether the project you pick today will still be operationally open in three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MinIO is archived. Bitnami's free catalog got cut. Crunchy Data's official images come with redistribution and consulting restrictions.&lt;/strong&gt; "Open source" today is doing less work than it used to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three questions decide whether an operator is &lt;em&gt;operationally&lt;/em&gt; open&lt;/strong&gt; or just source-available: are the container images publicly redistributable, are core operational features in the open-source build, and is the roadmap genuinely public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Percona's PostgreSQL Operator hard-forks at v3.0.0.&lt;/strong&gt; Fully redistributable images, public roadmap, public issue tracker. This post is the &lt;em&gt;why&lt;/em&gt;; parts 2 and 3 are the &lt;em&gt;how&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;part 1 of a 3-part series&lt;/strong&gt; on running PostgreSQL on Kubernetes with a fully open-source operator.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 (this post)&lt;/strong&gt;: how the open-source landscape has shifted under your feet, and what to look for in an operator before you commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;: migrating from the Crunchy Data PostgreSQL Operator to the Percona PostgreSQL Operator using the standby cluster method (near-zero downtime)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt;&lt;/strong&gt;: two simpler migration paths: backup-and-restore and persistent-volume reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, you will learn about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed in the open-source landscape over the last few years, with specific examples&lt;/li&gt;
&lt;li&gt;What licensing and redistribution restrictions actually mean for Kubernetes operators in production&lt;/li&gt;
&lt;li&gt;How to evaluate whether a project is "open source in theory" or open source in practice&lt;/li&gt;
&lt;li&gt;Where Percona's PostgreSQL Operator fits in, and what the practical migration looks like&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Open source isn't what it used to be
&lt;/h2&gt;

&lt;p&gt;The landscape of open source has undergone significant changes in recent years, and selecting the right operator and tooling for PostgreSQL clusters in Kubernetes has never been more important. Three recent shifts illustrate the pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  MinIO
&lt;/h3&gt;

&lt;p&gt;MinIO was, for years, the default open-source S3-compatible storage backend for Kubernetes workloads. The trajectory over the last few years tells the story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.min.io/agplv3-open-source-license/" rel="noopener noreferrer"&gt;Switched its license to AGPLv3&lt;/a&gt;, with several enterprise features moved into a commercial-only edition&lt;/li&gt;
&lt;li&gt;Entered what amounted to maintenance mode, narrowing community engagement, limiting support to paid subscriptions, and reducing acceptance of community contributions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On April 25, 2026, the &lt;a href="https://github.com/minio/minio" rel="noopener noreferrer"&gt;github.com/minio/minio&lt;/a&gt; repository was archived by the project owner&lt;/strong&gt;, ending public development of the open-source version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is still cloneable, but the project is no longer maintained as open source. Teams running MinIO in production now need an exit plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bitnami images
&lt;/h3&gt;

&lt;p&gt;Bitnami Docker images have long been a staple for databases (including Postgres), middleware, and developer tooling. In July 2025, Broadcom's Tanzu Division &lt;a href="https://news.broadcom.com/app-dev/broadcom-introduces-bitnami-secure-images-for-production-ready-containerized-applications" rel="noopener noreferrer"&gt;announced Bitnami Secure Images&lt;/a&gt; and &lt;a href="https://github.com/bitnami/containers/issues/83267" rel="noopener noreferrer"&gt;signalled the deprecation of the free public catalog&lt;/a&gt;. The concrete timeline that followed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;August 28, 2025&lt;/strong&gt;: deprecation of non-hardened Debian-based images in the free tier began, and non-latest images started to be removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 29, 2025&lt;/strong&gt; (after community pushback): the public &lt;code&gt;docker.io/bitnami&lt;/code&gt; catalog was reduced. The remaining free images were limited to a small curated set of latest-version, hardened images intended for development use; older versions of most applications were moved to a "Bitnami Legacy" repository&lt;/li&gt;
&lt;li&gt;The full catalog and the hardened production images now require a paid &lt;strong&gt;Bitnami Secure Images&lt;/strong&gt; subscription, &lt;a href="https://thenewstack.io/broadcom-ends-free-bitnami-images-forcing-users-to-find-alternatives/" rel="noopener noreferrer"&gt;reportedly priced in the tens of thousands of dollars per year&lt;/a&gt; per organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Kubernetes teams, the practical impact was immediate: any Helm chart that pinned a specific Bitnami image version (a recommended practice) found that image gone or moved, breaking CI pipelines and air-gapped deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crunchy Data PostgreSQL images
&lt;/h3&gt;

&lt;p&gt;Crunchy Data illustrates the same dynamic in the Postgres operator space. To be clear: the Crunchy Data PostgreSQL Operator is a mature, well-engineered project, and the team behind it has done a lot of valuable work upstream and around pgBackRest and Patroni integrations. The point of this section is not the engineering, it is the redistribution and usage terms that govern the official builds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Crunchy's licensing shifts, 2022 to 2024
&lt;/h4&gt;

&lt;p&gt;Between 2022 and 2024, several shifts occurred:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redistribution restrictions.&lt;/strong&gt; While the PostgreSQL code is open source, Crunchy's official Docker images include branding and enterprise features that are not freely redistributable. The &lt;a href="https://www.crunchydata.com/developers/terms" rel="noopener noreferrer"&gt;Crunchy Data Developer Program terms&lt;/a&gt; describe the software as intended for internal or personal use; production use by larger organizations typically requires an active support subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrictions on consulting and resale.&lt;/strong&gt; The terms explicitly prohibit using Crunchy's images to deliver support or consulting services to others without an authorized agreement. The PostgreSQL source itself remains open source, but the official images and the way they are packaged are not freely redistributable, which limits practical use in commercial and customer-facing settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry move.&lt;/strong&gt; Most images were moved to &lt;code&gt;registry.developers.crunchydata.com&lt;/code&gt;, which requires authentication and acceptance of terms before pulling. That draws a clearer line between open-source code and proprietary builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: the project is open source on the code side, but the practical artifacts (images, helm releases) are gated.&lt;/p&gt;




&lt;h2&gt;
  
  
  What these restrictions really mean for Kubernetes users
&lt;/h2&gt;

&lt;p&gt;When container images and operators come with redistribution limits, authentication requirements, or "internal-use-only" clauses, the impact on Kubernetes environments is immediate and concrete. Teams can no longer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build air-gapped clusters by mirroring images to a private registry without working through a license review&lt;/li&gt;
&lt;li&gt;Rely on GitOps workflows that assume publicly accessible OCI images&lt;/li&gt;
&lt;li&gt;Fork or customize the operator freely, because official images cannot be redistributed with modifications&lt;/li&gt;
&lt;li&gt;Use the software in commercial or customer-facing products without additional licensing&lt;/li&gt;
&lt;li&gt;Run multi-cluster or multi-tenant Postgres at scale without bumping into usage terms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a database operator, where almost every operational pattern depends on the container images you can pull and run, these restrictions effectively turn a project into a "source-available but not operationally open" solution. The code is open. The operating story is not.&lt;/p&gt;

&lt;p&gt;As a result, many teams are switching to fully open-source alternatives: the &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/" rel="noopener noreferrer"&gt;Percona Operator for PostgreSQL&lt;/a&gt;, &lt;a href="https://cloudnative-pg.io/" rel="noopener noreferrer"&gt;CloudNativePG&lt;/a&gt;, &lt;a href="https://github.com/zalando/postgres-operator" rel="noopener noreferrer"&gt;Zalando Postgres Operator&lt;/a&gt;, &lt;a href="https://stackgres.io/" rel="noopener noreferrer"&gt;StackGres&lt;/a&gt;, and a few others.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to evaluate "open source" in 2026
&lt;/h2&gt;

&lt;p&gt;The bigger picture here is that "open source" today often exists more in theory than in practice. It pays to look past the badge and check the operating reality. Three questions to ask before you commit to an operator:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Are the container images publicly redistributable?
&lt;/h3&gt;

&lt;p&gt;If you cannot pull the official images without authentication, or you cannot mirror them to your private registry without a license review, your air-gapped and GitOps stories are constrained from day one. This is the question that turned out to be the most consequential one for MinIO, Bitnami, and Crunchy users in 2025.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Are core operational features in the open-source build, or behind a paywall?
&lt;/h3&gt;

&lt;p&gt;Backup, monitoring, HA, and security features should be in the build everyone uses, not gated behind an enterprise tier. A "community edition" that omits the feature most teams actually need is a marketing build, not a real open-source build.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Is the governance and roadmap public?
&lt;/h3&gt;

&lt;p&gt;A project where you can see the issues, the PRs, and the roadmap is one you can plan around. The Percona PG Operator's &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;public roadmap&lt;/a&gt; is an example of what this looks like in practice. A project run inside a vendor's private tracker, by contrast, gives you no visibility.&lt;/p&gt;

&lt;p&gt;These are not gotchas. They are the questions that decide whether a project will still serve you the same way in three years.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migrate to freedom
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Announcing the hard fork
&lt;/h3&gt;

&lt;p&gt;We strongly believe in fully open-source software and want to increase our investment in the PostgreSQL and Kubernetes ecosystems. To back that up, we have decided to &lt;strong&gt;hard fork the Crunchy Data PostgreSQL Kubernetes Operator&lt;/strong&gt;. Starting from version 3.0.0, the Percona PostgreSQL Kubernetes Operator is a fully independent project, with a &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;public roadmap&lt;/a&gt;, public issue tracker, and freely redistributable images.&lt;/p&gt;

&lt;p&gt;The hard fork is not a critique of Crunchy's engineering. It is a commitment that the operator will keep evolving in a fully open-source direction, with no surprises about which features will be available to which audience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why migration is straightforward
&lt;/h3&gt;

&lt;p&gt;Because the Percona PostgreSQL Operator is a hard fork of the Crunchy operator, the migration paths are surprisingly straightforward. The same underlying tools (Patroni, pgBackRest, PgBouncer) and the same overall design are used in both, which means migration can be done in multiple ways, sometimes with near-zero downtime, sometimes faster with a small downtime window. The next two posts in this series walk through three concrete options.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This was the "why." The next two posts are the "how":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 2: Standby cluster migration&lt;/a&gt;.&lt;/strong&gt; Bring up a Percona cluster as a standby of the Crunchy primary, catch it up via pgBackRest plus streaming replication, and promote it at cutover. The only downtime is the cutover itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3: Backup-restore and PV reuse&lt;/a&gt;.&lt;/strong&gt; Two simpler paths: bootstrap a Percona cluster directly from a Crunchy pgBackRest backup, or retain the existing PGDATA persistent volume and have Percona pick up where Crunchy left off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reversibility and exit options
&lt;/h3&gt;

&lt;p&gt;All three paths are reversible: because Percona's operator, images, and tooling are 100 percent open source and remain compatible with the same backup format and the same Patroni HA model, you keep full control. You can migrate back to Crunchy if your team decides to, or out to another open-source operator (CloudNativePG, Zalando, StackGres) using the same patterns. That last journey is a topic for a future article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This series covers basic deployment patterns and simplified configuration examples. If your environment is more complex, uses custom images, includes Crunchy enterprise features like TDE, or otherwise needs tailored migration steps, contact the Percona team and we will help you plan and execute the move.&lt;/p&gt;
&lt;/blockquote&gt;







&lt;p&gt;&lt;em&gt;This post was originally published on the &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/" rel="noopener noreferrer"&gt;Percona blog&lt;/a&gt;. For the full Try It Out links (Percona PostgreSQL Operator docs, GitHub, public roadmap, issue tracker, community forum), see the bottom of the canonical post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator: Backup-Restore and Persistent Volume Reuse</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Thu, 28 May 2026 13:11:08 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-backup-restore-and-2hp6</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-backup-restore-and-2hp6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk0a97gjauc6jpap856e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk0a97gjauc6jpap856e.png" alt="Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Percona PostgreSQL operator pgBackRest restore is the simplest way to move off the Crunchy Data PostgreSQL Operator: take a full Crunchy backup, point the new Percona cluster's &lt;code&gt;dataSource&lt;/code&gt; at the existing pgBackRest archive, and the cluster bootstraps from it before its first start. This post covers that path, plus a second option, persistent-volume reuse, for cases where you want to skip the data copy entirely.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;part 3 of a 3-part series&lt;/strong&gt; on running PostgreSQL on Kubernetes with a fully open-source operator. &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; walked through the changing open-source landscape and announced the hard fork of the Crunchy Data PostgreSQL Operator into the fully independent Percona PostgreSQL Operator v3.0.0. &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; covered the &lt;strong&gt;standby cluster&lt;/strong&gt; method, the safest migration path when downtime budget is tight.&lt;/p&gt;

&lt;p&gt;This post covers two simpler paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backup and restore&lt;/strong&gt;, the fastest if you can tolerate a short application-downtime window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent volume reuse&lt;/strong&gt;, when you want to skip the data copy entirely and keep the existing PGDATA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are landing here cold, &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;start with part 1&lt;/a&gt; for the why, then read &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; for the standby method. The rest of this post assumes you have already decided to migrate and want a tested playbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tested with
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crunchy Data PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v5.8.x (tested on v5.8.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percona PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v3.x.x (tested on v3.0.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;18 (must match between source and target)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;SeaweedFS (Apache-2.0), or any S3-compatible service. Required for the backup-and-restore method, optional for PV reuse.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt; (v3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Different versions may have slight differences in CR fields or behavior. Always consult the official documentation for the operator and PostgreSQL version you are running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What this post does NOT cover
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Application-side connection-string changes beyond updating to the new pgBouncer service&lt;/li&gt;
&lt;li&gt;Schema-changing upgrades, major PostgreSQL version upgrades, or extension migrations&lt;/li&gt;
&lt;li&gt;Crunchy enterprise-only features like TDE or pgBackRest custom encryption&lt;/li&gt;
&lt;li&gt;Operating two operators against the same namespace before the hard fork. Use Percona PostgreSQL Operator v3.0.0 or higher.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Migration using backup and restore
&lt;/h2&gt;

&lt;p&gt;This is often the fastest and simplest path, especially when you do not need a live standby. You take a full backup of the Crunchy source cluster, then create a Percona cluster that automatically restores from that backup before its first start.&lt;/p&gt;

&lt;p&gt;Data written between the final backup and the application cutover is lost, so the migration window is the time between those two events. For a near-zero-downtime alternative, see &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;part 2: standby cluster method&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivr1nql8r4dkzpnlzqst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivr1nql8r4dkzpnlzqst.png" alt="Backup and restore migration overview" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you begin
&lt;/h3&gt;

&lt;p&gt;Set the namespace once. Every command in this guide reads from this variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres-migration
kubectl create namespace &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy SeaweedFS
&lt;/h3&gt;

&lt;p&gt;Skip this step if you already have an S3-compatible repository (AWS S3, GCS, Ceph). Update the endpoint and credentials in the YAML examples accordingly.&lt;/p&gt;

&lt;p&gt;SeaweedFS provides an S3-compatible object store that runs inside Kubernetes. Both operators will use it as the shared pgBackRest WAL archive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS is required.&lt;/strong&gt; pgBackRest always connects to S3 endpoints over HTTPS, even when &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt; is set (that flag skips certificate verification, it does not fall back to HTTP). The steps below generate a self-signed certificate and pass it to SeaweedFS via Helm values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a self-signed TLS certificate for SeaweedFS S3&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /tmp/seaweedfs.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=seaweedfs-all-in-one"&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; create secret tls seaweedfs-s3-tls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.key

helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;seaweedfs seaweedfs/seaweedfs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 4.23.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-backup-restore/examples/seaweedfs-values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Helm values file in the repo creates the &lt;code&gt;pg-migration&lt;/code&gt; bucket on first start, so no separate &lt;code&gt;aws s3 mb&lt;/code&gt; step is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0. Create pgBackRest secrets
&lt;/h3&gt;

&lt;p&gt;Both operators need credentials to read and write the shared SeaweedFS bucket. Apply the secrets from &lt;code&gt;examples/01-pgbackrest-secrets.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy and edit the file first to set your credentials.&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-backup-restore/examples/01-pgbackrest-secrets.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both contain the same SeaweedFS credentials (&lt;code&gt;pgmigration&lt;/code&gt; / &lt;code&gt;pgmigration123&lt;/code&gt;). For AWS S3, replace those with your IAM access key ID and secret access key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Start with your existing Crunchy Data cluster
&lt;/h3&gt;

&lt;p&gt;If you already have a running Crunchy cluster, ensure its pgBackRest &lt;code&gt;repo1&lt;/code&gt; points at the shared bucket. The &lt;code&gt;repo1-path&lt;/code&gt; value must match the path that will be referenced in the Percona &lt;code&gt;dataSource.pgbackrest.global.repo1-path&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;Optional: deploy the Crunchy operator for testing. &lt;strong&gt;The Helm install below is shown only as a quick way to reproduce this blog post's example. If you are running Crunchy PGO in production, follow &lt;a href="https://access.crunchydata.com/documentation/postgres-operator/latest/installation/" rel="noopener noreferrer"&gt;the official Crunchy Data documentation&lt;/a&gt; for installation. The migration steps in the rest of this post do not depend on how you deployed the source operator.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;pgo &lt;span class="se"&gt;\&lt;/span&gt;
  oci://registry.developers.crunchydata.com/crunchydata/pgo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 5.8.7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;singleNamespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start a fresh source cluster for testing, apply &lt;code&gt;examples/02-crunchy-source-cluster.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-backup-restore/examples/02-crunchy-source-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key pgBackRest settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repo1-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/crunchy-to-percona/repo1&lt;/span&gt;   &lt;span class="c1"&gt;# source repo referenced in Percona dataSource&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-uri-style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;                &lt;span class="c1"&gt;# required for path-style S3 endpoints (SeaweedFS, MinIO)&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-verify-tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;                &lt;span class="c1"&gt;# skip TLS verification for self-signed cert; remove for AWS S3&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;
    &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-migration&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seaweedfs-all-in-one.postgres-migration.svc.cluster.local:8443&lt;/span&gt;
      &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the cluster and its pgBackRest stanza to be ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;postgrescluster/crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Trigger a full backup (the migration cutover point)
&lt;/h3&gt;

&lt;p&gt;This is the backup the Percona cluster will restore from. Stop accepting writes on the application side before triggering it to ensure a consistent snapshot, or accept that data written after this backup will be lost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl annotate postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;job &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;manual,postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3. Deploy the Percona Operator
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;kubectl apply&lt;/code&gt; below pulls the operator manifest from the &lt;code&gt;v3.0.0&lt;/code&gt; release tag. For production deployments, also consult &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/installation.html" rel="noopener noreferrer"&gt;the official Percona Operator for PostgreSQL installation documentation&lt;/a&gt; and pick the release tag that matches the version you plan to run.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/tags/v3.0.0/deploy/bundle.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;deployment percona-postgresql-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4. Create the Percona cluster from the backup
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/03-percona-restored-cluster.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-backup-restore/examples/03-percona-restored-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key section that bootstraps the cluster from the Crunchy backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dataSource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgbackrest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;stanza&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;percona-pgbackrest-secret&lt;/span&gt;
    &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Must match repo1-path in the Crunchy source cluster exactly.&lt;/span&gt;
      &lt;span class="na"&gt;repo1-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/crunchy-to-percona/repo1&lt;/span&gt;
      &lt;span class="na"&gt;repo1-s3-uri-style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
      &lt;span class="na"&gt;repo1-s3-verify-tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;
      &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-migration&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seaweedfs-all-in-one.postgres-migration.svc.cluster.local:8443&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Percona cluster's own backup repository must use a different path from the Crunchy source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;backups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgbackrest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;repo1-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/percona-restored/repo1&lt;/span&gt;   &lt;span class="c1"&gt;# different from Crunchy's path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as the Custom Resource is applied, the cluster is bootstrapped from the storage referenced in &lt;code&gt;dataSource&lt;/code&gt; and then started. Once the cluster becomes ready, you can immediately create new backups; in this case, &lt;code&gt;repo1&lt;/code&gt; from the &lt;code&gt;backups&lt;/code&gt; section will be used as the target repository.&lt;/p&gt;

&lt;p&gt;Wait for the cluster to reach ready state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-restored &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the data was restored successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-restored,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;f&lt;/code&gt;. The cluster is the primary and accepts writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5. Verify the cluster is healthy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-restored &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-restored &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6. Take a post-migration backup
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/04-post-migration-backup.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-backup-restore/examples/04-post-migration-backup.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgbackup/post-migration-backup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Succeeded &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clean recovery baseline on the Percona cluster's own repository. All future PITR restores will use this backup, independent of the Crunchy archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7. Reconnect your application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get service &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-restored,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;pgbouncer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8. Clean up the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;Once the migration is verified and your application is connected to the new cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete postgrescluster crunchy-source &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
helm uninstall pgo &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rollback
&lt;/h3&gt;

&lt;p&gt;Until Step 8, rollback is straightforward: switch the application connection string back to the Crunchy pgBouncer service. The Crunchy primary still holds the authoritative state because no writes were directed at the Percona cluster during the cutover (you stopped writes before Step 2). Any writes the application sent to the Percona cluster after cutover will not be present on Crunchy and would need to be replayed manually.&lt;/p&gt;

&lt;p&gt;After Step 8, rollback requires restoring the Crunchy cluster from a backup, which is feasible because the original &lt;code&gt;repo1&lt;/code&gt; is still in the bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;archive.info missing&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;repo1-path&lt;/code&gt; in &lt;code&gt;dataSource.pgbackrest.global&lt;/code&gt; must match the Crunchy source cluster's &lt;code&gt;repo1-path&lt;/code&gt; exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get postgrescluster crunchy-source &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.backups.pgbackrest.global.repo1-path}'&lt;/span&gt;

kubectl get perconapgcluster percona-restored &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.dataSource.pgbackrest.global.repo1-path}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Restore job fails with TLS errors.&lt;/strong&gt; pgBackRest requires HTTPS even with &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt;. Verify SeaweedFS is reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; s3-check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;perconalab/awscli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    AWS_ACCESS_KEY_ID=pgmigration &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_SECRET_ACCESS_KEY=pgmigration123 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_DEFAULT_REGION=us-east-1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    aws --endpoint-url https://seaweedfs-all-in-one.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.cluster.local:8443 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        --no-verify-ssl &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        s3 ls s3://pg-migration
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cluster stuck in restoring state.&lt;/strong&gt; Check the pgBackRest restore job logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-restored,postgres-operator.crunchydata.com/pgbackrest-restore&lt;span class="o"&gt;=&lt;/span&gt;percona-restored &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; pgbackrest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Data missing after restore.&lt;/strong&gt; The restore captures data up to the latest backup. If post-backup data is critical, re-run the backup on the Crunchy cluster after quiescing writes, then delete and recreate the Percona cluster to restore from the newer backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Migration using existing persistent volumes
&lt;/h2&gt;

&lt;p&gt;This method reuses the Crunchy primary's PGDATA persistent volume directly. It avoids a full backup-restore cycle: you retain the Crunchy primary's PV, delete the Crunchy cluster, then create a Percona cluster whose PVC binds to that same PV. PostgreSQL starts on the existing data directory without any restore step.&lt;/p&gt;

&lt;p&gt;It is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want to avoid copying data&lt;/li&gt;
&lt;li&gt;your storage is very large&lt;/li&gt;
&lt;li&gt;you must preserve the original data directory exactly&lt;/li&gt;
&lt;li&gt;you removed the cluster but kept the PV&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2obj1w9ohfda0h6cbnpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2obj1w9ohfda0h6cbnpz.png" alt="PV reuse migration overview" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you begin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres-migration
kubectl create namespace &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1. Deploy the Crunchy and Percona operators
&lt;/h3&gt;

&lt;p&gt;Both operators run in the same namespace. Crunchy PGO is uninstalled during the migration once the PV is retained.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note (Crunchy)&lt;/strong&gt;: The Helm install for Crunchy PGO below is shown only as a quick way to reproduce this blog post's example. If you are running Crunchy PGO in production, follow &lt;a href="https://access.crunchydata.com/documentation/postgres-operator/latest/installation/" rel="noopener noreferrer"&gt;the official Crunchy Data documentation&lt;/a&gt; for installation. The migration steps in the rest of this post do not depend on how you deployed the source operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note (Percona)&lt;/strong&gt;: The &lt;code&gt;kubectl apply&lt;/code&gt; of the Percona operator below uses &lt;code&gt;3.0.0&lt;/code&gt; from the operator repo for reproducibility of this guide. For production deployments, follow &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/installation.html" rel="noopener noreferrer"&gt;the official Percona Operator for PostgreSQL installation documentation&lt;/a&gt; and pin to the latest released version.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;pgo &lt;span class="se"&gt;\&lt;/span&gt;
  oci://registry.developers.crunchydata.com/crunchydata/pgo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;singleNamespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;

kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/tags/v3.0.0/deploy/bundle.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;deployment pgo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;deployment percona-postgresql-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Start the Crunchy source cluster
&lt;/h3&gt;

&lt;p&gt;If you already have a running Crunchy cluster with &lt;code&gt;replicas: 1&lt;/code&gt;, proceed to Step 3.&lt;/p&gt;

&lt;p&gt;To start a fresh cluster for testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-pv/examples/01-crunchy-source-cluster.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;master &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3. Stop writes and identify the primary PV
&lt;/h3&gt;

&lt;p&gt;Stop your application from writing to the database. This is the start of the downtime window. Then identify the primary pod, its PVC, and the backing PV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;master &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PVC_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.volumes[?(@.name=="postgres-data")].persistentVolumeClaim.claimName}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pvc &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PVC_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.volumeName}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Primary pod: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PVC:         &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PVC_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PV:          &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4. Configure the source cluster to retain PVs
&lt;/h3&gt;

&lt;p&gt;If you want to delete the Crunchy source cluster but keep the persistent volumes, the PV reclaim policy must be set to &lt;code&gt;Retain&lt;/code&gt;. For dynamically provisioned PersistentVolumes, the default reclaim policy is &lt;code&gt;Delete&lt;/code&gt;, which removes the data once there are no more PersistentVolumeClaims associated with the PV.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'&lt;/span&gt;

kubectl get pv &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the Crunchy cluster and uninstall PGO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch postgrescluster crunchy-source &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"op":"remove","path":"/metadata/finalizers"}]'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;kubectl delete postgrescluster crunchy-source &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
helm uninstall pgo &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the PVC is deleted, the PV enters &lt;code&gt;Released&lt;/code&gt; state. A &lt;code&gt;Released&lt;/code&gt; PV retains its old &lt;code&gt;claimRef&lt;/code&gt; and cannot be claimed by a new PVC until it is cleared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"op":"remove","path":"/spec/claimRef"}]'&lt;/span&gt;

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.phase}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Label the PV so the Percona PVC selector binds to it exclusively. This prevents accidental binding to another available volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; percona-pv-migration&lt;span class="o"&gt;=&lt;/span&gt;migrated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5. Create the Percona cluster with the retained volume
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-pv/examples/02-percona-migrated-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key section that binds the PVC to the labelled PV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;instance1&lt;/span&gt;
    &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;dataVolumeClaimSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;percona-pv-migration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Percona Operator creates a PVC with that selector. The PVC binds to the labelled PV, and PostgreSQL starts on the existing PGDATA directory with no restore needed. pgBackRest uses a local PVC-backed repository (&lt;code&gt;repo1.volume&lt;/code&gt;), so no S3 credentials or external storage are required, but you can use S3 storage as well.&lt;/p&gt;

&lt;p&gt;Wait for the cluster to become ready and verify the data is intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-migrated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s

&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-migrated,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;f&lt;/code&gt;. The cluster is the primary and accepts writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6. Scale up replicas
&lt;/h3&gt;

&lt;p&gt;The cluster started with a single replica to reuse the migrated PV. Once the primary is healthy, drop the PVC selector and scale out so the operator can provision fresh replica volumes from the storage class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch perconapgcluster percona-migrated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[
    {"op":"remove","path":"/spec/instances/0/dataVolumeClaimSpec/selector"},
    {"op":"replace","path":"/spec/instances/0/replicas","value":3}
  ]'&lt;/span&gt;

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-migrated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing the selector here is important: leaving it in place would cause the new replica PVCs to fail provisioning because no other PV carries the migration label.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7. Take a post-migration backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-pv/examples/03-post-migration-backup.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgbackup/post-migration-backup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Succeeded &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the first backup on the Percona cluster's local pgBackRest repository, establishing a baseline for future PITR restores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8. Reconnect your application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get service &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-migrated,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;pgbouncer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9. Cleanup
&lt;/h3&gt;

&lt;p&gt;After the migration is verified, remove the migration label from the PV (Step 6 already removed the PVC selector that depended on it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; percona-pv-migration-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rollback
&lt;/h3&gt;

&lt;p&gt;PV migration is the least rollback-friendly of the three methods. Once the Percona cluster has started writing to the PGDATA directory, the original Crunchy timeline is gone. If you need a way back, take a Crunchy-side pgBackRest backup before Step 4 and treat that backup as your rollback point. Recovery is then a fresh Crunchy cluster restored from that backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PVC stays in &lt;code&gt;Pending&lt;/code&gt; state.&lt;/strong&gt; The PVC selector did not match the labelled PV. Verify the label and PV phase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--show-labels&lt;/span&gt;
kubectl get pv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PV_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.phase}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PostgreSQL fails to start (data directory errors).&lt;/strong&gt; Check the database container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; logs &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Crunchy cluster was shut down uncleanly, there may be incomplete WAL. Patroni will attempt crash recovery automatically; check the logs for progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PV was deleted before setting &lt;code&gt;Retain&lt;/code&gt;.&lt;/strong&gt; If the PV was deleted along with the PVC (default &lt;code&gt;Delete&lt;/code&gt; policy), the data is gone and PV migration is no longer possible. Use the backup-and-restore migration above, restoring from the most recent pgBackRest backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Two more migration paths from the Crunchy Data PostgreSQL Operator to the fully open-source Percona PostgreSQL Operator. Combined with &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-standby-cluster/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;, the series gives you three production-tested options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standby cluster&lt;/strong&gt; (part 2): near-zero downtime via streaming replication and pgBackRest standby&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup and restore&lt;/strong&gt; (this post): the simplest path, restoring directly from a Crunchy pgBackRest backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent volume reuse&lt;/strong&gt; (this post): when you want to keep storage and skip the data copy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three approaches are safe, predictable, and reversible, with the rollback caveats noted in each section. Because Percona's operator, images, and tooling are 100 percent open source, you keep full control: you can always migrate back to the Crunchy operator, or out to another open-source operator (Zalando, StackGres, CloudNativePG) using the same patterns. That last journey is a topic for a future post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post covers basic deployment patterns and simplified configuration examples. If your environment uses custom images, Crunchy enterprise features, or otherwise needs tailored migration steps, contact the Percona team and we will help you plan and execute the move.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Percona Operator for PostgreSQL docs&lt;/strong&gt;: &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/" rel="noopener noreferrer"&gt;https://docs.percona.com/percona-operator-for-postgresql/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/percona/percona-postgresql-operator" rel="noopener noreferrer"&gt;https://github.com/percona/percona-postgresql-operator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public roadmap&lt;/strong&gt;: &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;https://github.com/orgs/percona/projects/10/views/6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Forum&lt;/strong&gt;: &lt;a href="https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68" rel="noopener noreferrer"&gt;https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator: The Standby Cluster Method</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Wed, 27 May 2026 12:38:21 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-the-standby-cluster-2k5h</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-the-standby-cluster-2k5h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuem9x3be7wsdf21sv83f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuem9x3be7wsdf21sv83f.png" alt="Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Crunchy to Percona PostgreSQL migration is more straightforward than most cross-operator moves on Kubernetes, because the Percona PostgreSQL Operator is a hard fork of the Crunchy Data PostgreSQL Operator. Same Patroni HA, same pgBackRest backups, same overall CRD shape. This post walks through the safest of the three migration paths: a standby cluster method with near-zero downtime.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;part 2 of a 3-part series&lt;/strong&gt; on running PostgreSQL on Kubernetes with a fully open-source operator. &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; walked through the changing open-source landscape and announced the hard fork of the Crunchy Data PostgreSQL Operator into the fully independent Percona PostgreSQL Operator &lt;a href="https://github.com/percona/percona-postgresql-operator/tree/v3.0.0" rel="noopener noreferrer"&gt;v3.0.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is the first practical playbook of the series. It covers the &lt;strong&gt;standby cluster method&lt;/strong&gt;, the safest migration path when downtime budget is tight. &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt; covers two simpler paths: backup-and-restore and persistent-volume reuse.&lt;/p&gt;

&lt;p&gt;If you are landing here without context on why you might want to migrate at all, &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;start with part 1&lt;/a&gt;. The rest of this post assumes you have already decided to move and want a tested playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration approach in one paragraph
&lt;/h2&gt;

&lt;p&gt;The Percona PostgreSQL Kubernetes Operator is a hard fork of the Crunchy Data PostgreSQL Kubernetes Operator, which simplifies the migration paths considerably: the same underlying tools (Patroni, pgBackRest, PgBouncer) and the same overall design are used in both operators. All three migration paths in this series are reversible: because Percona's operator is fully open source and remains compatible with the same backup format, the move back to Crunchy is also possible if your team decides to walk it back.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on the storage layer
&lt;/h3&gt;

&lt;p&gt;All examples in this guide use an in-cluster &lt;a href="https://github.com/seaweedfs/seaweedfs" rel="noopener noreferrer"&gt;SeaweedFS&lt;/a&gt; instance as the pgBackRest S3 repository. SeaweedFS is Apache-2.0 licensed, actively maintained, and a clean drop-in replacement for the role MinIO used to fill in this stack. Any other S3-compatible storage works just as well: AWS S3, Google Cloud Storage (via HMAC keys), Ceph RadosGW, Cloudflare R2, and so on. For non-SeaweedFS endpoints, remove &lt;code&gt;repo1-s3-uri-style: path&lt;/code&gt; and &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt; from the pgBackRest configuration and replace the endpoint with your provider's URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this series does NOT cover
&lt;/h3&gt;

&lt;p&gt;To keep scope honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application-side connection-string changes beyond updating to the new pgBouncer service. If your app uses connection-pool tuning, custom auth, or a service mesh, that work stays with you.&lt;/li&gt;
&lt;li&gt;Schema-changing upgrades, major PostgreSQL version upgrades, or extension migrations. The PostgreSQL major version must match between source and target.&lt;/li&gt;
&lt;li&gt;Crunchy enterprise-only features like TDE, Crunchy Postgres for Kubernetes specific operators, or pgBackRest custom encryption. If your environment uses these, contact the Percona team for a tailored plan.&lt;/li&gt;
&lt;li&gt;Operating two operators against the same namespace before the hard fork. Use Percona PostgreSQL Operator v3.0.0 or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tested with
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crunchy Data PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v5.8.x (tested on v5.8.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percona PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v3.x.x (tested on v3.0.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;18 (must match between source and target)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;SeaweedFS (Apache-2.0), or any other S3-compatible service accessible from all cluster pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt; (v3), &lt;code&gt;yq&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Different versions may have slight differences in CR fields or behavior. Always consult the official documentation for the operator and PostgreSQL version you are running.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration using a standby cluster
&lt;/h2&gt;

&lt;p&gt;This is the safest method when downtime budget is tight. The Percona cluster is brought up as a standby of the Crunchy primary, catches up via pgBackRest plus streaming replication, and is promoted at cutover. The only downtime is the cutover step itself.&lt;/p&gt;

&lt;p&gt;You can wire the standby in two ways, and combining both gives you maximum safety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pgBackRest repo-based standby&lt;/strong&gt; seeds the standby from the latest base backup and replays archived WAL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming replication&lt;/strong&gt; keeps the standby in sync with the live primary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F350v7cc0v3ecb22uugwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F350v7cc0v3ecb22uugwu.png" alt="Standby cluster migration overview" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you begin
&lt;/h3&gt;

&lt;p&gt;Set the target namespace once. Every command in this guide reads from this variable so you can change it in a single place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres-migration
kubectl create namespace &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy SeaweedFS
&lt;/h3&gt;

&lt;p&gt;Skip this step if you already have an S3-compatible repository (AWS S3, GCS, Ceph). Update the endpoint and credentials in the YAML examples accordingly.&lt;/p&gt;

&lt;p&gt;SeaweedFS provides an S3-compatible object store that runs inside Kubernetes. Both operators will use it as the shared pgBackRest WAL archive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS is required.&lt;/strong&gt; pgBackRest always connects to S3 endpoints over HTTPS, even when &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt; is set (that flag skips certificate verification, it does not fall back to HTTP). The steps below generate a self-signed certificate and pass it to SeaweedFS via Helm values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a self-signed TLS certificate for SeaweedFS S3&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /tmp/seaweedfs.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=seaweedfs-all-in-one"&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; create secret tls seaweedfs-s3-tls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.key

helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;seaweedfs seaweedfs/seaweedfs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 4.23.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/seaweedfs-values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Helm values file in the repo creates the &lt;code&gt;pg-migration&lt;/code&gt; bucket on first start, so no separate &lt;code&gt;aws s3 mb&lt;/code&gt; step is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0. Create pgBackRest secrets
&lt;/h3&gt;

&lt;p&gt;Both operators need credentials to read and write the shared SeaweedFS bucket. Apply the secrets from &lt;code&gt;examples/01-pgbackrest-secret.yaml&lt;/code&gt; after filling in your access key and secret key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy and edit the file first to set your credentials.&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/01-pgbackrest-secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both secrets contain the same SeaweedFS credentials (&lt;code&gt;pgmigration&lt;/code&gt; / &lt;code&gt;pgmigration123&lt;/code&gt;). For AWS S3, replace those with your IAM access key ID and secret access key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Start with your existing Crunchy Data cluster
&lt;/h3&gt;

&lt;p&gt;If you already have a running Crunchy cluster, ensure its pgBackRest &lt;code&gt;repo1&lt;/code&gt; points at the shared bucket and path. The &lt;code&gt;repo1-path&lt;/code&gt; value must be identical in both cluster specs. Mismatched paths will prevent the Percona standby from finding the WAL archive.&lt;/p&gt;

&lt;p&gt;Optional: deploy a Crunchy operator to test the migration end to end. &lt;strong&gt;The Helm install below is shown only as a quick way to reproduce this blog post's example. If you are running Crunchy PGO in production, follow &lt;a href="https://access.crunchydata.com/documentation/postgres-operator/latest/installation/" rel="noopener noreferrer"&gt;the official Crunchy Data documentation&lt;/a&gt; for installation. The migration steps in the rest of this post do not depend on how you deployed the source operator.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;pgo &lt;span class="se"&gt;\&lt;/span&gt;
  oci://registry.developers.crunchydata.com/crunchydata/pgo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 5.8.7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;singleNamespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply &lt;code&gt;examples/02-crunchy-source-cluster.yaml&lt;/code&gt; (or adapt your existing cluster's pgBackRest config):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/02-crunchy-source-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key pgBackRest settings in the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repo1-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/crunchy-to-percona/repo1&lt;/span&gt;   &lt;span class="c1"&gt;# shared path, must match Percona side&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-uri-style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;                &lt;span class="c1"&gt;# required for path-style S3 endpoints (SeaweedFS, MinIO)&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-verify-tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;                &lt;span class="c1"&gt;# skip TLS verification for self-signed cert; remove for AWS S3&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;
    &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-migration&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seaweedfs-all-in-one.postgres-migration.svc.cluster.local:8443&lt;/span&gt;
      &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the cluster to be ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Trigger a full backup on the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;Wait for the pgBackRest stanza to be created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;postgrescluster/crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a full backup before creating the Percona standby. This gives the standby a recent base to restore from, so it only needs to replay a small amount of WAL to catch up. This matches the realistic production migration pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl annotate postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the backup job to complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;job &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;manual,postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3. Copy TLS certificates (cross-namespace only)
&lt;/h3&gt;

&lt;p&gt;If the Percona cluster is in a different namespace from the Crunchy cluster, copy the Crunchy TLS secrets to the Percona namespace. These allow mutual TLS authentication during streaming replication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;secret &lt;span class="k"&gt;in &lt;/span&gt;crunchy-source-cluster-cert crunchy-source-replication-cert&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;kubectl get secret &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;CRUNCHY_NS&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json | &lt;span class="se"&gt;\&lt;/span&gt;
    yq &lt;span class="s1"&gt;'{"apiVersion": .apiVersion, "kind": .kind, "data": .data,
         "metadata": {"name": .metadata.name}, "type": .type}'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="se"&gt;\&lt;/span&gt;
    kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both clusters are in the same namespace, skip this step. The secrets are already accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. Deploy the Percona PG Operator
&lt;/h3&gt;

&lt;p&gt;The Crunchy PGO operator can stay in the same or a different namespace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;kubectl apply&lt;/code&gt; below pulls the operator manifest from the &lt;code&gt;migration-from-crunchy-guide&lt;/code&gt; branch of the operator repo, which is the source for this guide's examples. For production deployments, follow &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/installation.html" rel="noopener noreferrer"&gt;the official Percona Operator for PostgreSQL installation documentation&lt;/a&gt; and pin to a released version tag rather than a feature branch.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/tags/v3.0.0/deploy/bundle.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the operator deployment is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;deployment percona-postgresql-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5. Create the Percona cluster in standby mode
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/03-percona-standby-cluster.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/03-percona-standby-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key settings that wire the Percona cluster to the Crunchy source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;standby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;repoName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;                                         &lt;span class="c1"&gt;# restore initial base backup from this repo&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-ha.postgres-migration.svc.cluster.local&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;

&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;customTLSSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-cluster-cert&lt;/span&gt;                     &lt;span class="c1"&gt;# Crunchy CA for mutual TLS&lt;/span&gt;
  &lt;span class="na"&gt;customReplicationTLSSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-replication-cert&lt;/span&gt;                 &lt;span class="c1"&gt;# cert for _crunchyreplication user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Percona operator will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restore the base backup from the SeaweedFS bucket.&lt;/li&gt;
&lt;li&gt;Replay WAL from SeaweedFS until it catches up with the live Crunchy cluster.&lt;/li&gt;
&lt;li&gt;Switch to streaming replication from &lt;code&gt;crunchy-source-ha&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wait for the cluster to reach ready state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that data is replicating to the standby:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;t&lt;/code&gt; (in recovery) and a non-null LSN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6. Verify replication lag before cutover
&lt;/h3&gt;

&lt;p&gt;Query the Crunchy primary to confirm the Percona standby has caught up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;master &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    SELECT
        client_addr,
        state,
        pg_wal_lsn_diff(sent_lsn, replay_lsn) AS byte_lag,
        write_lag,
        flush_lag,
        replay_lag
    FROM pg_stat_replication;
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proceed to the next step only when &lt;code&gt;write_lag&lt;/code&gt; and &lt;code&gt;replay_lag&lt;/code&gt; are NULL or under a few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7. Cutover the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;This is the only step that causes downtime. Stop accepting writes on the application side, then patch the Crunchy cluster into standby mode. Patroni steps down and archives the final WAL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"standby": {"enabled": true, "repoName": "repo1"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify demotion (poll until &lt;code&gt;pg_is_in_recovery()&lt;/code&gt; returns &lt;code&gt;t&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8. (Optional) Shut down the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;Once the Percona standby has replayed all WAL, shut down the Crunchy cluster to prevent split-brain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"shutdown": true}}'&lt;/span&gt;

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9. Promote the Percona cluster
&lt;/h3&gt;

&lt;p&gt;Confirm that the Percona standby has finished replaying all WAL (the LSN stops advancing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_last_wal_replay_lsn();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this a few times. When the LSN is stable, replay is complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch perconapgcluster percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"standby": {"enabled": false}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the cluster to become ready and confirm it is writable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;480s

&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;f&lt;/code&gt; (the cluster is now the primary and accepts writes).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10. Verify stanza creation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 11. Take a post-migration backup
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/04-post-migration-backup.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/04-post-migration-backup.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgbackup/post-migration-backup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Succeeded &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clean recovery point on the new timeline. All future PITR restores will use this backup as their starting point, independent of the old Crunchy WAL archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconnecting your application
&lt;/h3&gt;

&lt;p&gt;Update your application's connection string to point at the Percona cluster's pgBouncer service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get service &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;pgbouncer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This migration path works almost entirely out of the box. For users coming from the Crunchy Data PostgreSQL Operator, this method feels familiar because it leverages the same standby/replica mechanisms used for HA and disaster recovery. The key difference is that you can now use this familiar mechanism to migrate safely to the Percona PostgreSQL Operator, a fully open-source alternative running on a fully open-source storage layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback
&lt;/h3&gt;

&lt;p&gt;The standby method is the most rollback-friendly of the three. Until you take the post-migration backup, the Crunchy cluster still holds the original timeline. To roll back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop writes on the Percona side and patch the Percona cluster back into standby mode (&lt;code&gt;spec.standby.enabled: true&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Patch the Crunchy cluster out of standby mode and let Patroni promote it.&lt;/li&gt;
&lt;li&gt;Verify with &lt;code&gt;pg_is_in_recovery()&lt;/code&gt; on both sides.&lt;/li&gt;
&lt;li&gt;Switch the application connection string back to the Crunchy pgBouncer service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After Step 11 (post-migration backup), the timelines have diverged. From that point, the rollback story is the same as a fresh restore, and you should treat the Crunchy cluster as a historical reference, not a live target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Percona standby not connecting to the Crunchy primary.&lt;/strong&gt; Verify the &lt;code&gt;crunchy-source-ha&lt;/code&gt; service resolves from within the Percona pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"getent hosts crunchy-source-ha.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.cluster.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replication authentication errors.&lt;/strong&gt; The Percona standby authenticates as the &lt;code&gt;_crunchyreplication&lt;/code&gt; PostgreSQL user using the certificate in &lt;code&gt;crunchy-source-replication-cert&lt;/code&gt;. Verify the secret exists and matches what the Crunchy operator generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secret crunchy-source-replication-cert &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pgBackRest restore fails.&lt;/strong&gt; Confirm both secrets contain identical credentials and that &lt;code&gt;repo1-path&lt;/code&gt; is the same in both cluster specs (&lt;code&gt;/crunchy-to-percona/repo1&lt;/code&gt; in this guide). Mismatched paths cause an &lt;code&gt;archive.info missing&lt;/code&gt; error. Verify the bucket is reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; s3-check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;perconalab/awscli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    AWS_ACCESS_KEY_ID=pgmigration &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_SECRET_ACCESS_KEY=pgmigration123 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_DEFAULT_REGION=us-east-1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    aws --endpoint-url https://seaweedfs-all-in-one.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.cluster.local:8443 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        --no-verify-ssl &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        s3 ls s3://pg-migration
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timeline history file (&lt;code&gt;00000002.history&lt;/code&gt;) missing after promotion.&lt;/strong&gt; This is a known &lt;a href="https://github.com/CrunchyData/postgres-operator/issues/4472" rel="noopener noreferrer"&gt;issue&lt;/a&gt; with Crunchy PGO's async archive mode. After promotion, push the history file synchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    pgbackrest --stanza=db --no-archive-async &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
      archive-push &lt;/span&gt;&lt;span class="se"&gt;\"\$&lt;/span&gt;&lt;span class="s2"&gt;{PGDATA}/pg_wal/00000002.history&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; || true
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This was the safest migration path. &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt; covers two simpler options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backup and restore&lt;/strong&gt;. The simplest path. You take a Crunchy pgBackRest backup and the Percona cluster bootstraps from it. Cutover is the time between the final backup and pointing the application at the new cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent volume reuse&lt;/strong&gt;. For when you want to skip the data copy entirely. The Percona cluster takes over the existing PGDATA volume, no restore step required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the method that fits your downtime budget, data size, and storage layout.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post covers basic deployment patterns and simplified configuration examples. If your environment is more complex, uses custom images, includes Crunchy enterprise features like TDE, or otherwise needs tailored migration steps, contact the Percona team and we will help you plan and execute the move.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Percona Operator for PostgreSQL docs&lt;/strong&gt;: &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/" rel="noopener noreferrer"&gt;https://docs.percona.com/percona-operator-for-postgresql/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/percona/percona-postgresql-operator" rel="noopener noreferrer"&gt;https://github.com/percona/percona-postgresql-operator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue tracker&lt;/strong&gt;: &lt;a href="https://github.com/percona/percona-postgresql-operator/issues" rel="noopener noreferrer"&gt;https://github.com/percona/percona-postgresql-operator/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public roadmap&lt;/strong&gt;: &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;https://github.com/orgs/percona/projects/10/views/6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Forum&lt;/strong&gt;: &lt;a href="https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68" rel="noopener noreferrer"&gt;https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
