Migrate from Render

Last updated: June 24, 2026

Render made shipping a service as simple as connecting a repo, and the per-service model made the bill grow one row at a time. A real app on Render is rarely one service: it's a web service, maybe a background worker or two, a cron job, a PostgreSQL instance, and a Key Value store — each metered and billed on its own line, none of them a server you size yourself.

Here's the reframe that makes the move straightforward: a Render app is services, config, and a database — and your render.yaml Blueprint already describes most of it. Your assistant reads that Blueprint the way the Heroku playbook reads a Procfile: every entry under services and databases is a thing to recreate, and your environment groups are just environment variables. None of it is Render-specific magic — it's a normal Linux deployment that a platform has been managing for you.

This recipe hands that translation to your AI assistant. With the American Cloud MCP server connected, your assistant reads your render.yaml, turns each service into a systemd service on a VM you own, installs the runtime your code needs, migrates your PostgreSQL data with pg_dump / pg_restore, points your domain at the new server, and turns on HTTPS — all from prompts you paste in. The stack that spread across several Render services usually fits on one VM you can resize as you grow.

[ claude code · migrate in one conversation ]
The assistant prices the American Cloud equivalent first, then builds it on your approval — your old provider stays up until you cut over.

Why Claude Code for this

Any MCP client can create the infrastructure. But migrating an app also means running commands on the server — SSH in, install the runtime, restore a database dump, wire up nginx. Claude Code combines the American Cloud tools with your terminal, so one session can provision the VM, read your local repo and render.yaml, and deploy to the box without switching tools. That's the setup this recipe assumes. Cursor and the other clients work too — you'll just run the SSH steps yourself when the assistant tells you to.

Provisioning and migrating are write operations. You'll need a read-write API key from console.americancloud.com/api-keys and the --allow-writes flag on the MCP server. See the overview for setup and safety details. Start read-only while you take inventory and plan, then switch the key when you're ready to build.

How a Render app maps to a VM

Every piece of a Render Blueprint has a direct equivalent on a server. Your assistant works from this mapping:

On RenderOn your American Cloud VM
Web service (type: web)A systemd service, with nginx in front as a reverse proxy
Background worker (type: worker)A systemd service that restarts on crash and on boot
Cron job (type: cron)A cron entry on the VM
Render PostgreSQL (databases:)PostgreSQL installed on the VM, migrated with pg_dump / pg_restore
Key Value / RedisRedis installed on the same VM, listening on localhost
Static site (type: web, staticPublishPath)nginx serving the built output directly from the VM
Environment groups / env varsOne environment file on the server that every service reads
Persistent disk (disk:)The VM's own disk, or attached block storage for larger volumes
Custom domain + managed TLSDNS records via the hosted DNS tools plus a Let's Encrypt certificate from certbot
Docker-based service (runtime: docker)Docker on the VM — see the Docker Compose recipe

The shape that fits this best is a typical app — Node, Python, Ruby, or Go — with a web service, a worker or two, a PostgreSQL database, and maybe a Key Value store. That's exactly the workload per-service pricing adds up on, and exactly what one VM handles comfortably.

Before you start

  • Your app in a local git repo, ideally with its render.yaml Blueprint at the root.
  • Access to your Render dashboard so you can read your environment variable names, find your PostgreSQL connection details, and suspend services when it's time to cut over.
  • A domain you control, with the ability to point its DNS at American Cloud.
  • The MCP server connected to your assistant with a read-write key and --allow-writes.

Phase 1 — inventory your app (read-only)

Before anything is created or billed, have your assistant build a complete picture of what it's migrating. This phase is read-only on both sides — it reads your repo and lists American Cloud options without touching your Render services or your account.

Open your project in Claude Code and paste:

text
This is a Render app I want to migrate to a server I own. Take inventory
first — don't create anything yet:
 
1. Read render.yaml and list every entry under "services" and "databases":
   the name, the type (web, worker, cron, static), and the start/build
   command each one runs. Note any cron schedules.
2. Detect the runtime and version: check for package.json / requirements.txt /
   pyproject.toml / Gemfile / go.mod and tell me the language and version the
   app expects, plus any system packages it implies (image libs, a build step,
   a native module, etc.). Note whether any service uses runtime: docker.
