Guide

Migrate a self-managed PostgreSQL database to klickops

8 min read postgresqlmigrationpg_dumpmanaged database

You have a PostgreSQL database running on a VM, a droplet, or a box in a rack, and you want it on managed infrastructure without a rewrite. This is the whole path: how to get the data across, what happens to your roles and ownership, and how to cut over your app, using standard pg_dump and the klickops CLI. Nothing exotic, no downtime beyond a short restore window.

source → your machine → klickops. The data never has to face the internet.

Pick your path: can the cluster reach your database?

There is one fork that decides everything, and it is not about size. It is whether the klickops cluster can open a connection to your source database over the network.

A private address like 10.x.x.x or 192.168.x.x means “only from my machine”. That is fine, and arguably safer: your database never has to be exposed to move it.

Can klickops open a connection to your source database?
Yes → server-side Publicly reachable Public IP or DNS with the port open. klickops pulls the data itself, you paste one connection string and never touch a local tool.
No → through you Reachable only from your machine A private IP, VPN, or firewalled VM. Your machine sees both sides, so you dump locally and stream it in. The common, safer case.

Look at what you are moving

From a machine that can reach the source, connect and list the databases so you know exactly what you are copying. Let psql prompt for the password so it stays out of your shell history.

You will get back the real databases with their sizes, for example an app database called myapp at a few hundred megabytes. Note the one you want; that is your <source-db> below.

list databases + sizes on the source
# connects to the always-present maintenance DB just to look around
psql -h db.internal -U admin -d postgres -c \
  "SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
   FROM pg_database WHERE NOT datistemplate ORDER BY 2 DESC;"

Move the data

Create a managed PostgreSQL database in your klickops project first (the wizard's defaults are production-shaped, you only pick a size). Then find the target with the CLI, and run the migration itself as a single pipe, dump from the source, stream straight into klickops, which runs the restore inside the cluster:

You will see pg_restore progress line by line, then “Import complete”. The --skip-ownership flag is on by default, which matters (see the next section).

Publicly reachable instead? Skip the local dump entirely. On the database's Data & backups tab, choose Import → From another server and paste the source connection string; klickops copies it server-side and you can close the browser while it runs. The same route is best for very large databases, where a local upload would be the slow part.

find the klickops target database
klops databases list --project my-project
dump + import in one command
pg_dump -W --format=custom -h db.internal -U admin -d myapp \
  | klops db import myapp-db --project my-project --file -

What happens to roles and ownership

This trips people up, so it is worth being explicit: your database roles do not migrate, and they should not. A single-database pg_dump does not include roles at all (they are cluster-global objects), and --skip-ownership strips the ownership and grant references on top of that.

The result is clean: every table lands owned by klickops's managed app role, which is exactly the role your application connects as on klickops. The admin user from your old VM is irrelevant to the new database. After the move, point your app at klickops's connection string (host, port, database and user app, with the password revealed on the database's Connect tab), and that is the entire “user” change.

Need more than one login role (a read-only reporting user, say)? Recreate it in klickops as a managed role rather than importing it; the platform manages the password and keeps it in sync. For a single app talking to a single database, you do not need to.

Comes across

  • Tables & every row of data
  • Indexes & constraints
  • Sequences & their values
  • Functions, types, extensions

Left behind

  • Database roles / users
  • Object ownership
  • GRANTs & privileges
  • The old admin password
Everything ends up owned by klickops's managed app role, the one your application connects as.

If the target is not empty

If you already deployed your app to klickops before migrating, it likely ran its own migrations and created the schema. Importing on top of that collides on the first object that already exists. Two things fix it: stop the app first (scale it to zero so nothing writes while the tables drop and reload, then scale it back), and add --clean to the import, which drops each object before recreating it in dependency order so the reload starts from a clean slate.

reload over an existing schema
pg_dump -W --format=custom -h db.internal -U admin -d myapp \
  | klops db import myapp-db --project my-project --file - --clean

Verify and cut over

Compare a couple of row counts against the source to be sure everything landed. If the counts match, cut over: update your app's connection settings to the klickops app credentials from the previous section and start it back up. It reconnects to the populated database and you are done.

One thing to know about a very large table: a browser upload is capped, and a single enormous table can be slow to stream. For multi-gigabyte databases, prefer the server-side “From another server” copy (if reachable) or a port-forward with a local pg_restore; both move the data as a continuous stream with no upload ceiling.

sanity-check on klickops
klops db query myapp-db --project my-project -- \
  "SELECT count(*) FROM users;"

Ready to try it on Swiss infrastructure?

Private beta, free plan for one small stack, no card required.

Join the waitlist