Introduction: From Monolith to Microservices with Docker and JavaScript
Modern web development in JavaScript (especially in the Node.js ecosystem) is unthinkable without containerization. Docker has ceased to be just a tool for "packaging" an application — it is the de facto standard for delivering, scaling, and managing microservices. If you have already mastered basic Dockerfile and docker run, it's time to move to the advanced level.
In this article, we will analyze how to create efficient, secure, and lightweight Docker images for Node.js, configure multi-container environments using Docker Compose, organize inter-service communication through networks, and competently manage data via volumes. The material is aimed at developers who want to build production-ready architectures.
1. Optimizing Dockerfile for Node.js: Layers, Caching, and Security
A classic Dockerfile for Node.js often looks like this: FROM node:18, COPY . /app, RUN npm install. However, this approach leads to giant images (over 1 GB) and long build times. Advanced optimization is based on three principles:
1.1 Multi-stage Builds
Separate the process into a build stage (with compilers and dev dependencies) and a runtime stage (only runtime). This reduces the final image by 5-10 times.
# ---- Build Stage ----FROM node:18-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .RUN npm run build
# ---- Runtime Stage ----FROM node:18-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesEXPOSE 3000CMD ["node", "dist/main.js"]1.2 Using .dockerignore and Layer Caching
Docker caches layers. To avoid rebuilding npm install on every code change, copy package.json separately. Add a .dockerignore:
node_modules.git*.md.envdistNow npm install is executed only when dependencies change, not the entire project.
1.3 Security: Non-root User and Minimal Base Image
Use node:18-alpine (or even scratch with a compiled binary via pkg). Never run Node.js as root:
RUN addgroup -S appgroup && adduser -S appuser -G appgroupUSER appuserThis prevents privilege escalation in case of application compromise.
2. Docker Compose: Multi-Container Application (Node.js + Redis + Nginx)
A real application rarely consists of a single service. Docker Compose allows you to spin up the entire infrastructure with a single command docker-compose up. Let's look at an advanced example: Node.js API, Redis for caching, and Nginx as a reverse proxy.
2.1 Structure of docker-compose.yml
version: '3.8'services: app: build: context: ./app dockerfile: Dockerfile.prod ports: - "3000:3000" environment: - REDIS_URL=redis://redis:6379 - NODE_ENV=production depends_on: - redis restart: unless-stopped
redis: image: redis:7-alpine volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 3
nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - app
volumes: redis-data:2.2 Advanced Settings: Healthcheck and Dependencies
depends_on guarantees the startup order but does not wait for the service to be ready. Use healthcheck for Redis and a wait script in the application:
// In index.js Node.js
const co