3. List the NAMES of the environment variables the app reads (from the envVars
   blocks in render.yaml and by grepping the code for env lookups). Do NOT ask
   me for their values — I'll set those on the server myself.
4. Tell me which backing services this app needs: PostgreSQL? a Key Value /
   Redis store? a persistent disk? anything else?
 
Then summarize it back to me as a migration target: how many services I'll
run, what runtime to install, and what to install for the database and cache.

What your assistant will do

This phase is your assistant reading files — your local repo and, through the MCP server, your American Cloud options:

  1. Read the render.yaml Blueprint. Each entry under services becomes one service to recreate. A type: web service is the public app; type: worker is a background process; type: cron is a scheduled command; type: web with a staticPublishPath is a static site. The databases: block tells it you need PostgreSQL.
  2. Identify the runtime from the manifest. A package.json means Node; requirements.txt or pyproject.toml means Python; a Gemfile means Ruby; go.mod means Go. This is what Render inferred from your service settings — your assistant installs that exact runtime on the VM instead. If a service declares runtime: docker, it's a container build, and the Docker Compose recipe is the better fit for that part.
  3. Collect env var names. It reads the envVars blocks in render.yaml and greps your code for environment lookups (process.env.*, os.environ[...], ENV[...]) so the server's environment file has every key the app reads — without ever needing the secret values.
  4. Map the backing services. A databases: block means PostgreSQL on the VM; a Key Value store means Redis; a disk: mount means either the VM's own disk or a block storage volume, depending on how much you store.

Never paste secret env values into the chat. Your assistant only needs the names of your environment variables to build the env file's structure. Put the real values — database passwords, API keys, signing secrets — directly into the server's env file over SSH, where they stay on the box you own. Treat them like any other secret: not in the conversation, not in the repo.

Phase 2 — plan and price (read-only)

With the inventory in hand, ask for a sized plan and a cost estimate. This is still read-only — cost estimates create nothing.

text
Based on that inventory, plan the American Cloud side. List the available
regions and current Ubuntu images and the VM packages. Recommend ONE VM size
that comfortably runs the web service, the worker(s), PostgreSQL, and the Key
Value store together, with headroom for my database size. Show me the monthly
cost estimate before I approve anything, so I can compare it to my Render bill.

What your assistant will do:

  1. Call list_regions, list_images (filtered to Ubuntu), and list_vm_packages to find a region near you, a current Ubuntu LTS image, and a compute tier whose CPU, memory, and disk fit your workload.
  2. Call get_cost_estimate_vm with that exact region, package, size, and image, and show you the hourly and monthly numbers before creating anything. Nothing is billed yet.
  3. Hand you a side-by-side: one VM running everything versus the stack of Render services and managed add-ons on your current bill. You bring the Render numbers; the assistant brings the American Cloud estimate.

Because your web service, workers, database, and cache all share one machine, you're sizing one server instead of paying per metered service. Need more later? scale_vm resizes it in place.

Phase 3 — provision and deploy

Now the writes begin. This is the build prompt — paste it after you've approved the plan and cost from Phase 2. Read each step's output before approving the next.

text
Go ahead and build the new home for this app. Walk through it step by step
and wait for my confirmation on anything that costs money:
 
1. Check whether I have an SSH key registered; if not, create one and tell me
   where the private key is saved.
2. Create the Ubuntu VM we sized, on an isolated network, with that SSH key,
   and open inbound ports 22, 80, and 443.
3. Over SSH, install the runtime from my manifest (the exact language version),
   plus the system packages the app needs, PostgreSQL, and Redis. Keep both
   PostgreSQL and Redis listening on localhost only.
4. Clone this repo onto the VM, install dependencies, and run the build.
5. Create the app's environment file from the env var NAMES we found, with
   placeholder values — I'll fill in the real secrets myself afterward. Point
   the database URL and the Key Value URL at the local PostgreSQL and Redis.
6. Turn each service from render.yaml into its own systemd service that reads
   that environment file, restarts on crash, and starts on boot. Put nginx in
   front of the web service as a reverse proxy on port 80. For any static
   site, configure nginx to serve its built output directly.
7. Turn any cron services into cron entries on the VM.
 
