Back to Blog
Tutorial10 min read2026-06-28

How to Deploy a Fastify API to Production

A practical guide to taking a Fastify API from localhost to production: Dockerfile, graceful shutdown, health checks, environment config, and a one-command Git deploy.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Fastify is one of the fastest Node.js web frameworks, but raw throughput on localhost means nothing if your production setup mishandles shutdowns, leaks connections, or fails health checks during a rolling deploy. This guide walks through getting a Fastify API into production cleanly.

A minimal production-ready Fastify server

The most common mistake is binding to the default host. Inside a container you must listen on 0.0.0.0, not localhost, or the platform's load balancer can never reach you.

// server.js
import Fastify from 'fastify'

const app = Fastify({ logger: true })

app.get('/healthz', async () => ({ status: 'ok' }))

app.get('/api/users/:id', async (req) => {
  return { id: req.params.id }
})

const port = Number(process.env.PORT) || 3000

await app.listen({ port, host: '0.0.0.0' })

Use process.env.PORT. Most platforms inject the port they expect you to bind, and hardcoding 3000 will make your service unreachable.

Graceful shutdown

During a rolling deploy the orchestrator sends SIGTERM, waits a grace period, then sends SIGKILL. If you ignore SIGTERM you drop in-flight requests. Fastify's close() drains connections for you.

const shutdown = async (signal) => {
  app.log.info(`Received ${signal}, draining...`)
  await app.close()
  process.exit(0)
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))

Health checks

Keep liveness and readiness separate in concept. A liveness probe answers "is the process alive"; a readiness probe answers "can I take traffic right now". A simple /healthz covers liveness. If you depend on a database, add a readiness route that pings it:

app.get('/readyz', async (req, reply) => {
  try {
    await pool.query('SELECT 1')
    return { ready: true }
  } catch {
    reply.code(503)
    return { ready: false }
  }
})

A lean Dockerfile

Use a multi-stage build to keep the runtime image small and to avoid shipping dev dependencies.

FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --if-present

FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Running as the non-root node user is a small change that meaningfully shrinks your attack surface.

Tuning Fastify for production

  • Set logger: true but pipe to a structured sink. Fastify uses Pino, which emits JSON your platform can index.
  • Configure bodyLimit to reject oversized payloads.
  • Enable trustProxy if you sit behind a reverse proxy or ingress, so req.ip reflects the real client.
const app = Fastify({
  logger: true,
  trustProxy: true,
  bodyLimit: 1_048_576 // 1 MB
})

Deployment options compared

ApproachSetup effortScalingBest for
Bare VM + PM2HighManualFull control, legacy stacks
Raw KubernetesVery highExcellentLarge platform teams
Managed container PaaSLowAutomaticMost teams shipping APIs

Unless you already run a cluster, a managed container platform removes the work that has nothing to do with your API: building images, wiring TLS, configuring health probes, and rolling deploys.

Deploying on PandaStack

PandaStack auto-detects Node projects, so you often don't need to write a Dockerfile at all — though if you commit one, it's used as-is. The flow is:

  1. 1Push your Fastify repo to GitHub.
  2. 2Connect the repo in the [dashboard](https://dashboard.pandastack.io) and pick the container app type.
  3. 3PandaStack detects Node, runs the install and build, and produces an image using rootless BuildKit in an ephemeral Kubernetes Job (no host Docker socket).
  4. 4The image is pushed to Google Artifact Registry and deployed via Helm.
  5. 5You get a URL with automatic SSL, live build and app logs, and server-side metrics.

Set PORT and any secrets as environment variables in the dashboard. If you add a managed PostgreSQL database, DATABASE_URL is injected automatically, so your Fastify app can read it from process.env with no extra wiring.

For health checks, point the platform probe at /healthz. Because PandaStack does rolling deploys, your graceful-shutdown handler ensures zero dropped requests on each release.

A note on cold starts

Free-tier apps run on spot nodes with KEDA scale-to-zero, so the first request after idle incurs a cold start. For a hobby API that's fine; for a latency-sensitive production API, run on a paid tier that keeps at least one instance warm.

References

  • [Fastify documentation](https://fastify.dev/docs/latest/)
  • [Fastify: Recommendations for production](https://fastify.dev/docs/latest/Guides/Recommendations/)
  • [Node.js Docker best practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md)
  • [Kubernetes liveness, readiness and startup probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)

Ready to ship a Fastify API without managing a cluster? PandaStack's free tier includes 5 container apps and a managed database — connect a repo and deploy at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also