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: truebut pipe to a structured sink. Fastify uses Pino, which emits JSON your platform can index. - Configure
bodyLimitto reject oversized payloads. - Enable
trustProxyif you sit behind a reverse proxy or ingress, soreq.ipreflects the real client.
const app = Fastify({
logger: true,
trustProxy: true,
bodyLimit: 1_048_576 // 1 MB
})Deployment options compared
| Approach | Setup effort | Scaling | Best for |
|---|---|---|---|
| Bare VM + PM2 | High | Manual | Full control, legacy stacks |
| Raw Kubernetes | Very high | Excellent | Large platform teams |
| Managed container PaaS | Low | Automatic | Most 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:
- 1Push your Fastify repo to GitHub.
- 2Connect the repo in the [dashboard](https://dashboard.pandastack.io) and pick the container app type.
- 3PandaStack detects Node, runs the install and build, and produces an image using rootless BuildKit in an ephemeral Kubernetes Job (no host Docker socket).
- 4The image is pushed to Google Artifact Registry and deployed via Helm.
- 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).