xhostd
Sign in Start building
← Migrations

How to migrate a web app from a VPS to xhostd

Migrate a web app from a VPS to xhostd: pick a template, migrate Postgres with pg_dump, move files, and cut over your domain with a short write pause.

To migrate a web app from a VPS to xhostd, deploy the code to xhostd first, copy the Postgres database with pg_dump and pg_restore, move uploaded files to object storage, and then point your domain at the new copy. The old server keeps serving visitors until the last step, which makes the move a near zero-downtime migration.

xhostd is managed application hosting: it runs the process, the certificate, the database, and the backups that you operate yourself on a VPS. If you have not decided whether to move, read VPS vs. managed application hosting first.

What changes when you move to managed hosting

Your code and your data format stay the same. Postgres is Postgres, and a dump from your server loads into a channel database. These parts change:

On the VPS On xhostd
A process manager keeps the app running The platform runs and restarts the container
You install and renew certificates The platform issues and renews them
You configure the database and its backups Each channel has a database with nightly and pre-deploy snapshots
A directory holds uploads S3-compatible storage holds uploads
A .env file holds settings Platform environment values, set with set_env
You copy a build to the server You git push, then deploy

You can do the move yourself or ask your AI coding agent, such as Claude Code or Codex, to do the code and deploy steps through MCP tools.

Step 1: inventory the old server

Write down these facts before you change anything:

  • the runtime and version (Node.js, Python, or something else);
  • the command that starts the app, and the port it listens on;
  • every environment variable it reads;
  • the Postgres version, the database size, and the extensions in use;
  • where uploaded files live;
  • every scheduled job and background process;
  • the DNS records for your domain, and their time to live (TTL).

Step 2: pick a template

  • A Node.js or Python service fits the app template: install.sh builds it and launch.sh starts it.
  • Any other runtime, or an app that already has a Dockerfile, fits the docker template.
  • A site of plain files fits the static template.

Two changes are common. The app must listen on the port in the XHOSTD_HTTP_PORT environment variable, on 0.0.0.0. And GET / must return a 2xx or 3xx response, because the deploy health check probes it. The Node.js, Python, and Docker recipes show the exact files. A long-running process that is not a web server fits the background worker recipe.

xhostd has no separate scheduler. Move a recurring job into a background worker that sleeps between runs, as the worker recipe shows.

Step 3: connect the database

Read the connection string from DATABASE_URL, which xhostd sets. Do not hard-code credentials. A Python project that uses SQLAlchemy with psycopg 3 must rewrite the postgresql:// scheme to postgresql+psycopg://, or the app fails with No module named 'psycopg2'.

Run schema migrations in the start command, not at build time, because the build has no DATABASE_URL. Use DATABASE_URL_DIRECT for migrations, because it reaches the database server without the transaction pooler. The Postgres recipe explains both variables.

Step 4: deploy the code first

Create the app, push the code, and deploy. The Claude Code deploy article shows the calls, and they are the same for other agents. Deploy before you load data. This proves the build and the health check independently of your data.

To rehearse the whole move, deploy to a preview channel first. See Preview and staging environments.

Step 5: migrate Postgres

  1. Dump the old database in custom format:

    pg_dump --format=custom --file=app.dump "$OLD_DATABASE_URL"
    
  2. Turn on external database access on the project's Database page in the console, a tab of the project's Data & recovery section. In the External database access card, select Enable external access. Only a person can do this in the console. An agent receives a 403 protected_action error unless you first turn on agent access for it. Then copy the connection URL.

  3. Restore into the channel database, skipping owner and access commands that name roles your channel does not have:

    pg_restore --no-acl --no-owner -d "<connection URL>" app.dump
    
  4. Verify the data. Count rows in your largest tables on both sides, and open the new channel URL.

Two cautions. Change sslmode=require in the URL to sslmode=verify-full sslrootcert=system where your client supports it, because require encrypts but does not verify the gateway. And check your extensions: xhostd marks PostGIS as trusted, and you install it in a migration, but do not assume every other extension is available. Check before the day of the move.

For a large database, restore twice. A rehearsal restore tells you how long the real one takes.

Step 6: move the files

Copy uploaded files into the channel's object storage. The S3_* credentials are already in the environment, and any S3 client library works. Change the app to read and write through that storage instead of a local directory. The file upload recipe has a working example. A container's writable layer is not durable storage, so do not keep uploads there.

Step 7: set environment variables

Move every setting from your old .env file with set_env, except the database settings, which xhostd already provides. Keep secrets out of your repository.

Step 8: plan a zero-downtime cutover

Visitors keep reaching the old server until DNS changes, so the site stays up throughout. Only writes need a short pause, to keep the two databases identical.

  1. A day ahead, lower the TTL on your existing DNS records.
  2. Attach the domain. add_custom_domain returns a TXT record and a routing record. A subdomain uses a CNAME. A bare domain uses an A record, because most registrars do not allow a CNAME at the apex.
  3. Create the records at your registrar and call verify_custom_domain. Retry if DNS has not propagated.
  4. Pause writes on the old server, then run a final dump and restore.
  5. Switch DNS to xhostd. The certificate is issued on the first HTTPS request, which can take a few extra seconds.

The custom domains page has the details and error codes. Google sign-in works on verified custom domains with no extra setup.

Step 9: keep the old server briefly

Watch the runtime log and the error rate after the switch. Keep the old server for at least a week as your rollback: repoint DNS and you are back. Shut it down only after a nightly snapshot exists on xhostd and you have downloaded a dump of your own. See Postgres backups and recovery for what xhostd keeps and for how long.

You can also leave again. You can download a pg_dump of any channel database from the console, so your data is never locked in. To estimate cost, read the pricing page. To start from scratch instead of migrating, use Getting started.