Don't start the app against real data yet — we'll migrate the database next.

What your assistant will do:

  1. Sort out the SSH key. It calls list_ssh_keys; if nothing fits, create_ssh_key generates a pair — the private key is returned once and never stored, so the assistant saves it locally and sets the right permissions. It needs this key to install on the VM and to SSH in afterward.
  2. Create the server. On your go-ahead, create_vm provisions the Ubuntu VM. The same call carries networkAccess.inboundPorts to open 22, 80, and 443 — this opens the firewall and the port forwarding together, so those ports are actually reachable from the internet, not just allowed by a rule. It also carries keypairs to install your key. The VM provisions asynchronously, so the assistant polls get_vm until its status reaches STARTED and it has a public IP.
  3. Install the stack over SSH. It installs the exact runtime your manifest declares, the system packages your app needs, then PostgreSQL and Redis — both bound to localhost so neither is exposed to the internet. Nothing new opens on the firewall for them.
  4. Lay down the environment file. It writes one env file keyed by every env var name from Phase 1, with placeholders, and points the database and Key Value URLs at the local services. You fill in the real secret values over SSH after this step.
  5. Recreate the services. Each render.yaml service becomes a systemd unit reading that env file — web and workers each get one, restarting on crash and on boot. nginx reverse-proxies the public ports to the web service. A static site is different: there's no long-running process, so nginx serves the built files (the staticPublishPath output) directly.
  6. Schedule the jobs. Each type: cron service becomes a cron entry on the VM with the same schedule and command.

Opening a port later, after the VM exists, takes both a firewall rule and a port forwarding rule — a firewall rule alone does not make a port reachable. If you ask the assistant to expose a new port down the road, it'll call create_firewall_rule and create_port_forwarding_rule (or enable_static_nat). Opening 22, 80, and 443 up front via create_vm's inboundPorts handles both at once.

Phase 4 — migrate the data

This is the only phase with real downtime, so it's deliberately last and deliberately careful. The goal is a clean cutover: freeze writes on Render, take a final dump, restore it on the VM, and verify nothing was lost.

text
Now migrate the database with minimal downtime:
 
1. I'll suspend the web and worker services on Render (or put the app in
   maintenance mode) so nothing writes to the database while we copy it — tell
   me when to do that.
2. Take a final pg_dump of the Render PostgreSQL database in custom format,
   using its external connection string.
3. Copy the dump to the VM and pg_restore it into the local PostgreSQL.
4. Run the app's migration command against the restored database.
5. Verify the migration: compare row counts on the main tables between the
   Render database and the restored one, and flag any mismatch.
6. Start the systemd services and confirm the app responds locally on the VM
   (curl the health endpoint through nginx).
 
Report the row-count comparison and the local health check before we touch DNS.

What your assistant will do:

  1. Freeze writes. You suspend the Render services (or enable maintenance mode) so the live app stops writing mid-copy — the dump is then a consistent point-in-time snapshot.
  2. Take the final dump. A pg_dump in custom format against your Render PostgreSQL external connection string captures schema and data.
  3. Restore on the VM. It copies the dump over and runs pg_restore into the local PostgreSQL you installed in Phase 3.
  4. Run migrations. Your app's migration command runs against the restored database so the schema matches the code being deployed.
  5. Verify counts. It compares row counts on your key tables between source and target and surfaces any difference — your proof the data arrived intact before you commit to the cutover.
  6. Smoke-test locally. It starts the services and curls the app through nginx on the VM itself, confirming the stack works end to end before any public traffic hits it.

Your Key Value store is usually a cache or a job queue, not a system of record, so it rarely needs a data copy — a fresh Redis is fine, and the worker repopulates it. If yours holds data you can't lose, tell the assistant and it'll dump and restore Redis the same way.

Phase 5 — DNS cutover and HTTPS

With the app verified on the VM, point your domain at it and switch on TLS.

text
Cut my domain {your-domain.com} over to the new VM:
 
1. Check whether the domain is already a hosted DNS zone here; if not, create
   it and tell me the nameservers to set at my registrar.
2. Lower the TTL first if it's high, then point the A record at the VM's
   public IP.
