A deploy is four steps
Get the code onto a machine. Build it. Start it. Send traffic to it. Every platform, from a shell script to a Kubernetes cluster, is a wrapper around those four steps. When a deploy fails, ask which of the four.
Pick the shape before the vendor
What you are building determines what it costs, far more than which logo you choose.
- Static site — HTML, CSS, JavaScript, no server code. A bucket behind a CDN. Cloudflare Pages, Netlify, GitHub Pages. Free at any traffic level you will see this year.
- Static front end plus a few server routes — same, plus functions that run per request. Free tiers cover a genuinely useful amount: hundreds of thousands of requests a month.
- Always-on process — you need WebSockets, background jobs, or long-running work. A small virtual machine or a container platform. The honest floor is about $4–7 a month; Hetzner's smallest is around €4.
- A database — managed Postgres free tiers exist (Neon, Supabase) and typically suspend when idle, which means a cold query takes a second. Fine for a side project, not fine for a checkout page.
A small real application with a few thousand visits a day sits between $0 and $10 a month. Anyone telling you otherwise is selling something.
Build once, run the same artifact
"It works on my machine" almost always means one of a short list:
- Different runtime version. Pin it. A
.nvmrc, anenginesfield, a version in the Dockerfile. - Different environment variables. See the previous lesson.
- Case sensitivity. macOS filesystems are case-insensitive by default. Linux is not.
import Button from "./button"works perfectly on your Mac and fails in production. This one catches people repeatedly, and the error message is a bare module-not-found. - A dependency you installed globally months ago and forgot.
Building in CI rather than on your laptop finds all four, because CI starts from nothing every time.
Boring and reversible
Four properties are worth insisting on, even for a hobby project.
- One command or one push. If your deploy is eleven manual steps, someone will get step seven wrong at 1am, and that someone is you.
- The previous version stays online. Rollback should be selecting an earlier build, not rebuilding from an older commit and hoping. Most platforms give you this; make sure you know where the button is *before* you need it.
- A health check before traffic switches. A process that starts and immediately crashes should never receive requests.
- Migrations are separate from code.
The one that surprises people: rolling deploys
During a deploy, both versions of your code are running at once. Not metaphorically — for thirty to ninety seconds, some requests hit old instances and some hit new ones.
So this breaks:
ALTER TABLE users RENAME COLUMN name TO display_name;shipped together with the code that uses display_name. The migration runs, and every old instance still asking for name starts failing until it is replaced.
The pattern is expand, migrate, contract, and it takes three deploys:
- Add
display_name. Write to both columns. Old code keeps working. - Backfill. Switch reads to the new column. Deploy.
- Once nothing reads
name, drop it. Deploy.
Tedious. Also the difference between a rename and an outage.
Deploy on day one
Before you have an app, deploy a page that says the project's name. Point the domain at it. Get the certificate issued. Then every subsequent deploy is a small diff against something that already works, and you never face the situation where the first deploy and the first launch are the same stressful evening.
Before you move on