Skip to main content
Deploying a Feathers application to production requires careful configuration, security hardening, and performance optimization. This guide covers best practices for production deployments.

Pre-Deployment Checklist

Before deploying to production, ensure you’ve completed these critical steps:
1
Set NODE_ENV to production
2
export NODE_ENV=production
3
This enables:
4
  • Production optimizations in Express/Koa
  • Production configuration loading
  • Disabled verbose logging
  • Better error handling
  • 5
    Configure environment variables
    6
    Set all required environment variables for your production environment:
    7
    export PORT=3030
    export NODE_ENV=production
    export FEATHERS_SECRET=your-strong-random-secret
    export DATABASE_URL=your-production-database-url
    
    8
    Build your application
    9
    Compile TypeScript to JavaScript:
    10
    npm run compile
    
    11
    Run tests
    12
    Ensure all tests pass:
    13
    NODE_ENV=test npm test
    
    14
    Review security settings
    15
    Verify CORS, authentication, and other security configurations are production-ready.

    Production Configuration

    Environment-Specific Config

    Create config/production.json with production settings:
    {
      "host": "0.0.0.0",
      "port": 3030,
      "public": "./public/",
      "origins": [
        "https://yourdomain.com",
        "https://www.yourdomain.com"
      ],
      "paginate": {
        "default": 25,
        "max": 100
      }
    }
    
    Never commit config/production.json if it contains secrets. Use environment variables for sensitive data.

    Environment Variables Mapping

    Use config/custom-environment-variables.json to map environment variables:
    {
      "port": {
        "__name": "PORT",
        "__format": "number"
      },
      "host": "HOSTNAME",
      "authentication": {
        "secret": "FEATHERS_SECRET"
      },
      "mongodb": "DATABASE_URL",
      "postgres": {
        "connection": "DATABASE_URL"
      }
    }
    

    Application Setup

    Server Startup

    Your main entry point should properly initialize the application:
    // src/index.ts
    import { app } from './app'
    import { logger } from './logger'
    
    const port = app.get('port')
    const host = app.get('host')
    
    const server = app.listen(port, host)
    
    server.on('listening', () => {
      logger.info(
        `Feathers application started on http://${host}:${port}`
      )
    })
    
    process.on('unhandledRejection', (reason, promise) => {
      logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
      process.exit(1)
    })
    
    process.on('uncaughtException', (error) => {
      logger.error('Uncaught Exception:', error)
      process.exit(1)
    })
    

    Graceful Shutdown

    Implement graceful shutdown to cleanly close connections:
    // src/index.ts
    const shutdown = async (signal: string) => {
      logger.info(`${signal} received, starting graceful shutdown`)
      
      server.close(async () => {
        logger.info('HTTP server closed')
        
        try {
          await app.teardown()
          logger.info('Feathers app teardown complete')
          process.exit(0)
        } catch (error) {
          logger.error('Error during teardown:', error)
          process.exit(1)
        }
      })
    
      // Force shutdown after 30 seconds
      setTimeout(() => {
        logger.error('Forced shutdown after timeout')
        process.exit(1)
      }, 30000)
    }
    
    process.on('SIGTERM', () => shutdown('SIGTERM'))
    process.on('SIGINT', () => shutdown('SIGINT'))
    

    Security Best Practices

    1. Authentication Secret

    Use a strong, random secret for JWT signing:
    # Generate a strong secret
    node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
    
    # Set as environment variable
    export FEATHERS_SECRET="your-generated-secret-here"
    

    2. CORS Configuration

    Restrict origins to your actual domains:
    // src/app.ts
    import express from '@feathersjs/express'
    
    app.use(cors({
      origin: app.get('origins'), // From config
      credentials: true
    }))
    

    3. Rate Limiting

    Protect against brute force attacks:
    npm install express-rate-limit
    
    import rateLimit from 'express-rate-limit'
    
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      message: 'Too many requests from this IP'
    })
    
    app.use('/authentication', limiter)
    

    4. Helmet for Security Headers

    npm install helmet
    
    import helmet from 'helmet'
    
    app.use(helmet())
    

    5. Data Validation

    Always validate and sanitize user input:
    import { hooks } from '@feathersjs/schema'
    
    app.service('users').hooks({
      before: {
        create: [hooks.validateData(userDataSchema)],
        patch: [hooks.validateData(userPatchSchema)]
      }
    })
    

    Database Configuration

    Connection Pooling

    Configure appropriate connection pool settings:
    // PostgreSQL with Knex
    export const db = knex({
      client: 'pg',
      connection: app.get('postgres'),
      pool: {
        min: 2,
        max: 10,
        acquireTimeoutMillis: 30000,
        idleTimeoutMillis: 30000
      }
    })
    

    MongoDB Connection

    import { MongoClient } from 'mongodb'
    
    const client = await MongoClient.connect(app.get('mongodb'), {
      maxPoolSize: 10,
      minPoolSize: 2,
      maxIdleTimeMS: 30000,
      serverSelectionTimeoutMS: 5000
    })
    

    Database Cleanup on Shutdown

    app.hooks({
      teardown: [
        async () => {
          // Close database connections
          await db.destroy()
          logger.info('Database connections closed')
        }
      ]
    })
    

    Performance Optimization

    1. Enable Compression

    npm install compression
    
    import compression from 'compression'
    
    app.use(compression())
    

    2. Optimize Pagination

    Set reasonable pagination limits:
    {
      "paginate": {
        "default": 25,
        "max": 100
      }
    }
    

    3. Use Caching

    Implement caching for frequently accessed data:
    import Redis from 'ioredis'
    
    const redis = new Redis(app.get('redis'))
    
    app.service('users').hooks({
      after: {
        get: [
          async (context) => {
            const key = `user:${context.id}`
            await redis.setex(key, 3600, JSON.stringify(context.result))
            return context
          }
        ]
      },
      before: {
        get: [
          async (context) => {
            const key = `user:${context.id}`
            const cached = await redis.get(key)
            if (cached) {
              context.result = JSON.parse(cached)
            }
            return context
          }
        ]
      }
    })
    

    4. Database Indexes

    Ensure proper indexes on frequently queried fields:
    // MongoDB
    await db.collection('users').createIndex({ email: 1 }, { unique: true })
    await db.collection('messages').createIndex({ userId: 1, createdAt: -1 })
    
    // PostgreSQL
    await knex.schema.table('users', (table) => {
      table.index('email')
    })
    

    Platform-Specific Deployment

    Docker

    Create a Dockerfile:
    FROM node:20-alpine
    
    # Create app directory
    WORKDIR /app
    
    # Install dependencies
    COPY package*.json ./
    RUN npm ci --only=production
    
    # Copy compiled code
    COPY lib/ ./lib/
    COPY public/ ./public/
    COPY config/ ./config/
    
    # Expose port
    EXPOSE 3030
    
    # Start application
    CMD ["node", "lib/index.js"]
    
    Create docker-compose.yml for local production testing:
    version: '3.8'
    
    services:
      app:
        build: .
        ports:
          - "3030:3030"
        environment:
          - NODE_ENV=production
          - PORT=3030
          - FEATHERS_SECRET=${FEATHERS_SECRET}
          - DATABASE_URL=mongodb://mongo:27017/myapp
        depends_on:
          - mongo
    
      mongo:
        image: mongo:7
        ports:
          - "27017:27017"
        volumes:
          - mongo-data:/data/db
    
    volumes:
      mongo-data:
    

    Heroku

    Create a Procfile:
    web: node lib/index.js
    
    Set environment variables:
    heroku config:set NODE_ENV=production
    heroku config:set FEATHERS_SECRET=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")
    

    AWS (Elastic Beanstalk)

    Create .ebextensions/nodecommand.config:
    option_settings:
      aws:elasticbeanstalk:container:nodejs:
        NodeCommand: "npm start"
      aws:elasticbeanstalk:application:environment:
        NODE_ENV: production
    

    DigitalOcean App Platform

    Create .do/app.yaml:
    name: my-feathers-app
    services:
    - name: web
      build_command: npm run compile
      run_command: node lib/index.js
      environment_slug: node-js
      envs:
      - key: NODE_ENV
        value: production
      - key: FEATHERS_SECRET
        type: SECRET
    

    Monitoring and Logging

    Structured Logging

    Use Winston or Pino for production logging:
    // src/logger.ts
    import winston from 'winston'
    
    export const logger = winston.createLogger({
      level: process.env.LOG_LEVEL || 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
      ),
      transports: [
        new winston.transports.Console(),
        new winston.transports.File({ 
          filename: 'logs/error.log', 
          level: 'error' 
        }),
        new winston.transports.File({ 
          filename: 'logs/combined.log' 
        })
      ]
    })
    

    Error Tracking

    Integrate error tracking (Sentry, Rollbar, etc.):
    npm install @sentry/node
    
    import * as Sentry from '@sentry/node'
    
    if (process.env.NODE_ENV === 'production') {
      Sentry.init({
        dsn: process.env.SENTRY_DSN,
        environment: process.env.NODE_ENV
      })
    }
    
    app.hooks({
      error: {
        all: [
          async (context) => {
            if (process.env.NODE_ENV === 'production') {
              Sentry.captureException(context.error)
            }
          }
        ]
      }
    })
    

    Health Checks

    Implement health check endpoints:
    app.get('/health', async (req, res) => {
      try {
        // Check database connection
        await db.raw('SELECT 1')
        
        res.json({
          status: 'healthy',
          uptime: process.uptime(),
          timestamp: Date.now()
        })
      } catch (error) {
        res.status(503).json({
          status: 'unhealthy',
          error: error.message
        })
      }
    })
    

    SSL/TLS Configuration

    Using HTTPS

    For production, use HTTPS:
    import https from 'https'
    import fs from 'fs'
    
    const options = {
      key: fs.readFileSync(app.get('ssl').key),
      cert: fs.readFileSync(app.get('ssl').cert)
    }
    
    const server = https.createServer(options, app)
    server.listen(port)
    
    In most cases, use a reverse proxy like Nginx:
    server {
      listen 80;
      server_name yourdomain.com;
      return 301 https://$server_name$request_uri;
    }
    
    server {
      listen 443 ssl http2;
      server_name yourdomain.com;
    
      ssl_certificate /path/to/cert.pem;
      ssl_certificate_key /path/to/key.pem;
    
      location / {
        proxy_pass http://localhost:3030;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
      }
    }
    

    Scaling Considerations

    Horizontal Scaling

    When running multiple instances, use a message queue for real-time events:
    npm install @feathersjs/transport-commons
    
    Configure Redis for pub/sub:
    import Redis from 'ioredis'
    
    const pub = new Redis(app.get('redis'))
    const sub = new Redis(app.get('redis'))
    
    // Sync events across instances
    app.on('publish', (event) => {
      pub.publish('feathers-events', JSON.stringify(event))
    })
    
    sub.subscribe('feathers-events')
    sub.on('message', (channel, message) => {
      const event = JSON.parse(message)
      app.emit(event.name, event.data)
    })
    

    Process Management

    Use PM2 for process management:
    npm install -g pm2
    
    Create ecosystem.config.js:
    module.exports = {
      apps: [{
        name: 'feathers-app',
        script: './lib/index.js',
        instances: 'max',
        exec_mode: 'cluster',
        env: {
          NODE_ENV: 'production'
        }
      }]
    }
    
    Start with PM2:
    pm2 start ecosystem.config.js
    pm2 save
    pm2 startup
    

    Continuous Deployment

    GitHub Actions Example

    # .github/workflows/deploy.yml
    name: Deploy to Production
    
    on:
      push:
        branches: [main]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        
        steps:
        - uses: actions/checkout@v3
        
        - name: Setup Node.js
          uses: actions/setup-node@v3
          with:
            node-version: '20'
        
        - name: Install dependencies
          run: npm ci
        
        - name: Run tests
          run: npm test
        
        - name: Build
          run: npm run compile
        
        - name: Deploy to server
          run: |
            # Your deployment commands
    

    Post-Deployment

    After deployment:
    1. Verify health checks - Ensure the application is responding
    2. Check logs - Monitor for errors or warnings
    3. Test critical paths - Verify authentication, key services
    4. Monitor performance - Watch CPU, memory, response times
    5. Set up alerts - Configure monitoring alerts

    Build docs developers (and LLMs) love