3. Once DNS resolves to the VM, provision a Let's Encrypt certificate with
   certbot and switch nginx to HTTPS with auto-renewal. Keep the Render
   services suspended — anyone still on cached DNS should not be able to write
   to the old database.
 
Tell me when https://{your-domain.com} is live and served from the new server.

What your assistant will do:

  1. Call list_dns_zones; if your domain isn't hosted here, create_dns_zone adds it and the assistant shows you the American Cloud nameservers to set at your registrar. A nameserver change has its own propagation window, separate from any record's TTL.
  2. Call create_dns_record to add (or update) the A record pointing your domain at the VM's public IP. New records get a short TTL so future changes cut over fast.
  3. Once DNS resolves to the VM, run certbot for a Let's Encrypt certificate, reconfigure nginx for port 443, and enable automatic renewal.

Keep the Render services suspended from the final dump until you decommission. While DNS propagates, some visitors still resolve to Render — with the services suspended, they can't read or write the old database. Bringing them back during propagation is how migrations lose data.

A few notes on timing. Lowering the TTL only takes effect at whichever DNS host currently serves your domain, and only after the old TTL has expired — so lower it well before cutover day. If you're moving the domain's nameservers to American Cloud, that move propagates on its own schedule on top of any record TTL. Ask your assistant to "check what {your-domain.com} currently resolves to and tell me when it points at the VM's IP." certbot also needs DNS to resolve to the VM before it can issue the certificate.

Phase 6 — verify, then decommission

Run on the new server for a day or two before you delete anything on Render. Keep the Render services around (suspended) — they're your rollback if something surfaces under real traffic.

text
The app's been live on the VM for a couple of days and looks healthy. Walk me
through decommissioning Render safely: confirm the VM is the only thing serving
the domain, remind me to take one final offline backup of the database, and
list the steps to delete the Render services, the PostgreSQL instance, and the
Key Value store once I'm sure.

The assistant can confirm the American Cloud side — the VM is STARTED, the services are running, the certificate is valid — but the actual Render teardown stays in your hands: take one last database backup to keep offline, then delete the services, the PostgreSQL instance, and the Key Value store when you're confident. From here, every future deploy is a push-and-restart on a server you own.

Follow-up: a one-prompt deploy command

The migration is the hard part. Make every future deploy trivial:

text
Set up a deploy script in this repo that ships updates to the VM: push the
latest committed code over SSH, install dependencies, run the build and the
migration command, and restart the web and worker systemd services. Add a
"deploy" entry to my package.json (or a task runner) and document the one
command I run from now on.

After this, shipping a change is "run the deploy script" — or just "deploy the latest" and the assistant runs it.

Troubleshooting

A service won't start after migration. Ask your assistant to "show me the journalctl logs for the web (or worker) service and tell me what's failing." The usual culprit is a missing or placeholder value in the environment file — confirm every env var name from Phase 1 has its real value set on the server, not a placeholder.

The app can't reach the database. Have the assistant confirm PostgreSQL is running and that the database URL in the env file points at localhost with the right database and user. Since PostgreSQL listens only on localhost, nothing on the firewall needs to change — connections stay on the box.

The site is unreachable on 80 or 443. A reachable port needs both a firewall rule and a forwarding path. Ask your assistant to "list the firewall and port forwarding rules on the VM's public IP and confirm 80 and 443 are open end to end." It can call list_firewall_rules and list_port_forwarding_rules, then add anything missing with create_firewall_rule plus create_port_forwarding_rule (or enable_static_nat). Have it also check the VM is STARTED with get_vm and that nginx and the web service are both running.

A static site shows nginx's default page. Confirm nginx is pointed at the built output directory (the equivalent of Render's staticPublishPath) and that the build actually ran on the VM. Ask the assistant to "show me the nginx site config and the contents of the publish directory."

SSH connection refused. Confirm port 22 is open (same firewall and port forwarding check) and that the private key from the create_ssh_key step is the one your assistant is using. If the key was lost, the assistant can reset access with reset_vm_password or open a browser console session with create_vm_console.

Row counts don't match in Phase 4. Don't cut DNS over. Ask the assistant to re-run the dump and restore — the most common cause is a write that slipped in before the services were suspended. Re-freezing and re-dumping gives a clean snapshot.

Next steps