DEV Community

Cover image for 🐳 Enterprise-Grade Containerization for Node.js Backends
Waffeu Rayn
Waffeu Rayn

Posted on

🐳 Enterprise-Grade Containerization for Node.js Backends

This comprehensive guide details the pattern for containerizing a Node.js backend with PostgreSQL using a multi-stage Dockerfile and Podman Compose. The configuration is optimized for security, efficiency, and resilience, providing a robust framework suitable for any Node.js application deployment.

1. Core Application Configuration

1.1 package.json (The Essential Execution Scripts)

The package.json defines the build and runtime scripts used within the container environment. The primary goal is to execute compiled code after migrations are complete.

Script Command (Generic) Detailed Rationale
"build" [YOUR_BUILD_COMMAND] Compiles source code (e.g., TypeScript) into production JavaScript files, typically placed in a ./dist directory.
"db:migrate" [YOUR_DB_CLI_TOOL] [migration:run_command] Database Integrity: Command to run the necessary database schema migrations before the server starts.
"start" node ./dist/src/index.js Application Startup: Command to execute the main compiled entry point.

1.2 .env (Environment Variables)

This file manages configuration data, using generic placeholders for sensitive data.

# --- Application Configuration ---
PORT=3000
APP_ENV=production

# --- Database Credentials (Internal Application Use) ---
DB_USER=app_user
DB_PASS=secure_db_pass
DB_NAME=app_db

# --- Postgres Image Required Variables (Used by the Container Image) ---
POSTGRES_USER=${DB_USER}
POSTGRES_PASSWORD=${DB_PASS}
POSTGRES_DB=${DB_NAME}
Enter fullscreen mode Exit fullscreen mode

2. The Multi-Stage Dockerfile (Security and Efficiency)

A multi-stage build separates the environment with large development dependencies from the small, secure runtime environment.

Dockerfile

# --- STAGE 1: Build & Compilation (AS builder) ---
FROM node:20-alpine AS builder
WORKDIR /app

# 1. Dependency Installation: Install all dependencies (including devDependencies for compilation)
COPY package*.json ./
RUN npm ci

# 2. Source Compilation
COPY . .
RUN npm run build

# --- STAGE 2: Production Runtime (AS final) ---
FROM node:20-alpine as final
WORKDIR /usr/src/app

# Security Best Practice: Create and use a dedicated non-root user
RUN adduser --system --uid 1001 nodejs && \
    mkdir -p /usr/src/app && \
    chown -R 1001:1001 /usr/src/app
USER nodejs

# Configuration Copy (Temporarily elevating to copy system scripts)
USER root
COPY bin/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
USER nodejs # Switch back to non-root user

# 1. Dependency Optimization: Install ONLY production dependencies
COPY package*.json ./
RUN npm ci --omit=dev

# 2. Application Files: Copy ONLY the compiled output from the 'builder' stage
COPY --from=builder --chown=1001:1001 /app/dist ./dist

# Final configuration
EXPOSE 3000
ENTRYPOINT [ "entrypoint.sh" ]

# EXECUTION: Executes the 'start' script from package.json
CMD ["npm", "run", "start"]
Enter fullscreen mode Exit fullscreen mode
Section Detailed Rationale
Stage 1 (builder) Handles compilation. The large footprint of devDependencies is isolated here and discarded after the build.
USER nodejs Security: The application runs as a non-root user (uid 1001). This limits the privileges an attacker would gain if the container were compromised.
npm ci --omit=dev Efficiency: Installing only production dependencies significantly reduces the final image size and minimizes the security surface area.

3. The Database Entrypoint & Migrations

The entrypoint.sh script is the crucial element for reliable startup, managing the dependency on the PostgreSQL database.

bin/entrypoint.sh

#!/bin/sh

# This script ensures Postgres is up before running migrations and starting the app.

# Hostname is the service name in docker-compose (internal DNS resolution)
DB_HOST="postgres"
DB_PORT="5432"

