on this page
- A static site is files. A backend is a program that has to stay awake.
- The port: where the host talks to your app
- Environment variables: your secrets and your database string
- Logs: where you read the crash
- Health checks and the long-running process
- Databases: the thing next to your app
- Cold starts: the free tier takes naps
- Scaling and pricing, briefly
- The three managed platforms
- When a VPS makes sense instead
- Managed platforms vs a VPS at a glance
- Common mistakes
- Tying it back
You shipped a static site once, felt great about it, and figured the backend would be the same five-minute affair. Then you tried to deploy your little Node server, and the host handed you a URL that returns nothing, a log stream that scrolls past too fast to read, and a bill estimate you don’t understand. Welcome. The backend is a different animal, and it’s worth knowing why before you fight it.
If you haven’t read How localhost becomes a real website yet, start there. This guide picks up where that one stops, at the moment your project grows a server.
A static site is files. A backend is a program that has to stay awake.
Here’s the whole difference in one breath. A static site is a folder of finished files (HTML, CSS, JavaScript, a few images). A CDN copies that folder onto servers around the world and hands a piece of it to anyone who asks. Nothing is running. The files just sit there, patient, identical for every visitor.
A backend is a program. A Node/Express server, say, is a process that boots up, listens for requests, and stays running, possibly for weeks, answering whatever comes in. It holds a connection to a database. It might be talking to three users at 2am while you sleep. If that process crashes, your API goes dark until something restarts it. Nobody has to “restart” a static file, because a static file was never alive in the first place.
That single fact (a backend is a living process, not a frozen folder) is the root of almost everything that follows. The host isn’t just storing your code. It’s babysitting a program, keeping it running, and giving it a way to hear the outside world.
The port: where the host talks to your app
Your server listens on a port, a numbered door on the machine where requests come in. On your laptop you’ve probably written something like app.listen(3000) and visited localhost:3000. That works locally because you own the whole machine and you picked the door.
On a host, you don’t get to pick. The host runs dozens of apps on shared hardware, so it decides which door is yours and tells your app through an environment variable called PORT. Your job is to listen on whatever it hands you:
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`listening on ${port}`);
});
That process.env.PORT reads the port the host assigned. The || 3000 is a fallback so the same code still runs on your machine, where PORT isn’t set. Hardcode app.listen(3000) instead, and the host routes traffic to the door it expects while your app stubbornly listens on a different one. The result is a server that boots fine, logs nothing wrong, and answers absolutely no requests. This is the single most common first-backend bug, and now you won’t write it.
Environment variables: your secrets and your database string
That PORT value is one example of an environment variable, a setting the host injects into your program at runtime instead of you writing it into your code. You’ll use them for everything you don’t want hardcoded: your database connection string, API keys, a session secret, the URL of your frontend.
The reason is partly safety and partly sanity. You never commit a database password to GitHub, because anyone who can read the repo can then read your database. So the password lives in the host’s dashboard as DATABASE_URL, and your code reads process.env.DATABASE_URL without ever knowing the actual value. Same code, different secrets in dev and prod, nothing sensitive in version control.
Logs: where you read the crash
With a static site, when something breaks, the browser tells you. With a backend, the failure happens on a machine you can’t see, so you read the logs: the stream of text your server prints while it runs, including every stack trace when it falls over. Every platform here gives you a live log view in its dashboard. Learn where that button is on day one, because when your API returns a 500 at midnight, the logs are the only honest account of what happened. “It doesn’t work” is not debuggable. The exact error on line 42 of db.js is.
Health checks and the long-running process
Because your app is a process that’s supposed to stay alive, the host needs a way to tell whether it actually is. So it runs a health check: every so often it pings a route on your server (often / or a dedicated /health) and waits for a 200 reply. Answer promptly and the host considers you healthy. Stop answering and it assumes you’ve hung or crashed, and it restarts the process to try to fix you. A tiny route that just returns res.send("ok") is enough, and it saves you from a silently dead server.
Databases: the thing next to your app
Most real backends need to remember things between requests, which means a database. For a first project, PostgreSQL is the safe default: mature, free to start, and supported everywhere on this list. You generally don’t run it yourself. You add a managed Postgres instance through the same platform, and it hands you a DATABASE_URL to drop into your environment variables. The platform handles backups, the disk, and keeping the database process alive, which is a real gift, because a database is yet another long-running program you’d otherwise have to babysit.
Cold starts: the free tier takes naps
Here’s a surprise that trips up everyone on a free plan. To save money on idle apps, free tiers spin your server down when no requests have come in for a while. The process literally stops. The next request that arrives has to wait for the host to boot a fresh one, which can take ten, twenty, thirty seconds. That delay is a cold start, and on a sleeping free tier it sometimes stretches long enough that the request times out entirely.
This produces a classic head-scratcher: your frontend on Vercel or Cloudflare Pages calls your API, the first call spins for ages or fails, and you swear the code worked five minutes ago. It did. The server was just asleep. Paid tiers keep the process running so this never happens, which is often the first real reason to spend money on a backend.
Scaling and pricing, briefly
Scaling is what happens when one copy of your server can’t keep up: you run several copies and spread requests across them. You don’t need this on day one, but it’s worth knowing the platforms can do it when traffic shows up.
Pricing on all three managed platforms is usage based. You pay for the compute and memory your app actually uses, plus extras like the database. The free tiers are real and genuinely useful for learning and demos, with the sleeping caveat above. A VPS prices differently, and that’s the next fork in the road.
The three managed platforms
Railway leans hardest into “just works.” You push, it figures out the rest, and adding a database is two clicks. Render is similar and a touch more explicit about configuration, with a clear free tier (that sleeps). Fly.io runs your app close to your users in regions you choose and gives you more control over the underlying machines, which is handy later and slightly more to think about now. For a first backend, Railway or Render is the gentler landing.
When a VPS makes sense instead
A VPS (Virtual Private Server) is a bare Linux box you rent from a provider like
That sounds like a downgrade, and for a first project it is. The appeal shows up later. A VPS gives you full control, flat and predictable pricing (you pay for the box whether it’s busy or idle, with no usage surprises), and one machine that can run several services at once. The trade is that the Linux administration and the security patching are now your job. Reach for it when you’ve outgrown the managed convenience or specifically want to learn the plumbing, not before.
Managed platforms vs a VPS at a glance
| Railway | Render | Fly.io | VPS (Hetzner / DigitalOcean) | |
|---|---|---|---|---|
| Setup effort | Connect repo, push | Connect repo, push | Connect repo, a little config | Install and wire up everything yourself |
| Database | One-click Postgres | One-click Postgres | Postgres on Fly volumes | You install and run Postgres |
| Sleeping / cold starts | Free usage sleeps | Free tier sleeps | Can scale to zero (optional) | Never sleeps, always your box |
| Pricing model | Usage based | Usage based | Usage based | Flat monthly per machine |
| Who it’s for | First backend, fast | First backend, clear tiers | Regional control, growing apps | Full control, many services, flat cost |
Common mistakes
These are the ones that catch nearly everyone on a first deploy.
- Hardcoding the port.
app.listen(3000)instead ofapp.listen(process.env.PORT || 3000). The app boots, the host routes to a different door, no request ever lands. Fix the listen line first when a fresh deploy answers nothing. - Forgetting env vars. Your
.envnever left your laptop. Re-enterDATABASE_URLand every key in the platform dashboard, or watch the app crash on the first line that reads an undefined value. - The free instance is asleep. Your frontend calls the API, the first request hangs or times out, and the code looks fine because it is. The server was napping. Wake it with a paid tier or expect the cold start.
- Never reading the logs. A
500with no investigation is a guess. The logs hold the actual stack trace. Open them before you change a single line. - The database isn’t provisioned. Your code expects a Postgres that you never actually added. Create the managed database, copy its
DATABASE_URLinto your env vars, then deploy.
Tying it back
A static site goes live the moment a CDN holds your files. A backend goes live the moment a process boots, hears the host through PORT, reads its secrets from the environment, finds its database, and stays awake to answer. Once those five things line up, you have something a frontend can actually call, which is the whole point: a real project, with a real server behind it, that strangers can use without you sitting at the keyboard. Pick Railway or Render, push your repo, set your variables, add the Postgres, and go back to building the thing you actually wanted to build.