In this chapter
On day two, Anna packages her app as a container, deploys it to a cloud platform with a managed database, points a real domain at it and gets HTTPS — reviewing Acts 12, 16, 19, 20 and 21 in her own project, explained as moving a shop from a garage to a high street.
The Problem in Real Life
Friday, 9 AM. Day two. The app works on Anna's laptop — which means it works for exactly one person. "By lunch," she tells John, "anyone with a phone should be able to join a waitlist."
"Then do it the way we do it at BlueTicket," he says. "Container, pipeline, managed database, real domain, HTTPS. Just smaller. And after Sale Day, you know what to check about certificates."
Ship it, then improve it.
John
Running on a Laptop vs. Live on the Internet
"Works on my laptop"
The app depends on Anna's Node version, her database and her .env file.
A real address
People need a name to type, not a laptop's IP address.
Trust
Browsers warn users away from sites without valid HTTPS.
Containerising, Deploying and Adding HTTPS
The shop-moving analogy: the shop has been running from a garage. Moving to the high street means packing everything into boxes that fit any van (a container), renting a shop space with electricity and plumbing included (a cloud platform and managed database), putting up a sign with the shop's name (a domain), and getting the official licence displayed in the window (an HTTPS certificate).
- Containerise (Act 21): a Dockerfile:
FROM node:20-slim,npm ci --omit=dev, copy the code, run as thenodeuser,CMD ["node", "src/server.js"]. Adocker-compose.ymlruns the app and PostgreSQL together locally, so "how to run it" is one command. The same image will run in production. - Choose where to deploy (Act 20): for a small project, a PaaS-style container platform (Render, Railway, Fly.io, Google Cloud Run and similar) is ideal: give it the image or the repository, and it runs it, restarts it and handles the load balancer. Add a managed PostgreSQL from the same provider — backups included (Act 20's lesson).
- Configuration and secrets in the platform (Acts 16, 19):
DATABASE_URL,SESSION_SECRETandNODE_ENV=productiongo into the platform's environment settings — never into the image or Git. The app checks them at start-up and refuses to start without them (Act 19). - Run migrations on deploy: the database starts empty. The deploy runs
npm run migratebefore starting the new version, so the schema is always up to date. - Health check (Act 19): a
/healthendpoint that checks the database connection, so the platform only sends traffic to a healthy app — and restarts it if it isn't. - A domain (Acts 12, 19): she points
waitlist.anna-builds.exampleat the platform with a CNAME record in her DNS settings, and checks it withdig. - HTTPS (Acts 12, 19, 22, 26): the platform issues a free Let's Encrypt certificate automatically and renews it every 90 days. Remembering Sale Day, Anna checks the certificate's expiry date with
openssl, and adds a free external check that emails her if the certificate is ever within 14 days of expiring. Plainhttp://requests are redirected tohttps://, and the session cookie is markedSecure.
| Step | Shop version | Waitlist | Taught in |
|---|---|---|---|
| Container image | Boxes that fit any van | Dockerfile, node:20-slim | Act 21 |
| Platform + managed DB | Rented shop with utilities | Container platform + managed PostgreSQL | Act 20 |
| Config and secrets | The shop's own keys | Platform environment settings | Acts 16, 19 |
| Health check | Opening-hours check | /health with DB ping | Act 19 |
| Domain | The sign above the door | CNAME waitlist.anna-builds.example | Acts 12, 19 |
| HTTPS | The licence in the window | Auto-renewed + expiry alert | Acts 19, 22, 26 |
The Event Waitlist, deployed
https://waitlist.anna-builds.example
DNS CNAME → platform
Platform load balancer + HTTPS
Let's Encrypt, auto-renewed, expiry monitored
Container: event-waitlist:1.0.0
Node 20 · /health · env from platform
Managed PostgreSQL
daily backups
FROM node:20-slimWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY src ./srcCOPY migrations ./migrationsUSER nodeEXPOSE 3000CMD ["node", "src/server.js"]
dig +short waitlist.anna-builds.example# event-waitlist.platform-host.example.openssl s_client -connect waitlist.anna-builds.example:443 \-servername waitlist.anna-builds.example </dev/null 2>/dev/null | openssl x509 -noout -enddate# notAfter=Mar 15 09:12:44 2027 GMT <- valid, auto-renews ~30 days before
11:40 AM — live: Anna opens https://waitlist.anna-builds.example on her phone, on mobile data. The padlock appears. She creates a test event and joins the waitlist from three colleagues' phones. "You are #3 in line." Liam joins from his: "#4". She deliberately stops the database for a moment: /health turns red, the platform stops sending traffic, she restarts it, and everything recovers. It's a real deployment — just a small one.
Key Takeaway
Moving from a laptop to the internet is like moving a shop to the high street: package the app as a container image (Dockerfile, docker-compose for local), deploy it to a container platform with a managed database, keep config and secrets in the platform's environment, run migrations on deploy, expose a health check, point a domain at it with a CNAME, and use automatically renewed HTTPS — then verify the certificate and monitor its expiry yourself.
Why This Matters
A project that's actually live, at a real address with HTTPS, is worth far more in a portfolio than one that only runs locally — and deploying your own project is the best way to make the deployment Acts truly yours. Container platforms and managed databases make this achievable for one person in a morning.
The app is live. Which means things can now go wrong while Anna isn't looking. Before the demo, she wants to know — immediately — if anything breaks.
