Introduction: From Simple to Complex in the World of Containerization
\n\nAt the beginner level, working with Docker and JavaScript (Node.js) often comes down to creating a simple Dockerfile and running a container. However, in real production systems, this is not enough. The advanced level implies a deep understanding of build mechanisms, image size optimization, dependency management, network isolation, and security.
In this article, we will cover the key aspects of professional containerization of Node.js applications. You will learn how to use multi-stage builds, configure Docker Compose for a microservice architecture, manage environment variables, and minimize the attack surface. The material is aimed at developers already familiar with the basics of Docker and JavaScript.
\n\n1. Multi-stage builds for Node.js
\n\nOne of the main problems when containerizing Node.js applications is the excessive size of the final image. The standard approach with a single Dockerfile includes build tools (TypeScript, Webpack, Babel) that are not needed at runtime. Multi-stage builds allow you to split the process into stages: build and execution.
1.1. Example Dockerfile with two stages
\n\n# Build stage\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --only=production\nCOPY . .\nRUN npm run build # Assumes the build script compiles the project\n\n# Production stage\nFROM node:20-alpine AS production\nWORKDIR /app\n# Copy only node_modules and built files\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/dist ./dist\nCOPY package*.json ./\n\nEXPOSE 3000\nUSER node\nCMD ["node", "dist/index.js"]\n\nKey points:
\n- \n
node:20-alpine— a lightweight image based on Alpine Linux (about 30 MB). \n npm ci— a clean installation of dependencies strictly according topackage-lock.json. \n --only=production— excludes dev-dependencies (TypeScript, tests). \n USER node— avoids using the root user for security. \n
As a result, the final image contains only the files necessary for running, and the size is reduced from 300-400 MB to 80-120 MB.
\n\n1.2. Optimizing layer caching
\n\nDocker caches each layer (command). To speed up rebuilds, copy package*.json before copying the entire source code. If the dependencies haven't changed, the layer with npm ci will be taken from the cache.
COPY package*.json ./\nRUN npm ci --only=production\nCOPY . . # Only copied if the source code has changed\n\n2. Docker Compose for Microservice Architecture
\n\nIn production, a Node.js application rarely works in isolation. It usually requires a database (PostgreSQL, MongoDB), a cache (Redis), or a message queue (RabbitMQ). Docker Compose allows you to describe the entire infrastructure in a single YAML file.
\n\n2.1. Example docker-compose.yml for Node.js + Redis
\n\nversion: '3.8'\nservices:\n app:\n build:\n context: .\n dockerfile: Dockerfile.prod\n ports:\n - "3000:3000"\n environment:\n - NODE_ENV=production\n - REDIS_URL=redis://redis:6379\n depends_on:\n - redis\n networks:\n - backend\n\n redis:\n image: redis:7-alpine\n ports:\n - "6379:6379"\n volumes:\n - redis_data:/data\n networks:\n - backend\n\nvolumes:\n redis_data:\n\nnetworks:\n backend:\n driver: bridge\n\nImportant aspects:
\n- \n
- Network isolation: services communicate through the internal
backendnetwork. Redis is not accessible from outside (unless the port is exposed). \n - Named volumes:
redis_datasaves