Skip to main content

Migrate apps, databases, files and images to Cloider

This guide moves a typical production setup from another provider to Cloider: the app, its database (with a dump and restore), its files in object storage (with rclone or the AWS CLI), its container images (with skopeo or Docker), and finally its domain. It is written so you can rehearse everything while the old setup keeps serving, then switch over in a short, planned window, and it ends with a checklist to print.

At a glance

You will moveThe app api, the PostgreSQL database orders-db, the bucket acme-assets, the images of acme/api, and example.com
ProductsApps or Kubernetes, Databases, Object Storage, Container Registry, Domains
ToolsThe cloider CLI, pg_dump and pg_restore (or mysqldump, mongodump), rclone or the AWS CLI, skopeo or Docker, dig
DifficultyIntermediate
Time1 to 3 hours of work, plus your data transfer, plus 24 to 48 hours of DNS preparation
DowntimeA few minutes of read-only time for the final database copy, if you follow the cutover plan

What you'll move​

What maps to what​

You haveOn CloiderNotes
A web service built from git or a Dockerfile (Heroku, Render, Railway, App Runner, Cloud Run, a VM)An appBuilt with your Dockerfile or a buildpack. One container per app, which must answer on its port
Several services, workers, cron jobs, or prebuilt imagesA Kubernetes clusterStandard Kubernetes: Deployments, CronJobs, Services
Managed PostgreSQL, MySQL, MongoDB or RedisA managed databaseSame engines, one primary server, automatic backups with point-in-time restore
S3, Spaces, R2, B2 or another S3 compatible storeA bucketS3 API at https://s3.eu-central-2.cloider.com
Docker Hub, GHCR, ECR, GitLab registryA registry projectregistry.cloider.com/<project>/<repository>:<tag>
DNS at your registrar or a DNS hostCloider DNS, or keep your current DNSTargets route names to apps and Services with automatic HTTPS

Before you begin​

  • A Cloider account and the CLI, logged in with Read & Write access. See Install the CLI.
  • Admin access to the old provider: its database connection string, an access key for its storage, a login for its registry, and its DNS records.
  • The client tools of your database engine, at the version of the Cloider server or newer: the PostgreSQL 18 client (pg_dump, pg_restore), the MySQL 9 client (mysqldump, mysql), or the MongoDB database tools.
  • rclone or the AWS CLI v2, and skopeo or Docker.
Free trial accounts

A trial account holds one cluster, one database, one bucket and one registry project at a time. A typical migration fits; if you are moving several databases or buckets, upgrade first. See the billing model.

Part 1: Take inventory and prepare DNS​

Write down everything the app depends on, because whatever you forget breaks after the switch:

# Example: export the old app's environment (Heroku shown; other providers have a similar export)
heroku config --shell --app acme-api > old.env

# Example: list the old DNS records (use your DNS host's export or API where you can)
dig +noall +answer example.com ANY
dig +noall +answer www.example.com
dig +noall +answer example.com MX
dig +noall +answer example.com TXT

old.env holds secrets: keep it out of git and delete it when you are done.

Lower the TTL of the records you will change (the apex and www) to 300 seconds at your current DNS host, at least 24 to 48 hours before the cutover. Resolvers cache a record for its old TTL, so a lower TTL in advance makes the switch take minutes instead of a day.

Verify​

dig +noall +answer example.com A
Output
example.com. 300 IN A 198.51.100.10

The second column, the TTL, shows 300 or less once the change is live.

Part 2: Move the container images​

Skip this part if the old provider builds your app from git: Cloider will build it the same way.

Create the registry project and a short-lived token for the migration:

cloider registry create acme --wait
cloider registry token create --label migration --project acme --expires-days 7

Copy the Username and Secret from the output. Then copy the images. skopeo copies straight from registry to registry, with every platform of a multi-platform image, without a Docker daemon:

skopeo login ghcr.io -u acme-bot
skopeo login registry.cloider.com -u acme # paste the migration token's secret