echo "Waiting for PostgreSQL ($DB_HOST:$DB_PORT)..."

# Loop until the database service is ready (using netcat for port check)
while ! nc -z $DB_HOST $DB_PORT; do
  sleep 0.5
done

echo "PostgreSQL started. Running migrations..."

# Executes the database migration script
npm run db:migrate

echo "Migrations complete. Starting application..."

# Execute the application's main command (CMD from Dockerfile)
# 'exec' ensures proper signal handling and process management.
exec "$@"
Enter fullscreen mode Exit fullscreen mode
Component Detailed Rationale
while ! nc -z $DB_HOST $DB_PORT; do ... Race Condition Prevention: This loop forces the application to wait until the PostgreSQL port is open and listening, preventing a "Connection Refused" error upon startup.
npm run db:migrate Schema Integrity: Guarantees that the database schema is updated to the latest version before the main server process attempts to use the tables.
exec "$@" Process Management: The exec command replaces the current shell process with the final application process (npm run start). This is essential for ensuring that signals (like SIGTERM from Podman Compose on shutdown) are correctly received and handled by the Node.js process, allowing for graceful termination.

4. Podman Compose & Container Networking (CNI Explained)

Podman Compose defines and orchestrates the multi-container application and relies on the Container Network Interface (CNI) for connectivity.

CNI (Container Network Interface) Explained

CNI is the standard that governs how container runtimes (like Podman) create, manage, and connect containers to networks. When you define an app_network with driver: bridge, Podman utilizes the CNI to:

  1. Establish a Virtual Network: A private bridge network is created on the host machine.
  2. Enable Internal DNS: All services connected to this network are assigned IP addresses and can resolve each other using their service names (e.g., the backend container can connect to the database simply by using the hostname postgres).

docker-compose.yaml (Podman Compose)

services:
  # --- BACKEND APPLICATION SERVICE ---
  backend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:3000" # Host port 8080 -> Container port 3000
    env_file:
      - .env # Load environment variables
    environment:
      DB_HOST: postgres # Internal service name resolution
      DB_PORT: 5432
    networks:
      - app_network
    depends_on:
      - postgres

  # --- POSTGRESQL DATABASE SERVICE ---
  postgres:
    image: postgres:15-alpine
    ports:
      # Security: Binds the port ONLY to the localhost interface
      - "127.0.0.1:5432:5432"
    env_file:
      - .env # Loads POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
    volumes:
      - postgres_data:/var/lib/postgresql/data # Persistent storage for data
    networks:
      - app_network

# --- VOLUMES & NETWORKS ---
volumes:
  postgres_data:
networks:
  app_network:
    driver: bridge # Specifies the CNI bridge network type
Enter fullscreen mode Exit fullscreen mode
Component Detailed Rationale
DB_HOST: postgres The application uses the service name as the database hostname, leveraging the CNI's internal DNS resolver for efficient inter-container communication.
ports: 127.0.0.1:5432:5432 Security: Binding to the host's loopback address (127.0.0.1) prevents external connections to the database, ensuring the database is only accessible from the host itself and the internal app_network.
volumes: postgres_data Persistence: Ensures the database data survives container removal, preventing data loss during updates or redeployments.

Conclusion

This documentation outlines a robust and production-ready pattern for containerizing a Node.js backend. The integration of multi-stage builds, non-root execution, and CNI-managed networking provides a foundation that is secure, efficient, and resilient against common deployment failures. By strictly separating compilation from runtime and sequencing the database startup, this pattern guarantees a predictable and reliable deployment lifecycle.


Deployment and Cleanup Commands

  1. Build and Run (Initial or Full Redeployment):

    podman-compose up --build
    
  2. Remove Containers AND Persistent Volumes (Use with Caution):

    podman-compose down -v
    

    (Use the -v flag to delete the data volume if you need a completely clean start.)

Example of where you can find these files cm.holidays. But do not use it in this project 😡 as you will faced some problem as it's an old one i did inn hurry.

Top comments (0)