on this page

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

Heads up. You're leaving raindev.fyi

This link heads to railway.app, an external site we don't control. Cool to keep going?

Continue
, Render

Heads up. You're leaving raindev.fyi

This link heads to render.com, an external site we don't control. Cool to keep going?

Continue
, and Fly.io

Heads up. You're leaving raindev.fyi

This link heads to fly.io, an external site we don't control. Cool to keep going?

Continue
all do the same core job: you connect a GitHub repo, they detect it’s a Node app, they build and run it, they give you HTTPS and a place for env vars and logs, and they sell you a Postgres add-on. The differences are in feel and emphasis.

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 Hetzner

Heads up. You're leaving raindev.fyi

This link heads to www.hetzner.com, an external site we don't control. Cool to keep going?

Continue
or DigitalOcean

Heads up. You're leaving raindev.fyi

This link heads to www.digitalocean.com, an external site we don't control. Cool to keep going?

Continue
. No dashboard that detects your app, no automatic HTTPS, no Postgres add-on. You get a clean machine and a root password, and everything else is yours to set up: install Node, install Postgres, configure a web server, get certificates, keep something restarting your app when it crashes, and apply security updates so the box doesn’t get owned.

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

RailwayRenderFly.ioVPS (Hetzner / DigitalOcean)
Setup effortConnect repo, pushConnect repo, pushConnect repo, a little configInstall and wire up everything yourself
DatabaseOne-click PostgresOne-click PostgresPostgres on Fly volumesYou install and run Postgres
Sleeping / cold startsFree usage sleepsFree tier sleepsCan scale to zero (optional)Never sleeps, always your box
Pricing modelUsage basedUsage basedUsage basedFlat monthly per machine
Who it’s forFirst backend, fastFirst backend, clear tiersRegional control, growing appsFull 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 of app.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 .env never left your laptop. Re-enter DATABASE_URL and 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 500 with 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_URL into 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.