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.
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 Render | On 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 / Redis | Redis 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 vars | One 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 TLS | DNS 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.yamlBlueprint 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:
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:
- Read the
render.yamlBlueprint. Each entry underservicesbecomes one service to recreate. Atype: webservice is the public app;type: workeris a background process;type: cronis a scheduled command;type: webwith astaticPublishPathis a static site. Thedatabases:block tells it you need PostgreSQL. - Identify the runtime from the manifest. A
package.jsonmeans Node;requirements.txtorpyproject.tomlmeans Python; aGemfilemeans Ruby;go.modmeans Go. This is what Render inferred from your service settings — your assistant installs that exact runtime on the VM instead. If a service declaresruntime: docker, it's a container build, and the Docker Compose recipe is the better fit for that part. - Collect env var names. It reads the
envVarsblocks inrender.yamland 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. - Map the backing services. A
databases:block means PostgreSQL on the VM; a Key Value store means Redis; adisk: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.
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:
- Call
list_regions,list_images(filtered to Ubuntu), andlist_vm_packagesto find a region near you, a current Ubuntu LTS image, and a compute tier whose CPU, memory, and disk fit your workload. - Call
get_cost_estimate_vmwith that exact region, package, size, and image, and show you the hourly and monthly numbers before creating anything. Nothing is billed yet. - 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.
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:
- Sort out the SSH key. It calls
list_ssh_keys; if nothing fits,create_ssh_keygenerates 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. - Create the server. On your go-ahead,
create_vmprovisions the Ubuntu VM. The same call carriesnetworkAccess.inboundPortsto 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 carrieskeypairsto install your key. The VM provisions asynchronously, so the assistant pollsget_vmuntil its status reachesSTARTEDand it has a public IP. - 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
localhostso neither is exposed to the internet. Nothing new opens on the firewall for them. - 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.
- Recreate the services. Each
render.yamlservice becomes asystemdunit 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 (thestaticPublishPathoutput) directly. - Schedule the jobs. Each
type: cronservice becomes acronentry 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.
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:
- 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.
- Take the final dump. A
pg_dumpin custom format against your Render PostgreSQL external connection string captures schema and data. - Restore on the VM. It copies the dump over and runs
pg_restoreinto the local PostgreSQL you installed in Phase 3. - Run migrations. Your app's migration command runs against the restored database so the schema matches the code being deployed.
- 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.
- 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.
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:
- Call
list_dns_zones; if your domain isn't hosted here,create_dns_zoneadds 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. - Call
create_dns_recordto add (or update) theArecord pointing your domain at the VM's public IP. New records get a short TTL so future changes cut over fast. - 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.
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:
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
- Deploy a Next.js app — the same VM-plus-systemd pattern, start to finish
- Run a Docker Compose stack — for any service that was building from a Dockerfile on Render
- Object storage with your AI assistant — move user uploads and backups off the VM filesystem and store them durably
- Back up your VM and data — automate the offline snapshots that protect a self-managed server
- Teach your AI agent to deploy here — add one file to your repo and your agent knows this whole playbook
- Migrate from Heroku — the sibling playbook, if you also have a Heroku app to move