skopeo copy --all \
docker://ghcr.io/acme/api:1.4.0 \
docker://registry.cloider.com/acme/api:1.4.0

To copy every tag of a repository:

for tag in $(skopeo list-tags docker://ghcr.io/acme/api | jq -r '.Tags[]'); do
skopeo copy --all "docker://ghcr.io/acme/api:$tag" "docker://registry.cloider.com/acme/api:$tag"
done

With Docker instead (only the platform of your machine is copied):

docker pull ghcr.io/acme/api:1.4.0
docker tag ghcr.io/acme/api:1.4.0 registry.cloider.com/acme/api:1.4.0
echo "$MIGRATION_SECRET" | docker login registry.cloider.com -u acme --password-stdin
docker push registry.cloider.com/acme/api:1.4.0

Then point your CI at the new registry, see CI/CD with GitHub Actions or CI/CD with GitLab.

Verify​

cloider registry images acme api

Every tag you copied is listed, with the same digest as on the old registry (skopeo inspect shows it on both sides). See Push and pull images.

Part 3: Move the files​

Create the bucket (with --public if the old one served files publicly) and a key for it, then copy.

cloider bucket create acme-assets --wait
cloider bucket key create --label migration --bucket acme-assets

Bucket names are shared by every Cloider account, so the old bucket's name may be taken: choose another one and update the name in your app's configuration.

rclone copies between two remotes directly, in parallel, and checks the result. Configure both, here with environment variables:

# The old provider (an S3 compatible example; rclone supports most providers)
export RCLONE_CONFIG_OLD_TYPE=s3
export RCLONE_CONFIG_OLD_PROVIDER=Other
export RCLONE_CONFIG_OLD_ACCESS_KEY_ID=OLDKEY...
export RCLONE_CONFIG_OLD_SECRET_ACCESS_KEY=OLDSECRET...
export RCLONE_CONFIG_OLD_ENDPOINT=https://s3.old-provider.example
export RCLONE_CONFIG_OLD_REGION=us-east-1

# Cloider
export RCLONE_CONFIG_CLOIDER_TYPE=s3
export RCLONE_CONFIG_CLOIDER_PROVIDER=Other
export RCLONE_CONFIG_CLOIDER_ACCESS_KEY_ID=CLDRZ8Q4M2X7KD1PLW0B
export RCLONE_CONFIG_CLOIDER_SECRET_ACCESS_KEY=7hT2kV9qWm4LxR8zNc3PbY6sJd1FgA5eUo0iKlMn
export RCLONE_CONFIG_CLOIDER_ENDPOINT=https://s3.eu-central-2.cloider.com
export RCLONE_CONFIG_CLOIDER_REGION=eu-central-2
export RCLONE_CONFIG_CLOIDER_NO_CHECK_BUCKET=true

rclone copy old:legacy-assets cloider:acme-assets --metadata --transfers 16 --progress

--metadata carries each file's Content-Type, Cache-Control and other metadata across. Run the same command again at cutover: it only copies what is new or changed since the first run.

With the AWS CLI​

The AWS CLI can't copy between two providers in one command, so it goes through a local folder:

aws s3 sync s3://legacy-assets ./assets-export --profile old
aws s3 sync ./assets-export s3://acme-assets --profile cloider

The cloider profile needs the endpoint and the checksum setting from S3 tools. Content types are guessed again from the file extensions, and other metadata such as Cache-Control isn't carried over: pass --cache-control as in Static assets on object storage.

Verify​

rclone check old:legacy-assets cloider:acme-assets --one-way
rclone size old:legacy-assets
rclone size cloider:acme-assets
Output
2026/10/21 11:20:44 NOTICE: S3 bucket acme-assets: 0 differences found
2026/10/21 11:20:44 NOTICE: S3 bucket acme-assets: 18342 matching files
Total objects: 18342
Total size: 7.912 GiB (8495346291 Byte)
Total objects: 18342
Total size: 7.912 GiB (8495346291 Byte)

If the app wrote public URLs of the old storage into its database, those rows still point at the old provider. Plan a search and replace for them, or store keys instead of URLs from now on.

Part 4: Move the database​

Rehearse the copy now, with the old database still in use, so you know how long it takes. You repeat it for real at cutover.

# 1. The new database, reachable from this computer
cloider db create orders-db --engine postgresql --plan standard --allow-my-ip --wait

# 2. Dump the old one: custom format, without the old server's owners and grants
pg_dump "$OLD_DATABASE_URL" --format=custom --no-owner --no-privileges --file=app.dump

# 3. Load it, four tables at a time
NEW=$(cloider db get orders-db -o json)
PGPASSWORD="$(cloider db password orders-db)" PGSSLMODE=require \
pg_restore --no-owner --no-privileges --jobs 4 \
-h "$(echo "$NEW" | jq -r .host)" -p "$(echo "$NEW" | jq -r .database_port)" \
-U "$(echo "$NEW" | jq -r .database_user)" -d "$(echo "$NEW" | jq -r .database_name)" \
app.dump

--no-owner --no-privileges makes every object belong to your Cloider database user, because the old server's roles don't exist on Cloider. Extensions that need a superuser can't be created; check what the old database uses with SELECT extname FROM pg_extension; and what Cloider offers with cloider db exec orders-db "SELECT name FROM pg_available_extensions ORDER BY name". See PostgreSQL.

For a small database, cloider db exec -i orders-db < schema.sql loads a plain SQL script of up to 2 MiB without any local client and without opening the database to your IP.

Verify​

Compare row counts of your most important tables on both sides:

psql "$OLD_DATABASE_URL" -Atc "SELECT count(*) FROM orders"
cloider db exec orders-db "SELECT count(*) FROM orders"

The numbers match (as of the dump). Note how long the dump and the restore took: that is the read-only window you will need at cutover.

Part 5: Move the app​

From git. If the old provider deployed from GitHub, GitLab or Bitbucket, create the app from the same repository:

cloider git connect github

Split the exported environment into plain settings (config.env) and secrets (secrets.env, one KEY=VALUE per line, without quotes), and update the values that change: the database URL (use the new database's private host), the bucket name, endpoint and key, and the registry if the app pulls images. Then create the app with them, so its very first version starts with the right configuration:

SECRETS=()
while IFS= read -r line; do
[ -z "$line" ] || [ "${line#\#}" != "$line" ] && continue
SECRETS+=(--secret-env "$line")
done < secrets.env

cloider app create --git github --repo acme/api --branch main --name api --port 3000 \
--env-file config.env "${SECRETS[@]}"

From a folder. Without git, deploy the folder with the same flags:

cloider deploy --name api --port 3000 --env-file config.env "${SECRETS[@]}"

Things that work differently from some providers:

  • Release phase or migrations on deploy: run them yourself after a deploy with cloider app exec --timeout 15m api -- npm run migrate, or in your CI pipeline.
  • Workers and cron jobs: an app runs one container that must answer HTTP on its port to go live. Run background workers and scheduled jobs as Deployments and CronJobs in a cluster, or give a worker a tiny health endpoint on $PORT.
  • Procfile: a web: line starts the app, as on Heroku.

See Deploy from git and Environment variables.

Finally, close the database to everyone but the app (and your own IP while you migrate):

cloider db trusted-sources orders-db --set app:api --add-my-ip

Verify​

The app runs on its free cloider.app address against the Cloider database, while the domain still points at the old provider:

cloider app get api
curl -s https://api-q2v8.cloider.app/healthz
cloider app logs api --lines 100

Click through the main flows of your app on that address: sign in, read data, write data, upload a file. Writes you make now go to the rehearsal copy, which you replace at cutover.

Part 6: Prepare Cloider DNS​

Add the domain and recreate every record from part 1 in Cloider, so nothing breaks when the nameservers switch. Then add the records and targets that send your web traffic to Cloider:

cloider domain create example.com

# Records you keep, recreated as they were (examples)
cloider dns create example.com --type MX --value "10 mx1.mail.example.net." --ttl 3600
cloider dns create example.com --type TXT --value "v=spf1 include:mail.example.net ~all" --ttl 3600

# Web traffic to the app: www by CNAME. The apex already has an ALIAS record that points at Cloider, added with the domain
cloider dns create example.com --type CNAME --name www --value api-q2v8.cloider.app. --ttl 300

cloider target create example.com --app api
cloider domain create www.example.com
cloider target create www.example.com --app api

Nothing changes for visitors yet: the domain's nameservers still point at the old DNS host. To keep your current DNS host instead of moving to Cloider's nameservers, add only the web records there at cutover: the www CNAME, and an A record for the apex with the address dig +short api-q2v8.cloider.app A prints. See Add a custom domain.

Verify​

Ask Cloider's nameservers directly, before the switch:

dig @ns1.cloider.com example.com MX +short
dig @ns1.cloider.com example.com A +short
dig @ns1.cloider.com www.example.com +short

Every record answers as it does on the old DNS host today, except the web records, which point at Cloider.

Part 7: Cut over​

With everything rehearsed, the switch is short. Announce a maintenance window of about twice the time the rehearsal dump and restore took.

  1. Stop writes on the old side. Turn on maintenance mode or read-only mode in the old app, or scale its web and worker processes to zero.
  2. Copy the final data. Recreate the Cloider database's content from a fresh dump: drop the rehearsal copy and restore again (for PostgreSQL, pg_restore --clean --if-exists replaces the objects; or delete and recreate the database). Copy files once more with rclone copy old:legacy-assets cloider:acme-assets --metadata, which only transfers what changed.
  3. Check row counts as in part 4, and the app on its cloider.app address.
  4. Switch DNS. Change the nameservers at your registrar to ns1.cloider.com through ns4.cloider.com (or update the A and CNAME records at your current DNS host). With the low TTLs from part 1, most visitors arrive at Cloider within minutes. Cloider issues the HTTPS certificates as soon as the names resolve to it.
  5. Watch the app's logs (cloider app logs api -f) and errors for the first hour.

Keep the old setup running, read-only, for a few days, in case you need to compare something or roll back by switching DNS back.

Verify​

dig example.com NS +short
curl -sSI https://example.com/ | head -n 1
cloider domain list

The domain uses Cloider's nameservers, answers 200 over HTTPS, and is Active in Cloider.

Part 8: Tidy up​

  • Raise the TTLs of the web records back to 3600 with cloider dns update <record-id> --ttl 3600.
  • Delete the migration registry token and bucket key: cloider registry token delete migration --yes, cloider bucket key delete migration --yes.
  • Remove your own IP from the database: cloider db trusted-sources orders-db --remove <your-ip>.
  • Delete old.env, secrets.env, app.dump and other local copies of data and secrets.
  • After a few quiet days, take a last backup of the old database, then shut down and delete the old resources so you stop paying for them.

Migration checklist​

Copy this into your ticket and tick it off.

Days before

  • Inventory: services, workers, cron jobs, environment variables, databases, buckets, images, domains, DNS records
  • TTL of the apex and www lowered to 300 seconds, at least 24 to 48 hours before
  • Registry project created and images copied, CI pushing to registry.cloider.com
  • Bucket created and files copied, rclone check clean
  • Database created, rehearsal dump and restore done, timing noted, extensions checked
  • App (or cluster workloads) running on Cloider with the new configuration, tested on its cloider.app address
  • Domain added in Cloider, every DNS record recreated, web records and targets in place, checked with dig @ns1.cloider.com
  • Maintenance window announced

Cutover

  • Writes stopped on the old side
  • Final database dump and restore, final file sync
  • Row counts and a smoke test on the cloider.app address
  • Nameservers (or records) switched
  • HTTPS works on every name, logs are clean

After

  • TTLs raised again, migration tokens and keys deleted, your IP removed from trusted sources
  • Local dumps and env files deleted
  • Old resources backed up one last time, then deleted

What it costs​

During the migration you pay for both setups at once, so plan the cutover soon after the rehearsal. On Cloider, everything is billed by the second at the rates on the pricing page:

ResourceHow it is billed
App or clusterCompute units while it runs
DatabaseIts plan's monthly price, prorated while it is Ready
Bucket and registry projectPer GiB stored per month. Uploads, downloads and requests are free, so copying data in costs nothing beyond the storage
Domains, DNS records, targets, certificatesFree

The old provider may charge for data transferred out of it. Check its egress pricing before copying large buckets.

Clean up​

If you were only rehearsing and want to remove everything from Cloider again:

cloider domain delete www.example.com --yes
cloider domain delete example.com --yes
cloider app delete api --yes
cloider db delete orders-db --yes --wait
cloider bucket delete acme-assets --yes --wait
cloider registry delete acme --yes --wait

Leave the nameservers at your registrar untouched until you really cut over.

Troubleshooting​

ProblemCause and fix
pg_dump: error: aborting because of server version mismatchYour pg_dump is older than the old server. Install a newer PostgreSQL client (18 works for every older server)
pg_restore reports role "..." does not existAdd --no-owner --no-privileges
permission denied to create extensionThe extension needs a superuser, which isn't available. Find an alternative or drop its use
MySQL ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER ... privilege(s)DEFINER clauses are still in the dump. Run the sed line again
The restore can't connectYour IP isn't a trusted source. cloider db trusted-sources orders-db --add-my-ip
skopeo or docker push says unauthorizedLog in to registry.cloider.com with the token's username and secret; check it covers the project and isn't expired
rclone AccessDenied on CloiderThe key doesn't reach the bucket, or RCLONE_CONFIG_CLOIDER_NO_CHECK_BUCKET=true is missing
The app can't reach the new databaseIt uses the public host or isn't a trusted source. Use the private host and --add app:api
Some visitors still reach the old app after the switchTheir resolver cached the old record. It expires after the old TTL; that's why TTLs are lowered days before
Email stopped working after the switchAn MX, SPF, DKIM or DMARC record wasn't recreated in Cloider. See Email DNS records
Certificate warning on the domainThe names don't resolve to Cloider yet, or a proxying DNS provider sits in front. See HTTPS certificates

More in Troubleshooting.

Next steps​

Frequently asked questions

How do I migrate a PostgreSQL database to Cloider?

Create a Cloider PostgreSQL database with your IP as a trusted source, dump the old database with pg_dump --format=custom --no-owner --no-privileges, and load it with pg_restore --no-owner --no-privileges --jobs 4 against the new database's public host, port, user and password, with sslmode=require.

How do I copy files from S3 to a Cloider bucket?

Configure rclone with one remote for the old provider and one for Cloider (endpoint https://s3.eu-central-2.cloider.com, region eu-central-2, a bucket access key, no_check_bucket = true) and run rclone copy old:bucket cloider:bucket --metadata, then rclone check to compare.

How do I move Docker images to the Cloider registry?

Create a registry token with Pull & push permission, log in to registry.cloider.com, and copy each image with skopeo copy --all docker://<old image> docker://registry.cloider.com/<project>/<repository>:<tag>, or pull, tag and push it with Docker.

How do I migrate to Cloider without downtime?

Rehearse every step while the old setup serves, lower DNS TTLs days ahead, and keep only the final database copy for the cutover window. The read-only time is roughly how long the dump and restore take; for databases too large for that, contact support.

Can I keep my DNS provider when moving to Cloider?

Yes. Add the domain to Cloider for its targets, and at your current DNS provider point www with a CNAME to the app's cloider.app subdomain and the apex with an A record. Cloider issues the HTTPS certificate once the names resolve to it.