What You’ll Learn in This Tutorial
- How to write Dockerfiles and the meaning of each instruction
- Building images and starting containers
- Managing multiple containers with docker-compose
- Building a Node.js + PostgreSQL development environment
Prerequisites: Docker Desktop must be installed. If
docker --versiondisplays a version, you’re good to go.
What is Docker? Why Was It Created?
History of Container Technology
The roots of container technology date back to chroot in UNIX V7 in 1979. It has evolved as follows:
| Year | Technology | Overview |
|---|---|---|
| 1979 | chroot | Filesystem isolation |
| 2000 | FreeBSD Jail | Process isolation |
| 2006 | cgroups (Google) | Resource limits |
| 2008 | LXC (Linux Containers) | Lightweight containers for Linux |
| 2013 | Docker | Container standardization and adoption |
The Birth of Docker
In 2013, Solomon Hykes (dotCloud) announced Docker.
“Docker made it possible to easily package applications and run them identically anywhere.” — Docker Official
Why Docker Was Revolutionary
- Share “working environments” as-is: Solved the “it works on my machine” problem
- Lightweight: Shares OS unlike VMs, starts in seconds
- Image reproducibility: Complete environment reproduction via Dockerfile
- Ecosystem: Image sharing via Docker Hub
Difference Between VMs and Containers
flowchart TB
subgraph VM["Virtual Machine (VM)"]
direction TB
VA["App A"] --- VB["App B"]
VB --- VGA["Guest OS"]
VGA --- VGB["Guest OS"]
VGB --- VH["Hypervisor"]
VH --- VHO["Host OS"]
VHO --- VHW["Hardware"]
end
subgraph Container["Container"]
direction TB
CA["App A"] --- CB["App B"]
CB --- CR["Container Runtime<br/>(Docker)"]
CR --- CHO["Host OS"]
CHO --- CHW["Hardware"]
end
| Property | VM | Container |
|---|---|---|
| Startup time | Minutes | Seconds |
| Memory usage | GB scale | MB scale |
| Isolation level | Complete | Process level |
| OS independence | Complete | Shared kernel |
Official Documentation: Docker overview
Docker Basic Concepts
Images and Containers
- Image: The “blueprint” of an application. Read-only
- Container: A “running instance” created from an image
flowchart LR
subgraph Static["Image (static)"]
Dockerfile["Dockerfile"]
Image["node:20"]
Dockerfile -->|build| Image
end
subgraph Dynamic["Container (dynamic)"]
Running["running"]
Stdout["stdout"]
Running -->|logs| Stdout
end
Image -->|run| Running
Image Layer Structure
Docker images are composed of layers, and only changed parts are rebuilt:
FROM node:20-alpine # Base layer (~100MB)
WORKDIR /app # Config layer (~0KB)
COPY package*.json ./ # Dependencies layer (~1KB)
RUN npm install # node_modules layer (~50MB)
COPY . . # App code layer (~10KB)
Best Practice: Place less frequently changed items at the top and more frequently changed items at the bottom for efficient cache utilization
Step 1: Creating the Project Structure
First, create the project directory structure.
mkdir docker-tutorial
cd docker-tutorial
mkdir src
touch Dockerfile docker-compose.yml src/index.js package.json
Step 2: Creating the Node.js Application
Create a simple Express server.
package.json
{
"name": "docker-tutorial",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
src/index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from Docker!',
timestamp: new Date().toISOString()
});
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3: Creating the Dockerfile
A Dockerfile is a blueprint for building images.
Dockerfile
# Specify base image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy dependency files (cache optimization)
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Expose port (documentation purpose)
EXPOSE 3000
# Startup command
CMD ["npm", "start"]
Detailed Dockerfile Instruction Guide
| Instruction | Description | Example |
|---|---|---|
FROM | Specify base image | FROM node:20-alpine |
WORKDIR | Set working directory | WORKDIR /app |
COPY | Copy files | COPY . . |
RUN | Execute command at build time | RUN npm install |
CMD | Default command at container start | CMD ["npm", "start"] |
ENTRYPOINT | Container entry point | ENTRYPOINT ["node"] |
ENV | Set environment variable | ENV NODE_ENV=production |
EXPOSE | Expose port (documentation) | EXPOSE 3000 |
ARG | Build-time argument | ARG VERSION=1.0 |
Difference Between CMD and ENTRYPOINT
# CMD: Overridable default command
CMD ["npm", "start"]
# docker run myapp npm run dev ← Gets overridden
# ENTRYPOINT: Always executed command
ENTRYPOINT ["node"]
CMD ["index.js"]
# docker run myapp app.js ← Executes as node app.js
Official Documentation: Dockerfile reference
Step 4: Building and Running the Image
# Build image
docker build -t my-node-app .
# Check built image
docker images
# Start container
docker run -p 3000:3000 my-node-app
# Start in background
docker run -d -p 3000:3000 --name my-app my-node-app
# Access http://localhost:3000 in browser
Common docker run Options
docker run \
-d # Detached mode (background)
-p 3000:3000 # Port mapping (host:container)
--name my-app # Container name
-e NODE_ENV=production # Environment variable
-v $(pwd):/app # Volume mount
--rm # Auto-remove on exit
my-node-app # Image name
Step 5: Building Environment with docker-compose
Manage multiple services (app + database) at once.
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:password@db:5432/mydb
volumes:
- ./src:/app/src # For hot reload
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:
docker-compose.yml Structure
version: '3.8' # Compose specification version
services: # Service (container) definitions
service_name:
image: xxx # or build: ./path
ports: # Port mapping
environment: # Environment variables
volumes: # Volumes
depends_on: # Dependencies
restart: # Restart policy
volumes: # Named volumes
networks: # Custom networks
docker-compose Commands
# Start all services
docker-compose up
# Start in background
docker-compose up -d
# Rebuild and start
docker-compose up --build
# Check logs
docker-compose logs -f app
# Start specific service only
docker-compose up app
# Stop services
docker-compose down
# Remove including volumes
docker-compose down -v
# Check service status
docker-compose ps
Hot Reload: Mounting source code with
volumesreflects file changes immediately in the container.
Official Documentation: Docker Compose overview
Dockerfile Best Practices
1. Multi-Stage Build
Create lightweight images for production:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
2. Using .dockerignore
Exclude unnecessary files from build context:
.dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
*.md
.DS_Store
coverage
.nyc_output
3. Running as Non-Root User
Avoid root user for security:
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
WORKDIR /app
COPY . .
# Switch to non-root user
USER nodejs
CMD ["node", "index.js"]
4. Adding Health Checks
HEALTHCHECK \
CMD curl -f http://localhost:3000/health || exit 1
5. The 12 Factor App Principles
The Twelve-Factor App are design principles for cloud-native applications:
- Config: Manage configuration via environment variables
- Dependencies: Declare explicitly (package.json)
- Port: Expose services via port binding
- Processes: Execute as stateless processes
- Logs: Output as streams to stdout
Frequently Used Docker Commands
Container Operations
# Check running containers
docker ps
# Check all containers (including stopped)
docker ps -a
# Execute command in container
docker exec -it container_name sh
# Check container logs
docker logs container_name
docker logs -f container_name # Follow
# Stop container
docker stop container_name
# Remove container
docker rm container_name
# Remove all stopped containers
docker container prune
Image Operations
# List images
docker images
# Remove image
docker rmi image_name
# Remove unused images
docker image prune
# Check image history
docker history image_name
Cleanup
# Remove unused resources collectively
docker system prune
# Remove including volumes (caution!)
docker system prune -a --volumes
# Check disk usage
docker system df
Troubleshooting
Container Won’t Start
# Check logs
docker logs container_name
# Start in interactive mode for debugging
docker run -it my-node-app sh
Port Already in Use
# Check port in use
lsof -i :3000
# Map to different port
docker run -p 3001:3000 my-node-app
Slow Build
- Check
.dockerignore - Optimize layer order
- Use multi-stage builds
Large Image Size
# Check image size
docker images
# Use lightweight base image
FROM node:20-alpine # ~100MB (vs ~900MB for regular)
# Remove unnecessary files
RUN npm ci --only=production && npm cache clean --force
Next Steps
Once you’ve mastered Docker basics, learn about orchestration:
- Kubernetes Introduction → Build a Kubernetes Cluster
- CI/CD Pipeline → Build CI/CD with GitHub Actions
Reference Links
Official Documentation
- Docker Official Documentation - Official reference
- Dockerfile reference - Dockerfile instruction list
- Docker Compose file reference - Compose specification
- Docker Hub - Official image registry
Best Practices
- Dockerfile best practices - Docker official
- The Twelve-Factor App - Cloud-native app design principles
- Docker Security Best Practices - OWASP
Tools & Resources
- Docker Desktop - GUI management tool
- Dive - Docker image layer analysis tool
- Hadolint - Dockerfile static analysis tool
Cheat Sheets
- Docker Cheat Sheet - Docker official cheat sheet
- Docker Compose Cheat Sheet - Common commands list