8 Topics
Chapter•45 Minutes

Production Deployment and Monitoring

Prepare the REST API for production using environment configuration, structured logging, health checks, graceful shutdown, Docker, and deployment validation.

Prepare Production Environment

Before deployment, configure the application for a production environment.

Important settings include:

  • NODE_ENV=production.
  • PORT provided by the hosting platform.
  • Production MongoDB URI.
  • Strong JWT signing secret.
  • Trusted frontend origin.
  • Secure database credentials.

Never commit production secrets to Git.

Use the hosting platform's environment configuration or a dedicated secret manager.

Also ensure that your production database uses proper network restrictions and appropriate access privileges.

Production Configuration

NODE_ENV

production

PORT

Platform-provided port

MONGODB_URI

Production database

JWT_SECRET

Secure secret

CLIENT_ORIGIN

Trusted frontend origin

Key Takeaways
  • Separate production and development environments.

  • Store credentials in secure configuration.

  • Restrict database network access.

Implement Health and Readiness Checks

Production platforms need endpoints to determine whether the application is running and whether it can serve requests.

A liveness endpoint checks whether the application process is responsive.

A readiness endpoint checks whether required dependencies are available.

For our application, MongoDB is a critical dependency.

We can inspect the Mongoose connection state to determine whether the database is connected.

Health and Readiness Endpoints
import mongoose from 'mongoose';

app.get('/health/live', (req, res) => {
  res.status(200).json({
    status: 'alive'
  });
});

app.get('/health/ready', (req, res) => {
  const databaseReady = mongoose.connection.readyState === 1;

  if (!databaseReady) {
    return res.status(503).json({
      status: 'not-ready',
      database: 'disconnected'
    });
  }

  res.status(200).json({
    status: 'ready',
    database: 'connected'
  });
});

Provides a liveness endpoint and a readiness endpoint that checks the MongoDB connection state.

Key Takeaways
  • Liveness checks indicate whether the application process is running.

  • Readiness checks indicate whether the application can serve traffic.

  • Check critical dependencies before reporting readiness.

Implement Graceful Shutdown

When the application receives a termination signal, it should stop accepting new connections and release resources cleanly.

The shutdown sequence should:

  1. Stop accepting new HTTP connections.
  2. Allow active requests to finish within a reasonable deadline.
  3. Disconnect from MongoDB.
  4. Exit the process.

This is particularly important in containerized environments and during automated deployments.

Production Shutdown Deadline

Add a shutdown timeout and account for keep-alive connections. Your application should complete cleanup within the hosting platform's termination grace period.

Graceful Shutdown
import http from 'node:http';
import app from './app.js';
import { connectDatabase, disconnectDatabase } from './config/database.js';

const server = http.createServer(app);

async function startServer() {
  await connectDatabase();

  server.listen(process.env.PORT || 3000);
}

async function shutdown(signal) {
  console.log(`${signal} received`);

  server.close(async (error) => {
    try {
      await disconnectDatabase();

      if (error) {
        console.error(error);
        process.exit(1);
      }

      process.exit(0);
    } catch (shutdownError) {
      console.error(shutdownError);
      process.exit(1);
    }
  });
}

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

startServer().catch((error) => {
  console.error(error);
  process.exit(1);
});

Creates an explicit HTTP server, closes it on termination, and disconnects from MongoDB before exiting.

Key Takeaways
  • Handle SIGTERM and SIGINT.

  • Stop accepting new connections during shutdown.

  • Close database connections before exiting.

Configure Application Logging

Logging helps developers understand application behavior and diagnose production failures.

Avoid relying exclusively on console.log() for production applications.

A structured logger should provide:

  • Log levels.
  • Timestamps.
  • Request identifiers.
  • Error context.
  • JSON output.
  • Configurable log destinations.

Common log levels include:

  • debug.
  • info.
  • warn.
  • error.

Never log passwords, access tokens, authorization headers, or sensitive personal information.

Basic Structured Logging
export const logger = {
  info(message, metadata = {}) {
    console.log(JSON.stringify({
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      ...metadata
    }));
  },

  error(message, metadata = {}) {
    console.error(JSON.stringify({
      level: 'error',
      message,
      timestamp: new Date().toISOString(),
      ...metadata
    }));
  }
};

Creates a minimal JSON logger. For production applications, use a dedicated logging library with appropriate serialization and redaction.

Key Takeaways
  • Structured logs simplify troubleshooting.

  • Use appropriate log levels.

  • Never log authentication secrets.

Dockerize the Node.js REST API

Docker packages the application and its runtime dependencies into a container image.

This makes it easier to run the same application across development, testing, and deployment environments.

Create a Dockerfile in the project root.

We will use a multi-stage build structure only if the application requires a build step. Since this is a plain JavaScript application, a simple production dependency installation is sufficient.

The container should run as a non-root user and should not include unnecessary development dependencies.

Dockerfile
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY src ./src

ENV NODE_ENV=production

USER node

EXPOSE 3000

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

Creates a production container using Node.js Alpine, installs production dependencies, copies application source, and runs as a non-root user.

Key Takeaways
  • Docker packages the application into a container.

  • Install only production dependencies.

  • Run containers with non-root permissions.

Create Docker Ignore Configuration

Create a .dockerignore file.

Docker uses this file to exclude unnecessary files from the build context.

Exclude node_modules, environment files, test output, and local development artifacts.

This reduces image build context size and prevents accidental inclusion of secrets.

.dockerignore
node_modules
.env
.env.*
.git
.gitignore
coverage
logs
npm-debug.log
Dockerfile*
README.md

Prevents unnecessary files and local secrets from being included in the Docker build context.

Key Takeaways
  • Use .dockerignore to reduce the build context.

  • Exclude environment files and local dependencies.

  • Never bake production secrets into Docker images.

Build and Run the Docker Container

Build the Docker image from the project root.

Run:

docker build -t nodejs-rest-api .

Start the container:

docker run -d \
  --name rest-api \
  -p 3000:3000 \
  --env-file .env \
  nodejs-rest-api

The application will be accessible at:

http://localhost:3000

For this local demonstration, .env contains the required configuration. In production, use your deployment platform's secret management rather than copying development credentials into a container.

Build and Run Docker Image
docker build -t nodejs-rest-api .
docker run -d --name rest-api -p 3000:3000 --env-file .env nodejs-rest-api

Builds the image and starts a container with port 3000 exposed.

Key Takeaways
  • docker build creates the container image.

  • docker run starts the application container.

  • Environment variables configure the running application.

Verify the Production Deployment

After deploying the application, verify the most important endpoints.

1. Check application health

GET /health/live

2. Check database readiness

GET /health/ready

3. Register a user

POST /api/v1/auth/register

4. Log in

POST /api/v1/auth/login

5. Retrieve protected resources

GET /api/v1/users

Include the access token in the Authorization header.

6. Verify authorization

Try accessing an administrator-only endpoint using a normal user account.

7. Verify error handling

Send malformed IDs and invalid request bodies.

8. Verify logs

Inspect application and database connection logs.

The application should return appropriate status codes and should not expose internal credentials or stack traces.

Key Takeaways
  • Verify health and readiness endpoints.

  • Test registration, login, and protected resources.

  • Verify both successful and unsuccessful requests.

  • Check logs and ensure sensitive information is not exposed.