Ship Your Express App — Render, Fly, or Your Cloud
Deployment
The deployment landscape. PaaS for ease, containers for power, edge for some workloads.
What you'll learn
- Pick a platform
- Ship your first production deploy
- Set up graceful shutdown
Three styles of Express deployment in 2026: PaaS, container host, serverless. Different trade-offs, all work.
PaaS — Easy Mode
You push, they run. Auto-detect Node, run npm start, give you
HTTPS + a domain + basic metrics.
- Render — generous free tier, simple
- Railway — slick UI
- Fly.io — global edge, easy Postgres
- Heroku — the original
A typical Render deploy:
- Connect GitHub repo
- Build command:
npm ci - Start command:
npm start - Add env vars in the dashboard
- Hit deploy
Most PaaS providers also handle migrations on deploy via a “release command” or “build hook.”
Container Hosts
You build a Docker image (next lesson), push to a registry, run anywhere containers run:
- Cloud Run, ECS / Fargate, DigitalOcean App Platform, Kubernetes
More control, more responsibility. Use when you’ve outgrown PaaS.
Serverless
Functions that spin up per request. Suit some workloads:
- Vercel Functions (great for Astro/Next backends)
- Netlify Functions
- AWS Lambda
The catch: cold starts (Node 100-300ms boot), and persistent connections (DB pools, WebSocket) are awkward. Use for low-traffic or burst-traffic endpoints.
A Reasonable Production package.json
{
"type": "module",
"engines": { "node": ">=22.0.0" },
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"migrate": "drizzle-kit migrate",
"dev": "tsx watch src/index.ts"
}
} For pure JS (no TS), drop the build step.
Graceful Shutdown
When the platform sends SIGTERM, finish in-flight requests, then
exit:
const server = app.listen(env.PORT);
async function shutdown(signal) {
console.log(`got ${signal} — draining`);
// 1. Stop accepting new connections
server.close(async (err) => {
if (err) console.error(err);
// 2. Close DB / Redis
await pool.end();
await redis.quit();
process.exit(0);
});
// 3. Force after 10s
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT")); Without this, in-flight requests get cut off mid-response. Users notice.
Health Endpoints
Every deploy needs two:
app.get("/healthz", (req, res) => {
// "is the process alive?" — cheap
res.json({ ok: true });
});
app.get("/readyz", async (req, res) => {
// "can we serve traffic?" — deeper
try {
await pool.query("SELECT 1");
res.json({ ok: true });
} catch {
res.status(503).json({ ok: false });
}
}); Load balancers use these to route traffic.
Migrations on Deploy
Run migrations before the new code becomes live:
# release command in your PaaS
npm run migrate && npm start For zero-downtime: use expand-then-contract migrations (covered in the Node track).
Secrets
Never bake secrets into images. Pull from the platform’s secret manager at boot.
Docker →