{"course":{"slug":"cloud-and-shipping","title":"Shipping It","url":"https://addaly.com/learn/cloud-and-shipping","level":"intermediate","track":"build"},"lesson":{"slug":"deploying-small","title":"Deploying for nearly nothing","url":"https://addaly.com/learn/cloud-and-shipping/deploying-small","position":15,"of":74,"minutes":8,"answer":"Both versions run during a deploy, so schema changes take three deploys, not one.","keyIdea":"Both versions run during a deploy, so schema changes take three deploys, not one."},"body":{"format":"markdown","text":"## A deploy is four steps\n\nGet 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.\n\n## Pick the shape before the vendor\n\nWhat you are building determines what it costs, far more than which logo you choose.\n\n- **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.\n- **Static front end plus a few server routes**: same, plus functions that run per request. Free tiers cover a useful amount: hundreds of thousands of requests a month.\n- **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 to 7 a month; Hetzner's smallest is around €4.\n- **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.\n\nA small real application with a few thousand visits a day sits between $0 and $10 a month. Anyone telling you otherwise is selling something.\n\n## Build once, run the same artifact\n\n\"It works on my machine\" almost always means one of a short list:\n\n- Different runtime version. Pin it. A `.nvmrc`, an `engines` field, a version in the Dockerfile.\n- Different environment variables. See the previous lesson.\n- **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.\n- A dependency you installed globally months ago and forgot.\n\nBuilding in CI rather than on your laptop finds all four, because CI starts from nothing every time.\n\n## Boring and reversible\n\nFour properties are worth insisting on, even for a hobby project.\n\n1. **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.\n2. **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.\n3. **A health check before traffic switches.** A process that starts and immediately crashes should never receive requests.\n4. **Migrations are separate from code.**\n\n## The one that surprises people: rolling deploys\n\nDuring 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.\n\nSo this breaks:\n\n```sql\nALTER TABLE users RENAME COLUMN name TO display_name;\n```\n\nshipped 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.\n\nThe pattern is **expand, migrate, contract**, and it takes three deploys:\n\n1. Add `display_name`. Write to both columns. Old code keeps working.\n2. Backfill. Switch reads to the new column. Deploy.\n3. Once nothing reads `name`, drop it. Deploy.\n\nTedious. Also the difference between a rename and an outage.\n\n## Deploy on day one\n\nBefore 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."},"check":{"question":"You rename a database column and ship the migration in the same deploy as the code that uses the new name. The platform does a rolling replacement, so old and new instances both serve traffic for about ninety seconds. What happens?","options":[{"text":"Old instances keep querying a column that no longer exists, so a fraction of requests fail during the overlap","correct":true},{"text":"Nothing: the migration completes before the new code starts, so the schema and the code always agree","correct":false,"tempting":"Assumes a deploy is a single instantaneous switch, with only one version of the code ever running at a time."},{"text":"Only requests that arrive mid-restart fail, and the health check keeps those instances out of rotation","correct":false,"tempting":"Trusts the health check to catch it, but an old instance is genuinely healthy: the schema moved underneath a process that is running fine."},{"text":"The database blocks the conflicting queries until the deploy finishes, adding latency but no errors","correct":false,"tempting":"Imagines the database buffers around schema changes, when it simply answers that the column does not exist."}]},"prev":{"slug":"infrastructure-you-can-recreate","title":"The console clicks nobody wrote down","url":"https://addaly.com/learn/cloud-and-shipping/infrastructure-you-can-recreate"},"next":{"slug":"stopping-a-process-politely","title":"Ending a process without dropping a request","url":"https://addaly.com/learn/cloud-and-shipping/stopping-a-process-politely"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Deploying for nearly nothing', in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/deploying-small (Accessed: 31 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Deploying for nearly nothing","courseTitle":"Shipping It","module":{"index":2,"slug":"making-a-deploy-boring","title":"Making a deploy boring"},"answer":null,"keyIdea":"Both versions run during a deploy, so schema changes take three deploys, not one.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"a-deploy-is-four-steps","runs":[{"text":"A deploy is four steps"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"pick-the-shape-before-the-vendor","runs":[{"text":"Pick the shape before the vendor"}]},{"t":"p","runs":[{"text":"What you are building determines what it costs, far more than which logo you choose."}]},{"t":"ul","items":[[{"text":"Static site","b":true},{"text":": 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."}],[{"text":"Static front end plus a few server routes","b":true},{"text":": same, plus functions that run per request. Free tiers cover a useful amount: hundreds of thousands of requests a month."}],[{"text":"Always-on process","b":true},{"text":": you need WebSockets, background jobs, or long-running work. A small virtual machine or a container platform. The honest floor is about $4 to 7 a month; Hetzner’s smallest is around €4."}],[{"text":"A database","b":true},{"text":": 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."}]]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"build-once-run-the-same-artifact","runs":[{"text":"Build once, run the same artifact"}]},{"t":"p","runs":[{"text":"“It works on my machine” almost always means one of a short list:"}]},{"t":"ul","items":[[{"text":"Different runtime version. Pin it. A "},{"text":".nvmrc","code":true},{"text":", an "},{"text":"engines","code":true},{"text":" field, a version in the Dockerfile."}],[{"text":"Different environment variables. See the previous lesson."}],[{"text":"Case sensitivity.","b":true},{"text":" macOS filesystems are case-insensitive by default. Linux is not. "},{"text":"import Button from \"./button\"","code":true},{"text":" works perfectly on your Mac and fails in production. This one catches people repeatedly, and the error message is a bare module-not-found."}],[{"text":"A dependency you installed globally months ago and forgot."}]]},{"t":"p","runs":[{"text":"Building in CI rather than on your laptop finds all four, because CI starts from nothing every time."}]},{"t":"h2","id":"boring-and-reversible","runs":[{"text":"Boring and reversible"}]},{"t":"p","runs":[{"text":"Four properties are worth insisting on, even for a hobby project."}]},{"t":"ol","start":1,"items":[[{"text":"One command or one push.","b":true},{"text":" If your deploy is eleven manual steps, someone will get step seven wrong at 1am, and that someone is you."}],[{"text":"The previous version stays online.","b":true},{"text":" 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 "},{"text":"before","i":true},{"text":" you need it."}],[{"text":"A health check before traffic switches.","b":true},{"text":" A process that starts and immediately crashes should never receive requests."}],[{"text":"Migrations are separate from code.","b":true}]]},{"t":"h2","id":"the-one-that-surprises-people-rolling-deploys","runs":[{"text":"The one that surprises people: rolling deploys"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"So this breaks:"}]},{"t":"code","lang":"sql","label":"SQL","text":"ALTER TABLE users RENAME COLUMN name TO display_name;"},{"t":"p","runs":[{"text":"shipped together with the code that uses "},{"text":"display_name","code":true},{"text":". The migration runs, and every old instance still asking for "},{"text":"name","code":true},{"text":" starts failing until it is replaced."}]},{"t":"p","runs":[{"text":"The pattern is "},{"text":"expand, migrate, contract","b":true},{"text":", and it takes three deploys:"}]},{"t":"ol","start":1,"items":[[{"text":"Add "},{"text":"display_name","code":true},{"text":". Write to both columns. Old code keeps working."}],[{"text":"Backfill. Switch reads to the new column. Deploy."}],[{"text":"Once nothing reads "},{"text":"name","code":true},{"text":", drop it. Deploy."}]]},{"t":"p","runs":[{"text":"Tedious. Also the difference between a rename and an outage."}]},{"t":"h2","id":"deploy-on-day-one","runs":[{"text":"Deploy on day one"}]},{"t":"p","runs":[{"text":"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."}]}],"check":{"question":[{"text":"You rename a database column and ship the migration in the same deploy as the code that uses the new name. The platform does a rolling replacement, so old and new instances both serve traffic for about ninety seconds. What happens?"}],"options":[{"text":[{"text":"Old instances keep querying a column that no longer exists, so a fraction of requests fail during the overlap"}],"correct":true,"tempting":null},{"text":[{"text":"Nothing: the migration completes before the new code starts, so the schema and the code always agree"}],"correct":false,"tempting":[{"text":"Assumes a deploy is a single instantaneous switch, with only one version of the code ever running at a time."}]},{"text":[{"text":"Only requests that arrive mid-restart fail, and the health check keeps those instances out of rotation"}],"correct":false,"tempting":[{"text":"Trusts the health check to catch it, but an old instance is genuinely healthy: the schema moved underneath a process that is running fine."}]},{"text":[{"text":"The database blocks the conflicting queries until the deploy finishes, adding latency but no errors"}],"correct":false,"tempting":[{"text":"Imagines the database buffers around schema changes, when it simply answers that the column does not exist."}]}],"order":[1,3,0,2],"why":null},"prev":{"slug":"infrastructure-you-can-recreate","title":"The console clicks nobody wrote down","minutes":9},"next":{"slug":"stopping-a-process-politely","title":"Ending a process without dropping a request","minutes":8}}}