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
production
Platform-provided port
Production database
Secure secret
Trusted frontend origin
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.
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.
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:
- Stop accepting new HTTP connections.
- Allow active requests to finish within a reasonable deadline.
- Disconnect from MongoDB.
- Exit the process.
This is particularly important in containerized environments and during automated deployments.
Add a shutdown timeout and account for keep-alive connections. Your application should complete cleanup within the hosting platform's termination grace period.
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.
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.
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.
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.
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.
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.
node_modules
.env
.env.*
.git
.gitignore
coverage
logs
npm-debug.log
Dockerfile*
README.mdPrevents unnecessary files and local secrets from being included in the Docker build context.
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:
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.
docker build -t nodejs-rest-api .
docker run -d --name rest-api -p 3000:3000 --env-file .env nodejs-rest-apiBuilds the image and starts a container with port 3000 exposed.
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.
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.