Deployment

Ship Your Express App — Render, Fly, or Your Cloud

Deployment

The deployment landscape. PaaS for ease, containers for power, edge for some workloads.

4 min read Level 1/5 #express#deployment#paas
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:

  1. Connect GitHub repo
  2. Build command: npm ci
  3. Start command: npm start
  4. Add env vars in the dashboard
  5. 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 →