Skip to content
All essays
ArchitectureMarch 27, 202415 min

Deploying Fastify to Production

Best practices for deploying Fastify applications to production environments.

Ü
Ümit Uz
Mobile & Full Stack Developer

Deploy your Fastify applications with confidence using these production-ready strategies.

Environment Configuration

Environment Variables

javascript
// config/index.js
require('dotenv').config();

module.exports = {
  port: parseInt(process.env.PORT || '3000'),
  host: process.env.HOST || '0.0.0.0',
  nodeEnv: process.env.NODE_ENV || 'development',
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  redisUrl: process.env.REDIS_URL,
};

// .env.example
PORT=3000
HOST=0.0.0.0
NODE_ENV=production
DATABASE_URL=postgresql://...
JWT_SECRET=your-secret-key
REDIS_URL=redis://localhost:6379

Configuration Validation

javascript
// config/validate.js
const Joi = require('joi');

const envSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .required(),
  PORT: Joi.number().default(3000),
  DATABASE_URL: Joi.string().required(),
  JWT_SECRET: Joi.string().min(32).required(),
}).unknown();

const { value: envVars, error } = envSchema.validate(process.env);

if (error) {
  throw new Error(`Config validation error: ${error.message}`);
}

module.exports = {
  env: envVars.NODE_ENV,
  port: envVars.PORT,
  databaseUrl: envVars.DATABASE_URL,
  jwtSecret: envVars.JWT_SECRET,
};

Logging

Structured Logging with Pino

javascript
const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport:
    process.env.NODE_ENV === 'development'
      ? {
          target: 'pino-pretty',
          options: {
            colorize: true,
            translateTime: 'HH:MM:ss Z',
            ignore: 'pid,hostname',
          },
        }
      : undefined,
});

const fastify = require('fastify')({
  logger,
});

// Usage
fastify.log.info({ userId: 123 }, 'User logged in');
fastify.log.error({ error: err }, 'Database connection failed');

Log Aggregation

javascript
// Send logs to external service
const pino = require('pino');
const pinoElastic = require('pino-elasticsearch');

const ecsStream = pinoElastic({
  node: process.env.ELASTICSEARCH_URL,
  index: 'fastify-logs',
});

const logger = pino({}, ecsStream);

Security

Helmet Headers

javascript
const fastify = require('fastify')();

fastify.register(require('@fastify/helmet'), {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
});

Rate Limiting

javascript
fastify.register(require('@fastify/rate-limit'), {
  max: 100,
  timeWindow: '1 minute',
  cache: 10000,
  allowList: ['127.0.0.1'],
  skipOnError: true,
  addHeaders: {
    'x-ratelimit-limit': true,
    'x-ratelimit-remaining': true,
    'x-ratelimit-reset': true,
  },
});

Input Sanitization

javascript
fastify.register(require('@fastify/sanitize'));

fastify.post('/comment', {
  schema: {
    body: {
      type: 'object',
      properties: {
        text: { type: 'string' },
      },
    },
  },
  preHandler: fastify.sanitize({
    text: { escape: true, trim: true },
  }),
}, async (request, reply) => {
  const { text } = request.body;
  // Text is now sanitized
});

Performance Optimization

Clustering

javascript
const fastify = require('fastify')();
const cluster = require('cluster');
const os = require('os');

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;

  console.log(`Master ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  const start = async () => {
    try {
      await fastify.listen({ port: 3000, host: '0.0.0.0' });
      console.log(`Worker ${process.pid} started`);
    } catch (err) {
      fastify.log.error(err);
      process.exit(1);
    }
  };

  start();
}

Caching

javascript
fastify.register(require('@fastify/caching'), {
  privacy: 'private',
  expiresIn: 300, // 5 minutes
});

fastify.get('/data', {
  cache: true,
}, async (request, reply) => {
  return { data: 'expensive computation' };
});

Response Compression

javascript
fastify.register(require('@fastify/compress'), {
  encodings: ['gzip', 'deflate'],
  threshold: 1024, // Only compress if > 1KB
});

Health Checks

javascript
fastify.register(require('@fastify/healthcheck'), {
  healthcheckUrl: '/health',
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          status: { type: 'string' },
          timestamp: { type: 'string' },
        },
      },
    },
  },
});

// Custom health check
fastify.get('/health/ready', async (request, reply) => {
  const checks = {
    database: false,
    redis: false,
  };

  try {
    await fastify.db.query('SELECT 1');
    checks.database = true;
  } catch (err) {
    // Database not ready
  }

  try {
    await fastify.redis.ping();
    checks.redis = true;
  } catch (err) {
    // Redis not ready
  }

  const allHealthy = Object.values(checks).every(Boolean);

  return reply
    .code(allHealthy ? 200 : 503)
    .send({ status: allHealthy ? 'ready' : 'not ready', checks });
});

Monitoring

Prometheus Metrics

javascript
const promClient = require('prom-client');
const fastify = require('fastify')();

const register = new promClient.Registry();

promClient.collectDefaultMetrics({ register });

const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

fastify.addHook('onResponse', async (request, reply) => {
  const duration = reply.getResponseTime() / 1000;
  httpRequestDuration.observe(
    {
      method: request.method,
      route: request.routeOptions.url,
      status_code: reply.statusCode,
    },
    duration
  );
});

fastify.get('/metrics', async (request, reply) => {
  reply.type('text/plain').send(await register.metrics());
});

Graceful Shutdown

javascript
const fastify = require('fastify')();

const gracefulShutdown = async (signal) => {
  fastify.log.info(`Received ${signal}, shutting down gracefully...`);

  setTimeout(() => {
    fastify.log.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);

  await fastify.close();
  process.exit(0);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Docker Deployment

Dockerfile

dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["node", "dist/server.js"]

Docker Compose

yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://db:5432/app
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Kubernetes Deployment

Deployment YAML

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastify-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fastify
  template:
    metadata:
      labels:
        app: fastify
    spec:
      containers:
      - name: fastify
        image: your-registry/fastify-app:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: PORT
          value: "3000"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: fastify-service
spec:
  selector:
    app: fastify
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: LoadBalancer

Best Practices

DOs

  • Use environment variables
  • Implement proper logging
  • Add health checks
  • Monitor performance
  • Use HTTPS
  • Implement rate limiting
  • Graceful shutdown
  • Test before deploying

DON'Ts

  • Hardcode secrets
  • Skip logging
  • Ignore errors
  • Deploy without tests
  • Forget about security
  • Skip monitoring
  • Use default configurations

Conclusion

Deploying Fastify to production requires proper configuration, security, monitoring, and performance optimization. Follow these best practices to ensure your application runs reliably in production. Always test thoroughly and monitor your application's health!

Enjoyed this essay? Share it.

Related essays

Next essay
Fastify Validation and Testing Best Practices