Sample application: Carved Rock Fitness (multi-tier architecture with a React front-end, workout-gateway, and run-controller)
Table of Contents
- Course Overview
- Building Node Images
- Configuring and Running Containers
- Debugging Containers
- Interactive Debugging with IDEs
- Running Multi-tier Applications with Docker Compose
- Reference Tables
1. Course Overview
This course covers the complete development lifecycle of Node.js applications in Docker containers:
| Module | Content |
|---|---|
| Building Node Images | Dockerfile, layers, cache, multi-stage builds |
| Configuring and Running Containers | docker run, ENV, ARG, volumes, ENTRYPOINT |
| Debugging Containers | docker logs, docker inspect, docker exec, Node.js debugger |
| Interactive Debugging with IDEs | WebStorm, VS Code with Docker |
| Running Multi-tier Apps with Docker Compose | Networking, docker-compose.yml, interdependent services |
The sample application used throughout the course is Carved Rock Fitness — a web application with:
- A React front-end (port 3000)
- A workout-gateway Node.js/Express (port 9000)
- A run-controller Node.js/Express (port 8000)
2. Building Node Images
Anatomy of a Node.js Dockerfile
A Dockerfile is a sequence of instructions that defines how to build a Docker image. Each instruction creates a layer in the final image. These layers are cached to speed up subsequent builds.
# Select an appropriate base image
FROM node:16-alpine
# Metadata via labels
LABEL maintainer="dev@carvedrockfitness.com"
LABEL version="1.0"
# Set the working directory inside the container
WORKDIR /app
# Copy ONLY package.json first (cache optimization)
COPY package*.json ./
# Install dependencies (layer cached as long as package.json doesn't change)
RUN npm ci --only=production
# Create a non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Copy the rest of the source code
COPY . .
# Document the exposed port
EXPOSE 9000
# Default command on container startup
CMD ["node", "src/index.js"]
Key Dockerfile Instructions
| Instruction | Role | Example |
|---|---|---|
FROM | Base image | FROM node:18-alpine |
WORKDIR | Working directory | WORKDIR /app |
COPY | Copy files (preferred over ADD) | COPY package*.json ./ |
ADD | Copy + extract archives/URLs | ADD archive.tar.gz /app |
RUN | Execute a command at build time | RUN npm install |
CMD | Default command on startup | CMD ["node", "index.js"] |
ENTRYPOINT | Fixed entry point | ENTRYPOINT ["docker-entrypoint.sh"] |
ENV | Persistent environment variable | ENV NODE_ENV=production |
ARG | Build-only variable | ARG NODE_VERSION=18 |
EXPOSE | Port documentation | EXPOSE 3000 |
LABEL | Metadata | LABEL version="1.0" |
USER | Execution user | USER node |
VOLUME | Mount point | VOLUME ["/data"] |
Important: Prefer
COPYoverADDto avoid unexpected behavior.ADDcan extract archives and download URLs, which makes behavior less predictable.
Build Cache Optimization
Docker cache works layer by layer. As soon as a layer is invalidated, all subsequent layers must be rebuilt.
Bad practice — invalidates the cache on every code change:
FROM node:18-alpine
WORKDIR /app
# Copying EVERYTHING invalidates the cache whenever any file changes
COPY . .
RUN npm install
CMD ["node", "index.js"]
Good practice — separate package.json from source code:
FROM node:18-alpine
WORKDIR /app
# Copy only package.json first
# npm install only runs when package.json changes
COPY package*.json ./
RUN npm ci --only=production
# Source code last: changes don't invalidate npm install
COPY . .
CMD ["node", "index.js"]
Force rebuild without cache:
docker build --no-cache -t myapp:latest .
.dockerignore
The .dockerignore file excludes files from the build context sent to the Docker daemon. This reduces context size and avoids unnecessarily invalidating the cache.
node_modules
npm-debug.log
.env
.env.*
.git
.gitignore
*.md
dist
coverage
.nyc_output
Dockerfile*
docker-compose*
Diagram: Node.js Architecture in Docker
graph TB
subgraph Host["Host Machine"]
subgraph DockerEngine["Docker Engine"]
subgraph Container["Container: workout-gateway"]
NodeProcess["Node.js Process\n(src/index.js)"]
NodeModules["node_modules/"]
AppCode["Application Code"]
NodeProcess --> AppCode
end
end
HostPort["Host Port 9000"]
end
Client["Browser / Client"]
Client -->|"HTTP :9000"| HostPort
HostPort -->|"Port Forward -p 9000:9000"| NodeProcess
style Container fill:#e8f4f8,stroke:#2196F3
style Host fill:#f5f5f5,stroke:#9e9e9e
Diagram: Multi-stage Build for Production
A multi-stage build creates lightweight production images by separating the build environment from the runtime environment.
flowchart LR
subgraph Stage1["Stage 1: builder"]
S1FROM["FROM node:18 AS builder"]
S1COPY["COPY package*.json ./"]
S1NPM["RUN npm ci"]
S1BUILD["RUN npm run build"]
S1FROM --> S1COPY --> S1NPM --> S1BUILD
end
subgraph Stage2["Stage 2: production (final image)"]
S2FROM["FROM node:18-alpine"]
S2COPY["COPY --from=builder /app/dist ./dist"]
S2DEPS["COPY --from=builder /app/node_modules ./node_modules"]
S2CMD["CMD [node, dist/index.js]"]
S2FROM --> S2COPY --> S2DEPS --> S2CMD
end
Stage1 -->|"Artifacts copied"| Stage2
style Stage1 fill:#fff3e0,stroke:#FF9800
style Stage2 fill:#e8f5e9,stroke:#4CAF50
Example multi-stage Dockerfile:
# ── Stage 1: builder ──────────────────────────────────────────────
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 2: production ───────────────────────────────────────────
FROM node:18-alpine AS production
WORKDIR /app
# Copy only the necessary artifacts from the builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 9000
CMD ["node", "dist/index.js"]
The final image does not include build tools, TypeScript sources, devDependencies, etc. — result: a much smaller and more secure image.
3. Configuring and Running Containers
Essential docker run Commands
# Basic startup
docker run node:18-alpine node --version
# Named container, in background, with port forwarding
docker run -d --name workout-gateway -p 9000:9000 carvedrockfitness/workout-gateway
# Interactive container (for debugging)
docker run -ti --rm node:18-alpine sh
# With environment variable
docker run -e NODE_ENV=production -e PORT=9000 myapp:latest
# Remove the container automatically when done
docker run --rm myapp:latest
# With mounted volume
docker run -v $(pwd)/src:/app/src myapp:latest
# With read-only bind mount
docker run -v $(pwd)/.env:/app/.env:ro myapp:latest
Most used switches:
| Switch | Description |
|---|---|
-d | Detached (background) |
-ti | Interactive terminal |
--rm | Remove after stop |
--name | Name the container |
-p host:container | Port forwarding |
-e KEY=value | Environment variable |
-v src:dst | Volume or bind mount |
--network | Docker network |
--net=host | Share host network |
Environment Variables: ENV vs ARG
The fundamental difference between ENV and ARG:
ENV | ARG | |
|---|---|---|
| Available during build | ✅ | ✅ |
| Available in the container | ✅ | ❌ |
Usable before FROM | ❌ | ✅ |
| Overridable at runtime | Via -e | Not applicable |
Visible in docker inspect | ✅ | ❌ |
# ARG before FROM: useful to parameterize the base image version
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
# ARG after FROM: visible during build, not in the container
ARG BUILD_DATE
RUN echo "Built on: $BUILD_DATE"
# ENV: persistent in the container
ENV NODE_ENV=production
ENV PORT=9000
ENV APP_VERSION=3.2.1
Passing ARG at build time:
docker build --build-arg NODE_VERSION=20 --build-arg BUILD_DATE=$(date) -t myapp .
Overriding ENV at runtime:
docker run -e NODE_ENV=development -e PORT=3000 myapp:latest
Volumes and Bind Mounts
Docker containers are stateless by default. Volumes allow data persistence independent of the container lifecycle.
Two mechanisms:
| Volume | Bind Mount | |
|---|---|---|
| Managed by | Docker | Host filesystem |
| Location | /var/lib/docker/volumes/ | Any host path |
| Primary use | Data persistence (DB) | Local development, hot-reload |
| Portability | High | Depends on host path |
| Permissions | Managed by Docker | Can be problematic |
# Create a named volume
docker volume create appdata
# Use a named volume
docker run -v appdata:/var/lib/postgresql/data postgres:15
# Bind mount (development) — share source code
docker run -v $(pwd)/src:/app/src -p 9000:9000 myapp:dev
# Read-only bind mount
docker run -v $(pwd)/.env:/app/.env:ro myapp:latest
# List volumes
docker volume ls
# Remove a volume
docker volume rm appdata
Diagram: Volumes for Hot-reload in Development
graph LR
subgraph Host["Host Machine"]
IDE["VS Code / IDE"]
HostSrc["./src/ (source code)"]
HostEnv[".env"]
IDE -->|edits| HostSrc
end
subgraph Container["Container: myapp-dev"]
NodeDev["Node.js + nodemon"]
ContSrc["/app/src/"]
ContEnv["/app/.env (read-only)"]
NodeDev -->|watches| ContSrc
end
HostSrc <-->|"bind mount -v ./src:/app/src"| ContSrc
HostEnv -->|"bind mount :ro"| ContEnv
NodeDev -->|"auto hot-reload"| Reload["♻️ App restarted"]
style Host fill:#fff3e0,stroke:#FF9800
style Container fill:#e3f2fd,stroke:#2196F3
Development Dockerfile with nodemon:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# Source code comes from a bind mount, not copied into the image
EXPOSE 9000
CMD ["npx", "nodemon", "--legacy-watch", "src/index.js"]
Launch in development mode with hot-reload:
docker run -d \
--name myapp-dev \
-p 9000:9000 \
-v $(pwd)/src:/app/src \
-e NODE_ENV=development \
myapp:dev
Reading Configuration in Node.js
// Method 1: process.env (standard)
const port = process.env.PORT || 9000;
const dbUrl = process.env.DATABASE_URL;
// Method 2: dotenv module (read a .env file)
// npm install dotenv
require('dotenv').config();
const port = process.env.PORT || 9000;
// Method 3: JSON configuration
const config = JSON.parse(
require('fs').readFileSync('./config.json', 'utf8')
);
// Method 4: nconf or config packages
// npm install config
const config = require('config');
const port = config.get('server.port');
Example .env (never commit in production):
PORT=9876
NODE_ENV=development
DEBUG=true
DATABASE_URL=postgres://user:password@localhost:5432/mydb
Initializing Containers: CMD and ENTRYPOINT
# CMD alone: default command, replaceable
CMD ["node", "src/index.js"]
# docker run myapp → runs node src/index.js
# docker run myapp npm test → runs npm test (replaces CMD)
# ENTRYPOINT: always executed
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "src/index.js"]
# docker run myapp → entrypoint.sh node src/index.js
# docker run myapp npm test → entrypoint.sh npm test
Entrypoint script to wait for a database:
#!/bin/sh
# docker-entrypoint.sh
set -e
# Wait for Postgres to be available
until pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER"; do
echo "Waiting for PostgreSQL..."
sleep 2
done
echo "PostgreSQL is ready!"
# Run the command passed as argument
exec "$@"
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "src/index.js"]
4. Debugging Containers
Logging in Containers
Docker containers capture stdout and stderr as logs. Everything logged to the console is accessible via docker logs.
# View logs of a container
docker logs workout-gateway
# Follow logs in real time
docker logs -f workout-gateway
# Logs from a timestamp
docker logs --since 2024-01-01T00:00:00 workout-gateway
# Last N lines only
docker logs --tail 50 workout-gateway
# With timestamps
docker logs -t workout-gateway
Logging with Express and Winston
Winston log levels (lowest to highest):
silly → debug → verbose → http → info → warn → error
// npm install winston morgan
const winston = require('winston');
const morgan = require('morgan');
const express = require('express');
const app = express();
// Configure the Winston logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
defaultMeta: { service: 'workout-gateway' },
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// If in debug mode, lower the log level
if (process.env.DEBUG === 'true') {
logger.level = 'debug';
}
// Helper for Morgan → Winston
const stream = {
write: (message) => logger.http(message.trim())
};
// Morgan middleware to log all HTTP requests
app.use(morgan('combined', { stream }));
// Use the logger in code
logger.info('Server starting...');
logger.error('Connection failed', { error: err.message });
logger.debug('Request payload', { body: req.body });
Alternatives to Winston:
| Library | Advantages | Usage |
|---|---|---|
console.log | No config | Quick debugging only |
debug | Namespace filtering | npm libraries |
winston | Multi-transport, levels, format | Production applications |
pino | Very performant, native JSON | High performance |
bunyan | Structured JSON | Microservices |
Inspecting and Executing in a Container
# Inspect the complete configuration of a container
docker inspect workout-gateway
# Filter with --format (Go template)
docker inspect --format='{{.NetworkSettings.IPAddress}}' workout-gateway
docker inspect --format='{{.Config.Env}}' workout-gateway
# Open a shell in a running container
docker exec -ti workout-gateway sh
# Execute a single command in a container
docker exec workout-gateway env
docker exec workout-gateway curl http://run-controller:8000/health
# View running processes in a container
docker top workout-gateway
# Real-time statistics
docker stats workout-gateway
Node.js Debugger via CLI
Two Node.js debugging modes:
| Command | Mode | Description |
|---|---|---|
node inspect index.js | Interactive CLI | Built-in terminal debugger |
node --inspect index.js | Remote (Chrome DevTools / IDE) | Exposes port 9229 |
node --inspect-brk index.js | Remote + pause | Pauses before the first line |
# Launch with remote debugger enabled
docker run -ti --rm \
-p 9000:9000 \
-p 9229:9229 \
myapp:dev \
node --inspect=0.0.0.0:9229 src/index.js
# Access via Chrome: chrome://inspect
# Access via VS Code: attach to port 9229
5. Interactive Debugging with IDEs
WebStorm
WebStorm is part of the IntelliJ family (JetBrains). It offers native Node.js and Docker support.
Docker configuration in WebStorm:
File→Settings→Build, Execution, Deployment→Docker- Add a Docker environment (Docker for Windows, Docker Machine, or TCP Socket)
- Create a Run Configuration of type
Dockerfile - Create a
Node.jsconfiguration with Docker as the Remote Target - Set breakpoints and start debugging
IDEs compatible with the Node.js debugger:
| IDE | Minimum version |
|---|---|
| VS Code | 1.10+ |
| Visual Studio | 2017+ |
| WebStorm | 2017.1+ |
| Eclipse | + Wild Web Developer extension |
VS Code
VS Code offers a better Node.js/Docker debugging experience than WebStorm according to the course.
Automatically generate a Dockerfile in VS Code:
- Open the command palette (
Ctrl+Shift+P) - Search for “Add Docker Files to Workspace”
- Select Node.js, the port, and whether docker-compose is wanted
- VS Code generates
Dockerfile,.dockerignore, and a debug configuration
Debug configuration .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker: Attach to Node",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"restart": true,
"protocol": "inspector"
},
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"node": {
"package": "${workspaceFolder}/package.json"
}
}
]
}
Tasks configuration .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"type": "docker-build",
"label": "docker-build",
"platform": "node",
"dockerBuild": {
"tag": "myapp:latest",
"dockerfile": "${workspaceFolder}/Dockerfile",
"context": "${workspaceFolder}"
}
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": ["docker-build"],
"dockerRun": {
"ports": [
{ "hostPort": 9000, "containerPort": 9000 },
{ "hostPort": 9229, "containerPort": 9229 }
]
},
"node": {
"enableDebugging": true
}
}
]
}
6. Running Multi-tier Applications with Docker Compose
Docker Networking
Docker offers several network modes:
| Mode | Description | Usage |
|---|---|---|
| Bridge (default) | Shared bridge network docker0 | Isolated containers, little service discovery |
| User-defined Bridge | Custom bridge with auto DNS | ✅ Recommended for multi-containers |
| Host | Shares the host’s network | Maximum performance, less isolation |
| None | No network | Completely isolated containers |
| Overlay | Multi-host (Docker Swarm) | Distributed clusters |
Network commands:
# Create a user-defined network
docker network create frontend-network
docker network create backend-network
# Connect a container to a network
docker network connect backend-network run-controller
# Disconnect
docker network disconnect frontend-network run-controller
# Inspect a network
docker network inspect frontend-network
# List networks
docker network ls
Advantages of User-defined Bridge vs Default Bridge:
- ✅ Automatic DNS: containers find each other by name
- ✅ Better isolation: containers on different networks can’t see each other
- ✅ Attach/detach on the fly without restarting
- ✅ Multiple networks possible per container
Port Forwarding
# Simple forwarding: host:container
docker run -p 3000:3000 myapp
# Random port on the host
docker run -p 3000 myapp
# Bind to a specific interface
docker run -p 127.0.0.1:3000:3000 myapp
# All EXPOSE ports from the Dockerfile
docker run -P myapp
# Document ports in the Dockerfile (does not actually publish)
EXPOSE 9000/tcp
EXPOSE 9229/tcp
Docker Compose: YAML Syntax
Structure of a docker-compose.yml file:
version: "3.9"
services:
# ─── React Front-end ──────────────────────────────────────────
front:
image: carvedrockfitness/front:v1.3
build:
context: ./front
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- REACT_APP_BACKEND_URL=http://localhost:9000
depends_on:
- workout-gateway
networks:
- frontend
# ─── Node.js API Gateway ──────────────────────────────────────
workout-gateway:
image: carvedrockfitness/workout-gateway:v1.3
build:
context: ./workout-gateway
ports:
- "9000:9000"
environment:
- NODE_ENV=production
- PORT=9000
- RUN_CONTROLLER_URL=http://run-controller:8000
depends_on:
- run-controller
networks:
- frontend
- backend
volumes:
- gateway-logs:/app/logs
# ─── Node.js Backend ──────────────────────────────────────────
run-controller:
build:
context: ./run-controller
environment:
- NODE_ENV=production
- PORT=8000
- DATABASE_URL=postgres://user:password@db:5432/fitness
depends_on:
db:
condition: service_healthy
networks:
- backend
# ─── PostgreSQL Database ──────────────────────────────────────
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=fitness
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
# ─── Named Volumes ──────────────────────────────────────────────
volumes:
pgdata:
gateway-logs:
# ─── Networks ───────────────────────────────────────────────────
networks:
frontend:
backend:
Docker Compose for development (override):
# docker-compose.override.yml (loaded automatically with docker-compose up)
version: "3.9"
services:
workout-gateway:
build:
target: development
volumes:
- ./workout-gateway/src:/app/src # hot-reload
environment:
- NODE_ENV=development
- DEBUG=true
ports:
- "9229:9229" # Node.js debugger port
command: ["node", "--inspect=0.0.0.0:9229", "src/index.js"]
run-controller:
volumes:
- ./run-controller/src:/app/src
environment:
- NODE_ENV=development
Diagram: Docker Compose Multi-services
graph TB
subgraph DockerCompose["docker-compose.yml"]
subgraph FrontendNet["Network: frontend"]
Front["front\nReact :3000"]
Gateway["workout-gateway\nNode.js :9000"]
end
subgraph BackendNet["Network: backend"]
Gateway2["workout-gateway"]
RunCtrl["run-controller\nNode.js :8000"]
DB["db\nPostgreSQL :5432"]
end
Front -->|"depends_on"| Gateway
Gateway -->|"RUN_CONTROLLER_URL\nhttp://run-controller:8000"| RunCtrl
RunCtrl -->|"DATABASE_URL"| DB
DB --> PGVol[("pgdata\nvolume")]
end
Browser["Browser"]
Browser -->|"http://localhost:3000"| Front
Browser -->|"http://localhost:9000"| Gateway
style FrontendNet fill:#e3f2fd,stroke:#1565C0
style BackendNet fill:#f3e5f5,stroke:#6A1B9A
style DockerCompose fill:#f5f5f5,stroke:#616161
Automatic DNS: In a user-defined bridge,
workout-gatewaycan reachrun-controllersimply by its service name. Docker automatically resolvesrun-controller→ container IP.
Essential Docker Compose Commands
# Start all services (build if necessary)
docker-compose up
# In the background
docker-compose up -d
# Forced image rebuild
docker-compose up --build
# Stop and remove containers, networks, volumes
docker-compose down
# Also remove named volumes
docker-compose down -v
# Stop without removing
docker-compose stop
# Restart a service
docker-compose restart workout-gateway
# Build images without starting
docker-compose build
# Logs for all services
docker-compose logs -f
# Logs for a specific service
docker-compose logs -f workout-gateway
# Execute a command in a service
docker-compose exec workout-gateway sh
# Run a one-shot container
docker-compose run --rm workout-gateway npm test
# View service status
docker-compose ps
# Scale a service
docker-compose up --scale run-controller=3
Testing multiple Node.js versions with Docker Compose:
# docker-compose.test.yml
version: "3.9"
services:
test-node18:
build:
args:
NODE_VERSION: "18"
command: npm test
test-node20:
build:
args:
NODE_VERSION: "20"
command: npm test
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
7. Reference Tables
Official Node.js Images
| Image | Approximate Size | Recommended Usage |
|---|---|---|
node:20 | ~950 MB | Build, development |
node:20-slim | ~240 MB | General production |
node:20-alpine | ~60 MB | Optimized production |
node:20-bullseye | ~950 MB | Maximum compatibility (Debian) |
node:20-bullseye-slim | ~240 MB | Production on Debian |
node:lts | ~950 MB | Always the latest LTS |
node:lts-alpine | ~60 MB | LTS + Alpine |
node:current-alpine | ~60 MB | Latest version + Alpine |
Alpine Linux: very lightweight images based on
musl libcandBusyBox. Sometimes problematic with native npm modules that expectglibc. Prefer-slim(Debian) in case of issues.
Docker Optimizations for Node.js
| Optimization | Description | Impact |
|---|---|---|
Separate COPY package.json from code | Caches npm install as long as dependencies don’t change | ⬆️ Build speed |
npm ci instead of npm install | Installs exactly what is in package-lock.json | ✅ Reproducibility |
--only=production | Does not install devDependencies | ⬇️ Image size |
| Multi-stage build | Separates build and runtime | ⬇️⬇️ Image size |
| Alpine base image | Minimal base image | ⬇️ Image size |
.dockerignore | Excludes node_modules, .git, etc. | ⬆️ Build speed |
| Non-root user | USER node or dedicated user | ✅ Security |
COPY before RUN | Minimize invalidated layers | ⬆️ Cache |
--no-cache for Alpine packages | apk add --no-cache | ⬇️ Image size |
| Labels | Metadata for traceability | ✅ Maintenance |
Complete optimized example:
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine AS base
LABEL maintainer="team@company.com"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
# ── Dependencies ──────────────────────────────────────────────────
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# ── Builder ───────────────────────────────────────────────────────
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Production ────────────────────────────────────────────────────
FROM base AS production
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=9000
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json ./
USER appuser
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:9000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD ["node", "dist/index.js"]
Volumes vs Bind Mounts Comparison
| Criterion | Named Volume | Bind Mount | tmpfs Mount |
|---|---|---|---|
| Managed by Docker | ✅ | ❌ | ✅ |
| Persists after container stop | ✅ | ✅ | ❌ (memory) |
| Shareable between containers | ✅ | ✅ | ❌ |
Backupable with docker volume | ✅ | ❌ | ❌ |
| Performance (Linux) | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Performance (macOS/Windows) | ⭐⭐⭐ | ⭐⭐ (virtualization) | N/A |
| Typical usage | DB, persistent data | Development, hot-reload | Cache, temporary sensitive data |
| Syntax | -v pgdata:/var/lib/pgsql | -v $(pwd)/src:/app/src | --tmpfs /tmp |
Best practices summary:
- Always use a specific base image (not
:latest)- Run containers with a non-root user
- Separate
package.jsonfrom source code to optimize cache- Use multi-stage builds for production images
- Always have a
.dockerignorewith at leastnode_modules- Use user-defined bridges for inter-container communication
- Use
depends_onwith healthcheck for critical dependencies (databases)- Never store secrets in images — use environment variables or secrets managers
Search Terms
node.js · developing · apps · docker · containerization · containers · kubernetes · compose · diagram · volumes · bind · commands · debugging · dockerfile · essential · images · logging · mounts · running