Back to blog

Home Lab

Moving Application Databases to Shared PostgreSQL

Consolidating databases reduced duplicated operational work, but it also made isolation, backups, credentials, and upgrade planning explicit responsibilities.

One database per application feels clean until there are enough applications to make “clean” expensive. Every new service brings another container, another volume, another password, another backup job, and another upgrade path.

I moved the application databases onto a shared PostgreSQL service to reduce that repetition. The move was useful precisely because it forced assumptions into the open: shared infrastructure needs stronger boundaries than a collection of incidental database containers.

The shape of the service

application-a  →  database-a  →  role-a
application-b  →  database-b  →  role-b
application-c  →  database-c  →  role-c

                    PostgreSQL

The instance is shared. The application credentials are not. Each service gets its own database, role, Kubernetes Secret, and network authorization. That does not protect against a compromised PostgreSQL administrator or a server-level failure, but it prevents an ordinary application credential from becoming a key to every database.

I kept the administration identity separate from application roles. “The application needs to run migrations” is not a good reason to give it the same privileges used to manage the entire instance.

Before changing a connection string

The old database contract was larger than the hostname and password. I recorded the database name, role, extensions, collation, schema ownership, migration behavior, connection limits, and the parts of the application that actually exercised the database.

This caught the kind of failure that a readiness probe misses. An application can connect, report healthy, and still fail on its first migration or on a feature that expects an extension to exist. Compatibility had to be tested by using the application, not just by starting the pod.

Keep separateWhy it matters
Database and roleA compromised application should not automatically inherit another application’s data.
Application and administrator secretsCredential rotation and emergency access remain independent.
Logical export and physical backupOne supports selective recovery; the other supports efficient service restoration.
Database service and application deploymentA database upgrade should not be an accidental application release.

The cutover was a small operation, not a single command

For the smaller databases, dump and restore was the most transparent option. It created an artifact I could inspect, restore into a clean target, and use to test the application before the real switch. Replication may be worthwhile for a busier service, but the migration method should match the downtime requirement.

  1. Create the target database and least-privilege role.
  2. Restore a representative export and run the application against it.
  3. Pause writes for the final export.
  4. Restore the final data, repair ownership, and update the encrypted application Secret.
  5. Verify reads, writes, migrations, background jobs, and connection behavior.
  6. Keep the old database read-only until the rollback window closes.

The write pause was the important part. A live export followed by an immediate client switch can lose transactions that arrived after the dump began. A clear cutover boundary is easier to reason about than an optimistic assumption that the application was quiet enough.

Centralizing the work—and the blast radius

Shared PostgreSQL simplified patching, monitoring, and backup policy. It also made an outage more consequential. One exhausted connection pool, failed upgrade, storage problem, or poorly tested extension change could affect several applications at once.

That changed what I watched: connection usage, slow queries, failed authentication, storage health, backup freshness, restore results, and activity by database. Connection limits became a platform concern rather than something every application could consume independently.

Major PostgreSQL upgrades are migrations. Application compatibility, extension support, dump-and-restore tests, rollback limits, and the maintenance window need to be known before changing the server image.

Where shared stops making sense

Not every workload belongs on the shared service. An application with unusual extensions, incompatible version requirements, extreme resource behavior, strict isolation needs, or an independent lifecycle may be better served by its own PostgreSQL instance.

The useful boundary is operational, not ideological. Shared PostgreSQL is a good default when compatible applications can live behind separate databases, roles, secrets, and policies—and when the platform has a tested recovery path.

Centralization reduced repetition. It did not eliminate responsibility; it made responsibility visible.

Further Reading

PostgreSQL backup and restore

PostgreSQL roles and privileges