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}
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"]
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 "$@"
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:
- Establish a Virtual Network: A private bridge network is created on the host machine.
- 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 hostnamepostgres
).
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
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
-
Build and Run (Initial or Full Redeployment):
podman-compose up --build
-
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)