From d37cfb837562eea5c416e84a04190411c4b72689 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Mon, 21 Sep 2026 01:36:21 +0100 Subject: [PATCH 01/13] docs: Add guide for migrating a self-hosted database to Cloud --- .../guides/migrate-a-self-hosted-database.md | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 cloud_docs/guides/migrate-a-self-hosted-database.md diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md new file mode 100644 index 00000000..097a6208 --- /dev/null +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -0,0 +1,232 @@ +--- +sidebar_position: 5 +sidebar_label: Migrate a self-hosted database +description: Database migration from a self-hosted Serverpod server to Serverpod Cloud. Copy your data, users, and auth secrets with pg_dump and pg_restore. +--- + +# Migrate a self-hosted database + +You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS. This guide moves your data, your users, and their sessions into a new Serverpod Cloud project. + +You deploy your project to Cloud first, so Cloud creates the tables from your migrations. Then you copy the rows from your old database into those tables. + +## Before you start + +You need: + +- The Serverpod Cloud CLI set up and authenticated. See [Set up the Cloud CLI](/cloud/getting-started/installation). +- Your Serverpod project on your machine, with the same code and migrations that run on your server. +- Shell access to the server that runs your database. +- The PostgreSQL client tools (`pg_dump`, `pg_restore`, and `psql`) on your machine. Use the same major version as your self-hosted database or newer. See [PostgreSQL downloads](https://www.postgresql.org/download/). + +The commands below use example names. Your database runs in a Docker Compose service called `postgres`, and your server runs in a service called `server`. The database is called `my_project`. Replace these names with your own. + +Run the `docker compose` commands on your server, and the `serverpod cloud` commands from your server directory on your machine. + +## Deploy your project to Cloud + +Create the project with the database enabled, and deploy it: + +```bash +serverpod cloud launch +``` + +Cloud applies your migrations on deploy. Your tables now exist on Cloud, with no app data in them yet. See [Deploy your first app](/cloud/getting-started/launch) for the full walkthrough. + +Your self-hosted database must be on the same migration versions. Check which versions it has: + +```bash +docker compose exec postgres psql -U postgres -d my_project \ + -c "SELECT module, version FROM serverpod_migrations ORDER BY module;" +``` + +You run the same query against Cloud later in this guide. If the versions differ, bring your server up to date and deploy the same migrations to both. + +## Copy your auth secrets to Cloud + +Cloud generates its own auth secrets for a new project. Your users' passwords and sessions depend on the secrets from your server, so they stop working with the new ones: + +- Signing in with a correct password fails with `invalidCredentials`. +- Refreshing a session fails with `RefreshTokenInvalidSecretException`, and the server deletes that refresh token. + +Copy the values from the `production` section of your server's `config/passwords.yaml`, or from the matching `SERVERPOD_PASSWORD_*` environment variables: + +```bash +serverpod cloud password set emailSecretHashPepper "" +serverpod cloud password set jwtHmacSha512PrivateKey "" +serverpod cloud password set jwtRefreshTokenHashPepper "" +``` + +Set every other password your server reads the same way, for example `serverSideSessionKeyHashPepper` or the client secrets for your sign-in providers. Then deploy, so the server picks up the new values: + +```bash +serverpod cloud deploy +``` + +:::warning + +Copy the secrets before you restore any users. When a session refresh fails on a mismatched secret, the server deletes that refresh token. The user is signed out and has to sign in again, and setting the secrets afterwards doesn't bring the session back. + +::: + +## Stop your self-hosted server + +Stop the server, so no new rows are written after you take the dump. Keep PostgreSQL running: + +```bash +docker compose stop server +``` + +Your API is offline from here until your apps point at Cloud. + +## Dump your data + +Take a data-only dump. Leave out the data Serverpod keeps about each deployment, such as logs, health checks, and migration history. Cloud wrote its own rows for those tables when it deployed your project: + +```bash +docker compose exec -T postgres pg_dump -U postgres -d my_project \ + --data-only --format=custom \ + --exclude-table-data='serverpod_migrations*' \ + --exclude-table-data='serverpod_runtime_settings*' \ + --exclude-table-data='serverpod_health_*' \ + --exclude-table-data='serverpod_*log*' \ + --exclude-table-data='serverpod_readwrite_test*' \ + --exclude-table-data='serverpod_future_call_claim*' \ + > app-data.dump +``` + +Three details in this command matter: + +- **Dump with `--data-only`.** A data-only dump orders tables by their foreign keys, so users are restored before their profiles. A full dump doesn't, so restoring it can fail on foreign key errors. +- **Keep the `*` at the end of each pattern.** It also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. +- **Everything else is included.** That covers your own tables, users, sessions, future calls, and files stored in the database. + +`pg_dump` warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. Count them: + +```bash +docker compose exec postgres psql -U postgres -d my_project -At \ + -c 'SELECT count(*) FROM serverpod_auth_core_profile WHERE "imageId" IS NOT NULL;' +``` + +If the count is `0`, skip to [Create a database user](#create-a-database-user). + +### Dump users with profile images + +`serverpod_auth_core_profile` and `serverpod_auth_core_profile_image` point at each other, so neither can be restored first. Cloud doesn't let you turn off foreign key checks during a restore either. Instead, you restore the profiles without their image links and add the links back afterwards. + +First, save the links as SQL statements. They name the `public` schema, because `pg_restore` leaves the `search_path` empty on the connection it used: + +```bash +docker compose exec -T postgres psql -U postgres -d my_project -At \ + -c "SELECT format('UPDATE public.serverpod_auth_core_profile SET \"imageId\" = %L WHERE id = %L;', \"imageId\", id) FROM serverpod_auth_core_profile WHERE \"imageId\" IS NOT NULL;" \ + > profile-images.sql +``` + +Next, create a copy of the database and clear the links in the copy. Your original database stays untouched. Copying only works while nothing is connected to `my_project`, and stopping the server took care of that: + +```bash +docker compose exec postgres psql -U postgres \ + -c "CREATE DATABASE my_project_export TEMPLATE my_project;" +docker compose exec postgres psql -U postgres -d my_project_export \ + -c 'UPDATE serverpod_auth_core_profile SET "imageId" = NULL;' +``` + +Then run the `pg_dump` command from [Dump your data](#dump-your-data) again, with `-d my_project_export` instead of `-d my_project`. + +## Create a database user + +The restore connects as a database user that you create yourself. It needs the host and database name, so print the connection details first: + +```bash +serverpod cloud db connection +``` + +Now create that user. The password is shown only once, so save it: + +```bash +serverpod cloud db user create migrator +``` + +The `migrator` user can read and write rows. It can't change the schema, disable triggers, or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. + +Check that Cloud is on the same migration versions as your server: + +```bash +psql "postgresql://migrator@/?sslmode=require" \ + -c "SELECT module, version FROM serverpod_migrations ORDER BY module;" +``` + +Replace `` and `` with the values from `serverpod cloud db connection`. + +## Restore the data + +Download `app-data.dump` to your machine, for example with `scp`. If you created `profile-images.sql`, download it too. + +If your project is on the Growth plan, take a backup snapshot first. See [Database backups](/cloud/concepts/database-backups). + +Restore the dump into Cloud: + +```bash +pg_restore \ + --dbname="postgresql://migrator@/?sslmode=require" \ + --data-only --single-transaction --exit-on-error \ + app-data.dump +``` + +`--single-transaction` and `--exit-on-error` make the restore all or nothing. If any row fails, nothing is written. Fix the problem and run the same command again. + +If you created `profile-images.sql`, add the image links back: + +```bash +psql "postgresql://migrator@/?sslmode=require" \ + -v ON_ERROR_STOP=1 -f profile-images.sql +``` + +## Check the result + +Count the rows in your most important tables on Cloud: + +```bash +psql "postgresql://migrator@/?sslmode=require" \ + -c "SELECT count(*) FROM public.serverpod_auth_core_user;" +``` + +Run the same query on your server, and compare the numbers. Then call your Cloud API, for example from a debug build of your Flutter app: + +- Call an endpoint that reads your data. +- Create a new row, and check that it gets the next ID after your migrated rows. +- Sign in with an existing account. + +When everything works, point your apps at your Cloud URLs, or attach your existing domain. See [Custom domains](/cloud/concepts/custom-domains). Existing sessions keep working, because Cloud now uses your server's auth secrets. + +## Clean up + +Delete the migration user: + +```bash +serverpod cloud db user delete migrator +``` + +If you created an export copy, drop it on your server: + +```bash +docker compose exec postgres psql -U postgres -c "DROP DATABASE my_project_export;" +``` + +Keep your self-hosted server and its data until your apps run against Cloud without problems. Then shut it down. + +## Troubleshooting + +**`duplicate key value violates unique constraint "serverpod_migrations_pkey"`.** The dump includes data that Cloud already wrote when it deployed your project. The same error can name `serverpod_runtime_settings`, `serverpod_health_metric`, or `serverpod_session_log`. Dump again with every `--exclude-table-data` option from [Dump your data](#dump-your-data). With `--single-transaction`, nothing was written, so you can restore again right away. + +**`violates foreign key constraint`.** If the constraint is `serverpod_auth_core_profile_fk_1`, some of your users have profile images. Follow [Dump users with profile images](#dump-users-with-profile-images). For any other constraint, check that you dumped with `--data-only`. + +**Signing in fails with `invalidCredentials`, or refreshing fails with `RefreshTokenInvalidSecretException`.** Cloud uses different auth secrets from your server. Follow [Copy your auth secrets to Cloud](#copy-your-auth-secrets-to-cloud). Users whose refresh failed before the fix need to sign in again. + +**`relation "..." does not exist` in `psql` right after a restore.** `pg_restore` sets `search_path` to an empty value on its connection. Cloud pools connections, so a later session can get that connection back with the empty value still set. Run `SET search_path TO public;` or reconnect later. Your deployed server isn't affected. + +## Related + +- [Database](/cloud/concepts/database) for how the managed database works. +- [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for how Cloud stores your auth secrets. +- [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [pg_restore](https://www.postgresql.org/docs/current/app-pgrestore.html) in the PostgreSQL documentation. From 78bcac078131505ff19cf3489484d86dc6af8781 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Mon, 21 Sep 2026 08:02:51 +0100 Subject: [PATCH 02/13] docs: Reword the migration guide intro --- cloud_docs/guides/migrate-a-self-hosted-database.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 097a6208..32ef0f9e 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -6,9 +6,9 @@ description: Database migration from a self-hosted Serverpod server to Serverpod # Migrate a self-hosted database -You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS. This guide moves your data, your users, and their sessions into a new Serverpod Cloud project. +You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS, and you want to be on Serverpod Cloud instead. This guide takes your data, your users, and their sessions across. -You deploy your project to Cloud first, so Cloud creates the tables from your migrations. Then you copy the rows from your old database into those tables. +It happens in two halves. Deploying your project to Cloud comes first, because that is what creates the tables from your migrations. Copying the rows comes second, out of your old database and into those tables. ## Before you start From 10cb10599052bfd674bb91400114cfc1f7b88cbb Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 09:56:29 +0100 Subject: [PATCH 03/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 32ef0f9e..0711d6a7 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -21,7 +21,7 @@ You need: The commands below use example names. Your database runs in a Docker Compose service called `postgres`, and your server runs in a service called `server`. The database is called `my_project`. Replace these names with your own. -Run the `docker compose` commands on your server, and the `serverpod cloud` commands from your server directory on your machine. +Run the `docker compose` commands on your server, and the `serverpod cloud` commands from your project's `_server` folder on your own machine. ## Deploy your project to Cloud From 5bba75c2299589c1df041b5db8ff78f3e262148f Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 09:56:58 +0100 Subject: [PATCH 04/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 0711d6a7..4a4e9c30 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -147,7 +147,7 @@ Now create that user. The password is shown only once, so save it: serverpod cloud db user create migrator ``` -The `migrator` user can read and write rows. It can't change the schema, disable triggers, or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. +The `migrator` user can read and write rows, but it can't disable triggers or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. Check that Cloud is on the same migration versions as your server: From 6b41a2265546b592771f5cfb06c955b3329cca84 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 09:57:22 +0100 Subject: [PATCH 05/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 4a4e9c30..3e9bf7c8 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -1,7 +1,7 @@ --- sidebar_position: 5 sidebar_label: Migrate a self-hosted database -description: Database migration from a self-hosted Serverpod server to Serverpod Cloud. Copy your data, users, and auth secrets with pg_dump and pg_restore. +description: Moving a self-hosted database into Serverpod Cloud with pg_dump and pg_restore, so your data, users, and sessions come across intact. --- # Migrate a self-hosted database From 661e2768393552e25f605b5d5868ba8412d58a7d Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 09:58:41 +0100 Subject: [PATCH 06/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 3e9bf7c8..be56bf08 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -156,7 +156,7 @@ psql "postgresql://migrator@/?sslmode=require" \ -c "SELECT module, version FROM serverpod_migrations ORDER BY module;" ``` -Replace `` and `` with the values from `serverpod cloud db connection`. +Replace `` and `` with the values from `serverpod cloud db connection`. If it prints a port, add it after the host as `:`. ## Restore the data From a46c0ca3cc43e5be1ddaeab98770198f482e2102 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 10:00:31 +0100 Subject: [PATCH 07/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index be56bf08..2dc2ef42 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -101,7 +101,7 @@ Three details in this command matter: - **Keep the `*` at the end of each pattern.** It also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. - **Everything else is included.** That covers your own tables, users, sessions, future calls, and files stored in the database. -`pg_dump` warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. Count them: +The `pg_dump` command warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. Count them: ```bash docker compose exec postgres psql -U postgres -d my_project -At \ From 3f0bf14ecd83a57ea22b708100c93bdbd6651ce3 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 10:00:49 +0100 Subject: [PATCH 08/13] Update cloud_docs/guides/migrate-a-self-hosted-database.md Co-authored-by: Jamiu Okanlawon <50176100+developerjamiu@users.noreply.github.com> --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 2dc2ef42..371c53a1 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -97,7 +97,7 @@ docker compose exec -T postgres pg_dump -U postgres -d my_project \ Three details in this command matter: -- **Dump with `--data-only`.** A data-only dump orders tables by their foreign keys, so users are restored before their profiles. A full dump doesn't, so restoring it can fail on foreign key errors. +- **Dump with `--data-only`.** A data-only dump orders tables by their foreign keys, so users are restored before their profiles. A full dump doesn't, so restoring it with `--data-only` can fail on foreign key errors. - **Keep the `*` at the end of each pattern.** It also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. - **Everything else is included.** That covers your own tables, users, sessions, future calls, and files stored in the database. From 86561951bcb0f9761957adeb1326ec4332b151ea Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 12:17:35 +0100 Subject: [PATCH 09/13] docs(cloud): address review on the self-hosted database migration guide --- .../guides/migrate-a-self-hosted-database.md | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 371c53a1..56e89580 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -4,7 +4,7 @@ sidebar_label: Migrate a self-hosted database description: Moving a self-hosted database into Serverpod Cloud with pg_dump and pg_restore, so your data, users, and sessions come across intact. --- -# Migrate a self-hosted database +# Migrate a self-hosted database to Cloud You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS, and you want to be on Serverpod Cloud instead. This guide takes your data, your users, and their sessions across. @@ -17,7 +17,7 @@ You need: - The Serverpod Cloud CLI set up and authenticated. See [Set up the Cloud CLI](/cloud/getting-started/installation). - Your Serverpod project on your machine, with the same code and migrations that run on your server. - Shell access to the server that runs your database. -- The PostgreSQL client tools (`pg_dump`, `pg_restore`, and `psql`) on your machine. Use the same major version as your self-hosted database or newer. See [PostgreSQL downloads](https://www.postgresql.org/download/). +- The PostgreSQL client tools (`pg_restore` and `psql`) on your machine. Use the same major version as your self-hosted database or newer. Cloud runs PostgreSQL 17, so version 17 works for most projects. See [PostgreSQL downloads](https://www.postgresql.org/download/). The commands below use example names. Your database runs in a Docker Compose service called `postgres`, and your server runs in a service called `server`. The database is called `my_project`. Replace these names with your own. @@ -87,7 +87,6 @@ Take a data-only dump. Leave out the data Serverpod keeps about each deployment, docker compose exec -T postgres pg_dump -U postgres -d my_project \ --data-only --format=custom \ --exclude-table-data='serverpod_migrations*' \ - --exclude-table-data='serverpod_runtime_settings*' \ --exclude-table-data='serverpod_health_*' \ --exclude-table-data='serverpod_*log*' \ --exclude-table-data='serverpod_readwrite_test*' \ @@ -95,11 +94,12 @@ docker compose exec -T postgres pg_dump -U postgres -d my_project \ > app-data.dump ``` -Three details in this command matter: +The dump uses these options: -- **Dump with `--data-only`.** A data-only dump orders tables by their foreign keys, so users are restored before their profiles. A full dump doesn't, so restoring it with `--data-only` can fail on foreign key errors. -- **Keep the `*` at the end of each pattern.** It also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. -- **Everything else is included.** That covers your own tables, users, sessions, future calls, and files stored in the database. +- **`--data-only` copies rows, not tables.** Your migrations already created the tables on Cloud, and the database user you restore with can't create or change tables. A data-only dump also orders tables by their foreign keys, so each row is restored after the rows it points to. +- **Each pattern ends with `*`.** The `*` also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. +- **The excluded tables stay behind.** Your server's logs and health checks stay in your old database, so Cloud starts with a clean history. Future call claims are short-lived locks held by a running server, so they aren't needed. The future calls themselves are copied. +- **Everything else is included.** That covers your own tables, users, sessions, runtime settings, future calls, and files stored in the database. The `pg_dump` command warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. Count them: @@ -135,19 +135,21 @@ Then run the `pg_dump` command from [Dump your data](#dump-your-data) again, wit ## Create a database user -The restore connects as a database user that you create yourself. It needs the host and database name, so print the connection details first: +The restore connects as a database user that you create yourself. Print the connection details first: ```bash serverpod cloud db connection ``` +The output ends with a `psql` command that contains your connection string, in the form `postgresql:///?sslmode=require`. The commands below use that string with `migrator@` added after `postgresql://`. + Now create that user. The password is shown only once, so save it: ```bash serverpod cloud db user create migrator ``` -The `migrator` user can read and write rows, but it can't disable triggers or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. +The `migrator` user can read and write rows, but it can't create or change tables, disable triggers, or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. Check that Cloud is on the same migration versions as your server: @@ -156,14 +158,25 @@ psql "postgresql://migrator@/?sslmode=require" \ -c "SELECT module, version FROM serverpod_migrations ORDER BY module;" ``` -Replace `` and `` with the values from `serverpod cloud db connection`. If it prints a port, add it after the host as `:`. - ## Restore the data -Download `app-data.dump` to your machine, for example with `scp`. If you created `profile-images.sql`, download it too. +Copy `app-data.dump` from your server to your machine. One way is [`scp`](https://man.openbsd.org/scp), which copies files over SSH. Run it on your machine, with your own user, server address, and path: + +```bash +scp user@your-server:~/my_project/app-data.dump . +``` + +If you created `profile-images.sql`, copy it the same way. If your project is on the Growth plan, take a backup snapshot first. See [Database backups](/cloud/concepts/database-backups). +Cloud wrote default runtime settings when it deployed your project. Delete them, so the settings from your server can take their place: + +```bash +psql "postgresql://migrator@/?sslmode=require" \ + -c "DELETE FROM public.serverpod_runtime_settings;" +``` + Restore the dump into Cloud: ```bash @@ -182,6 +195,12 @@ psql "postgresql://migrator@/?sslmode=require" \ -v ON_ERROR_STOP=1 -f profile-images.sql ``` +Your server reads its runtime settings when it starts. Deploy again, so it picks up the ones you restored: + +```bash +serverpod cloud deploy +``` + ## Check the result Count the rows in your most important tables on Cloud: @@ -197,7 +216,11 @@ Run the same query on your server, and compare the numbers. Then call your Cloud - Create a new row, and check that it gets the next ID after your migrated rows. - Sign in with an existing account. -When everything works, point your apps at your Cloud URLs, or attach your existing domain. See [Custom domains](/cloud/concepts/custom-domains). Existing sessions keep working, because Cloud now uses your server's auth secrets. +When everything works, move your apps over to Cloud. Existing sessions keep working, because Cloud now uses your server's auth secrets. + +- **Keep your domain.** Attach it to your Cloud project, and your apps don't need a new build. See [Custom domains](/cloud/concepts/custom-domains). +- **Use your Cloud URL.** Your API runs at `https://.api.serverpod.space/`. For mobile and desktop apps, set `apiUrl` in your Flutter app's `assets/config.json` to that URL, or pass it with `--dart-define=SERVER_URL=` when you build. Then ship a new build. +- **Flutter web apps deployed with your server** get the Cloud URL from the server, so they need no change. ## Clean up @@ -217,7 +240,7 @@ Keep your self-hosted server and its data until your apps run against Cloud with ## Troubleshooting -**`duplicate key value violates unique constraint "serverpod_migrations_pkey"`.** The dump includes data that Cloud already wrote when it deployed your project. The same error can name `serverpod_runtime_settings`, `serverpod_health_metric`, or `serverpod_session_log`. Dump again with every `--exclude-table-data` option from [Dump your data](#dump-your-data). With `--single-transaction`, nothing was written, so you can restore again right away. +**`duplicate key value violates unique constraint "serverpod_migrations_pkey"`.** The dump includes data that Cloud already wrote when it deployed your project. The same error can name `serverpod_health_metric` or `serverpod_session_log`. Dump again with every `--exclude-table-data` option from [Dump your data](#dump-your-data). If the error names `serverpod_runtime_settings`, delete Cloud's runtime settings as shown in [Restore the data](#restore-the-data). With `--single-transaction`, nothing was written, so you can restore again right away. **`violates foreign key constraint`.** If the constraint is `serverpod_auth_core_profile_fk_1`, some of your users have profile images. Follow [Dump users with profile images](#dump-users-with-profile-images). For any other constraint, check that you dumped with `--data-only`. From ab78916e9977f6fb99117babac2fc2ec23a43e5b Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 12:21:40 +0100 Subject: [PATCH 10/13] docs(cloud): move the pg_dump circular key warning into a tip --- cloud_docs/guides/migrate-a-self-hosted-database.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 56e89580..e4052746 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -101,7 +101,13 @@ The dump uses these options: - **The excluded tables stay behind.** Your server's logs and health checks stay in your old database, so Cloud starts with a clean history. Future call claims are short-lived locks held by a running server, so they aren't needed. The future calls themselves are copied. - **Everything else is included.** That covers your own tables, users, sessions, runtime settings, future calls, and files stored in the database. -The `pg_dump` command warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. Count them: +:::tip + +The `pg_dump` command warns about circular foreign keys between `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image`, with a hint to use a full dump. Ignore the hint. The warning only matters if some of your users have profile images. + +::: + +Count the users with profile images: ```bash docker compose exec postgres psql -U postgres -d my_project -At \ From 93b09213be176775f184c20b3c6296940a426532 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 12:41:54 +0100 Subject: [PATCH 11/13] docs(cloud): shorten long sentences and add version floors in the migration guide --- cloud_docs/guides/migrate-a-self-hosted-database.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index e4052746..21b474b5 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -6,7 +6,7 @@ description: Moving a self-hosted database into Serverpod Cloud with pg_dump and # Migrate a self-hosted database to Cloud -You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS, and you want to be on Serverpod Cloud instead. This guide takes your data, your users, and their sessions across. +You run Serverpod and PostgreSQL yourself, for example with Docker Compose on a VPS. Now you want to move to Serverpod Cloud. This guide takes your data, your users, and their sessions across. It happens in two halves. Deploying your project to Cloud comes first, because that is what creates the tables from your migrations. Copying the rows comes second, out of your old database and into those tables. @@ -17,7 +17,7 @@ You need: - The Serverpod Cloud CLI set up and authenticated. See [Set up the Cloud CLI](/cloud/getting-started/installation). - Your Serverpod project on your machine, with the same code and migrations that run on your server. - Shell access to the server that runs your database. -- The PostgreSQL client tools (`pg_restore` and `psql`) on your machine. Use the same major version as your self-hosted database or newer. Cloud runs PostgreSQL 17, so version 17 works for most projects. See [PostgreSQL downloads](https://www.postgresql.org/download/). +- The PostgreSQL client tools (`pg_restore` and `psql`) on your machine. Use the same major version as your self-hosted database or newer. Cloud runs PostgreSQL 17, so version 17 works unless your self-hosted database is newer. See [PostgreSQL downloads](https://www.postgresql.org/download/). The commands below use example names. Your database runs in a Docker Compose service called `postgres`, and your server runs in a service called `server`. The database is called `my_project`. Replace these names with your own. @@ -96,7 +96,7 @@ docker compose exec -T postgres pg_dump -U postgres -d my_project \ The dump uses these options: -- **`--data-only` copies rows, not tables.** Your migrations already created the tables on Cloud, and the database user you restore with can't create or change tables. A data-only dump also orders tables by their foreign keys, so each row is restored after the rows it points to. +- **`--data-only` copies rows, not tables.** Your migrations already created the tables on Cloud. The database user you restore with can't create or change tables anyway. A data-only dump also orders tables by their foreign keys, so each row is restored after the rows it points to. - **Each pattern ends with `*`.** The `*` also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. - **The excluded tables stay behind.** Your server's logs and health checks stay in your old database, so Cloud starts with a clean history. Future call claims are short-lived locks held by a running server, so they aren't needed. The future calls themselves are copied. - **Everything else is included.** That covers your own tables, users, sessions, runtime settings, future calls, and files stored in the database. @@ -155,7 +155,7 @@ Now create that user. The password is shown only once, so save it: serverpod cloud db user create migrator ``` -The `migrator` user can read and write rows, but it can't create or change tables, disable triggers, or turn off foreign key checks. That's why the dump contains data only. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. +The `migrator` user can read and write rows. It can't create or change tables, disable triggers, or turn off foreign key checks. See [Access the database directly](/cloud/concepts/database#access-the-database-directly) for more about database users. Check that Cloud is on the same migration versions as your server: @@ -225,8 +225,8 @@ Run the same query on your server, and compare the numbers. Then call your Cloud When everything works, move your apps over to Cloud. Existing sessions keep working, because Cloud now uses your server's auth secrets. - **Keep your domain.** Attach it to your Cloud project, and your apps don't need a new build. See [Custom domains](/cloud/concepts/custom-domains). -- **Use your Cloud URL.** Your API runs at `https://.api.serverpod.space/`. For mobile and desktop apps, set `apiUrl` in your Flutter app's `assets/config.json` to that URL, or pass it with `--dart-define=SERVER_URL=` when you build. Then ship a new build. -- **Flutter web apps deployed with your server** get the Cloud URL from the server, so they need no change. +- **Use your Cloud URL.** Your API runs at `https://.api.serverpod.space/`. For mobile and desktop apps, set `apiUrl` in your Flutter app's `assets/config.json` to that URL. You can also pass it with `--dart-define=SERVER_URL=` when you build. Then ship a new build. +- **Flutter web apps deployed with your server** get the Cloud URL from the server on Serverpod 4.0 or later, so they need no change. ## Clean up From a796a39a78933029b6571fcdc37bace5e522e036 Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 13:41:59 +0100 Subject: [PATCH 12/13] docs(cloud): apply review suggestions on the migration guide --- cloud_docs/guides/migrate-a-self-hosted-database.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index 21b474b5..ebf5c5c8 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -57,7 +57,7 @@ serverpod cloud password set jwtHmacSha512PrivateKey "" serverpod cloud password set jwtRefreshTokenHashPepper "" ``` -Set every other password your server reads the same way, for example `serverSideSessionKeyHashPepper` or the client secrets for your sign-in providers. Then deploy, so the server picks up the new values: +Set every other password your server reads the same way, for example `serverSideSessionKeyHashPepper` or the client secrets for your sign-in providers. If you accepted them when `serverpod cloud launch` asked, they're already set. Then deploy, so the server picks up the new values: ```bash serverpod cloud deploy @@ -96,7 +96,7 @@ docker compose exec -T postgres pg_dump -U postgres -d my_project \ The dump uses these options: -- **`--data-only` copies rows, not tables.** Your migrations already created the tables on Cloud. The database user you restore with can't create or change tables anyway. A data-only dump also orders tables by their foreign keys, so each row is restored after the rows it points to. +- **Dump rows only, with `--data-only`.** Your migrations already created the tables on Cloud. The database user you restore with can't create or change tables anyway. A data-only dump also orders tables by their foreign keys, so each row is restored after the rows it points to. - **Each pattern ends with `*`.** The `*` also leaves out each table's ID sequence. Without it, the dump carries your server's sequence values, and the restore resets Cloud's counters for those tables. - **The excluded tables stay behind.** Your server's logs and health checks stay in your old database, so Cloud starts with a clean history. Future call claims are short-lived locks held by a running server, so they aren't needed. The future calls themselves are copied. - **Everything else is included.** That covers your own tables, users, sessions, runtime settings, future calls, and files stored in the database. @@ -118,7 +118,7 @@ If the count is `0`, skip to [Create a database user](#create-a-database-user). ### Dump users with profile images -`serverpod_auth_core_profile` and `serverpod_auth_core_profile_image` point at each other, so neither can be restored first. Cloud doesn't let you turn off foreign key checks during a restore either. Instead, you restore the profiles without their image links and add the links back afterwards. +The `serverpod_auth_core_profile` and `serverpod_auth_core_profile_image` tables point at each other, so neither can be restored first. Cloud doesn't let you turn off foreign key checks during a restore either. Instead, you restore the profiles without their image links and add the links back afterwards. First, save the links as SQL statements. They name the `public` schema, because `pg_restore` leaves the `search_path` empty on the connection it used: @@ -192,7 +192,7 @@ pg_restore \ app-data.dump ``` -`--single-transaction` and `--exit-on-error` make the restore all or nothing. If any row fails, nothing is written. Fix the problem and run the same command again. +The `--single-transaction` and `--exit-on-error` options make the restore all or nothing. If any row fails, nothing is written. Fix the problem and run the same command again. If you created `profile-images.sql`, add the image links back: @@ -230,7 +230,7 @@ When everything works, move your apps over to Cloud. Existing sessions keep work ## Clean up -Delete the migration user: +Delete the `migrator` user: ```bash serverpod cloud db user delete migrator @@ -252,7 +252,7 @@ Keep your self-hosted server and its data until your apps run against Cloud with **Signing in fails with `invalidCredentials`, or refreshing fails with `RefreshTokenInvalidSecretException`.** Cloud uses different auth secrets from your server. Follow [Copy your auth secrets to Cloud](#copy-your-auth-secrets-to-cloud). Users whose refresh failed before the fix need to sign in again. -**`relation "..." does not exist` in `psql` right after a restore.** `pg_restore` sets `search_path` to an empty value on its connection. Cloud pools connections, so a later session can get that connection back with the empty value still set. Run `SET search_path TO public;` or reconnect later. Your deployed server isn't affected. +**`relation "..." does not exist` in `psql` right after a restore.** The `pg_restore` command sets `search_path` to an empty value on its connection. Cloud pools connections, so a later session can get that connection back with the empty value still set. Run `SET search_path TO public;` or reconnect later. Your deployed server isn't affected. ## Related From 10f3b5ef23f6cb78f7e6b0425c82a057a19e3ade Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Thu, 24 Sep 2026 14:03:42 +0100 Subject: [PATCH 13/13] docs(cloud): correct the version floor for web app config --- cloud_docs/guides/migrate-a-self-hosted-database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud_docs/guides/migrate-a-self-hosted-database.md b/cloud_docs/guides/migrate-a-self-hosted-database.md index ebf5c5c8..6b919267 100644 --- a/cloud_docs/guides/migrate-a-self-hosted-database.md +++ b/cloud_docs/guides/migrate-a-self-hosted-database.md @@ -226,7 +226,7 @@ When everything works, move your apps over to Cloud. Existing sessions keep work - **Keep your domain.** Attach it to your Cloud project, and your apps don't need a new build. See [Custom domains](/cloud/concepts/custom-domains). - **Use your Cloud URL.** Your API runs at `https://.api.serverpod.space/`. For mobile and desktop apps, set `apiUrl` in your Flutter app's `assets/config.json` to that URL. You can also pass it with `--dart-define=SERVER_URL=` when you build. Then ship a new build. -- **Flutter web apps deployed with your server** get the Cloud URL from the server on Serverpod 4.0 or later, so they need no change. +- **Flutter web apps deployed with your server** get the Cloud URL from the server, so they need no change. This applies to projects created on Serverpod 3.2 or later. ## Clean up