🐳

Docker Toàn tập

Từ kiến trúc OS-level đến production deployment – mọi thứ bạn cần biết về container

Container Runtime
OS · Volume · Network
Dockerfile Best Practices
Docker Compose
Image Optimization
Multi-stage Build
01 · Nền tảng

Kiến trúc Docker

Docker là nền tảng đóng gói và chạy ứng dụng trong container – môi trường cô lập, nhẹ, chạy trên cùng OS kernel của host nhưng tách biệt hoàn toàn về process, network, filesystem.

🖥 Docker Client docker build docker run docker pull REST API ⚙ Docker Daemon (dockerd) 📦 Container A nginx:latest port 80→8080 SG: bridge 📦 Container B postgres:16 port 5432 volume: /data 🖼 Local Images Cache nginx · postgres · node · python · ... 📁 Volumes /var/lib/docker 🌐 Networks bridge/host/overlay containerd → runc → OCI Runtime Linux Kernel Namespaces + Cgroups pull/push 🏛 Registry Docker Hub ECR / GCR Harbor GHCR self-hosted Host OS · Linux Kernel

So sánh VM vs Container

🖥 Virtual Machine
  • Bao gồm toàn bộ Guest OS (hàng GB)
  • Hypervisor (VMware, VirtualBox, KVM) quản lý
  • Boot time: 30–60 giây
  • Cô lập hoàn toàn – kernel riêng
  • Overhead lớn: CPU, RAM, disk
  • Phù hợp: workload cần cô lập tuyệt đối
🐳 Docker Container
  • Chia sẻ kernel của Host OS
  • Docker Engine (containerd + runc) quản lý
  • Start time: milliseconds
  • Cô lập qua Namespace + Cgroups
  • Overhead nhỏ – chỉ chạy process cần thiết
  • Phù hợp: microservices, CI/CD, scale nhanh
💡 Core Concept
Container không phải là VM nhỏ. Container là một process được cô lập trên host, dùng chung kernel Linux. Điều này giải thích tại sao container nhẹ và khởi động nhanh đến vậy.
02 · Hệ điều hành

Docker & OS / Kernel

Container cô lập nhờ 3 tính năng cốt lõi của Linux kernel. Hiểu điều này giúp bạn debug sự cố và thiết kế container đúng cách.

Linux Namespaces – Cô lập tài nguyên

NamespaceCô lậpÝ nghĩa thực tế
PIDProcess IDsContainer thấy PID của mình bắt đầu từ 1. Host thấy PID khác (VD: 12345). Container không thấy process của host.
NETNetwork interfacesContainer có network stack riêng: eth0, lo, routing table, iptables riêng. Mặc định không thấy mạng host.
MNTMount pointsContainer có filesystem tree riêng. Không thấy disk của host trừ khi mount volume.
UTSHostnameContainer có hostname riêng (thường là container ID). Không ảnh hưởng hostname host.
IPCInter-Process CommunicationShared memory, semaphore, message queue cô lập giữa các container.
USERUser/Group IDsUID 0 trong container có thể map sang UID khác trên host (user namespace remapping).
CGROUPCgroup hierarchyContainer có cgroup namespace riêng để quản lý resource limits.
TIMESystem clockContainer có thể có system time khác host (Linux 5.6+, ít dùng).

Cgroups (Control Groups) – Giới hạn tài nguyên

Cgroups cho phép Docker giới hạn và theo dõi resource usage của từng container:

bash · Resource Limits
# Giới hạn CPU và RAM khi chạy container
docker run \
  --cpus="2.0"          # Tối đa 2 CPU cores
  --cpu-shares=512      # Relative weight (default 1024)
  --memory="512m"        # RAM tối đa 512MB
  --memory-swap="1g"     # RAM + Swap tối đa 1GB
  --memory-reservation="256m"  # Soft limit
  --pids-limit=200       # Tối đa 200 processes
  --blkio-weight=500     # I/O weight (100-1000)
  --device-read-bps /dev/sda:10mb  # Giới hạn read speed
  nginx

# Xem resource usage thời gian thực
docker stats

# Xem cgroup của container trên host
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes

Overlay Filesystem – Image Layers

Docker dùng OverlayFS (hoặc overlay2 driver) để xếp chồng các read-only image layers và một writable layer trên cùng:

✏️
Container Layer (R/W)
Mỗi container có 1 writable layer riêng. Xóa container → mất layer này. Dùng Volume để persist data.
📦
App Layer (RO)
COPY source code, npm install, pip install... Thay đổi code → rebuild layer này.
⚙️
Config Layer (RO)
ENV, EXPOSE, config files. Thay đổi ít khi nào.
📚
Dependencies Layer (RO)
apt install, yum install... Thay đổi khi cần package mới.
🐧
Base Image Layer (RO)
ubuntu:22.04, alpine:3.19, node:20-slim... Hiếm khi thay đổi → cache lâu dài.
⚠️ Copy-on-Write (CoW)
Khi container sửa một file từ layer bên dưới, Docker copy file đó lên writable layer rồi mới sửa. File gốc trong image layer vẫn nguyên vẹn. Đây gọi là Copy-on-Write – giải thích tại sao nhiều container cùng dùng 1 image mà không conflict.

Windows Container

Trên Windows, Docker Desktop chạy một Linux VM nhẹ (WSL2 hoặc Hyper-V) để host Docker daemon. Container Linux vẫn chạy trên Linux kernel trong VM đó – không phải trực tiếp trên Windows kernel.

Chế độBackendPerformanceKhuyến nghị
WSL2 BackendWindows Subsystem for Linux 2⭐⭐⭐⭐⭐ Tốt nhấtDùng cho dev, gần giống Linux native
Hyper-V BackendHyper-V VM⭐⭐⭐Legacy, ít dùng hơn
Windows ContainerWindows kernel trực tiếp⭐⭐⭐⭐Chỉ cho .NET Framework apps cũ
03 · Image

Image & Layer System

Docker Image là template read-only để tạo container. Image gồm nhiều layers được xếp chồng, mỗi layer tương ứng với một instruction trong Dockerfile.

Anatomy của một Image

bash · Inspect Image
# Xem các layers của image
docker history node:20-alpine
IMAGE          CREATED        CREATED BY                    SIZE
abc123def456   2 days ago     /bin/sh -c #(nop) CMD ["node"]  0B
...            2 days ago     /bin/sh -c apk add --no-cache   45.2MB
...            2 days ago     /bin/sh -c #(nop) ENV NODE_VER  0B

# Xem chi tiết manifest của image
docker inspect node:20-alpine

# Xem size từng layer
docker image inspect node:20-alpine \
  --format='{{range .RootFS.Layers}}{{println .}}{{end}}'

# Export image thành tar
docker save nginx:latest | gzip > nginx.tar.gz

# Import image từ tar
docker load < nginx.tar.gz

# Xem image manifest (multi-arch)
docker manifest inspect node:20-alpine

Image Tags & Naming Convention

Format: registry/namespace/name:tag@digest
# Format đầy đủ
registry/namespace/name:tag@sha256:abc...

# Ví dụ thực tế
nginx:latest                          # Docker Hub, official image
nginx:1.25.3-alpine                   # Specific version + variant
mycompany/myapp:v2.1.0               # Custom namespace
ghcr.io/org/repo:main-abc1234        # GitHub Container Registry
123456789.dkr.ecr.ap-southeast-1.amazonaws.com/myapp:prod  # AWS ECR
localhost:5000/myapp:dev             # Local registry

# Immutable reference bằng digest (KHÔNG dùng :latest trong production)
nginx@sha256:a2b3c4d5e6f7...           # 100% reproducible
🚨 Không dùng :latest trong Production
Tag :latest thay đổi khi image được update → deployment không reproducible, có thể bị breaking changes. Production luôn dùng version cụ thể (VD: nginx:1.25.3-alpine) hoặc digest.

Base Image – Chọn đúng cho từng use case

Base ImageSizeDùng khiLưu ý
scratch0 MBGo binary, static binaryKhông có shell, không có libc – chỉ copy binary vào
alpine:3.x~7 MBProduction nhỏ gọn nhấtDùng musl libc – một số C libs không tương thích
distroless~20 MBSecurity tối đa (Google)Không có shell, không có package manager
debian:slim~80 MBKhi cần glibc, Python, RubyĐủ package, nhỏ hơn debian full
ubuntu:22.04~77 MBDev env, cần apt package đầy đủKhông nên dùng cho production image
node:20-alpine~180 MBNode.js apps productionAlpine-based, nhỏ hơn node:20 (~1GB)
node:20-slim~240 MBNode.js cần glibcDebian slim, an toàn hơn Alpine với native modules
python:3.12-slim~130 MBPython productionSlim Debian, đủ để pip install hầu hết packages
04 · Vòng đời

Container Lifecycle

created 🟢 running PID1 đang chạy paused SIGSTOP 🔴 stopped exit code 0/1 deleted rm / prune start pause unpause stop/kill restart rm

Restart Policies

PolicyHành viKhi nào dùng
noKhông tự restart (mặc định)Dev/test, one-shot jobs
on-failure[:max]Restart khi exit code ≠ 0, giới hạn N lầnBatch jobs có thể retry
alwaysLuôn restart, kể cả sau docker stop rồi daemon restartServices production
unless-stoppedRestart trừ khi tự tay stopServices cần control thủ công
bash · Container Management
# Chạy container với đầy đủ options
docker run \
  --name myapp \                    # Đặt tên
  --restart unless-stopped \        # Restart policy
  -d \                                # Detached mode
  -p 8080:80 \                       # host:container port
  -e NODE_ENV=production \           # Environment variable
  --env-file .env \                  # Env từ file
  -v /data/myapp:/app/data \        # Bind mount
  --network mynet \                  # Network
  --health-cmd="curl -f http://localhost/health" \
  --health-interval=30s \
  --health-retries=3 \
  --read-only \                       # Filesystem read-only
  --security-opt=no-new-privileges \# Security hardening
  --user 1000:1000 \                 # Non-root user
  myimage:v1.2.3

# Exec vào container đang chạy
docker exec -it myapp /bin/sh

# Copy file vào/ra container
docker cp myapp:/app/logs/error.log ./error.log
docker cp ./config.yaml myapp:/app/config.yaml

# Xem logs
docker logs -f --tail=100 myapp

# Healthcheck status
docker inspect --format='{{.State.Health.Status}}' myapp
05 · Lưu trữ

Volume & Storage

Data trong writable layer của container mất khi container bị xóa. Docker cung cấp 3 cơ chế để persist data ngoài container.

📦 Container /app/data /tmp /logs Writable Layer (ephemeral) 📁 Named Volume /var/lib/docker/volumes/ myapp-data/_data 🔗 Bind Mount Host path → Container path /home/user/code → /app ⚡ tmpfs Mount RAM only – không persist Cực nhanh, secrets temp -v myapp-data:/app/data -v /host/path:/app --tmpfs /tmp

So sánh 3 loại Storage

LoạiPersistPerformanceDùng choChia sẻ
Named Volume Khuyến nghị ✅ CóTốt (native) Database data, app data production Nhiều container
Bind Mount ✅ CóTốt (native trên Linux) Dev: live reload source code Nhiều container
tmpfs ❌ Không⚡ Cực nhanh (RAM) Session cache, temp files, secrets Không chia sẻ
Writable Layer ❌ KhôngChậm (CoW overhead) Không nên dùng cho data quan trọng Không

Volume Commands & Best Practices

bash · Volume Management
# Tạo named volume
docker volume create postgres-data
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.1,rw \
  --opt device=:/path/on/nfs \
  nfs-vol

# Mount vào container
docker run -v postgres-data:/var/lib/postgresql/data postgres:16

# Bind mount – dev workflow (source code)
docker run -v $(pwd):/app -v /app/node_modules node:20-alpine
# Lưu ý: -v /app/node_modules để container giữ node_modules riêng
# không bị override bởi host (tránh platform mismatch)

# Read-only bind mount
docker run -v $(pwd)/config:/app/config:ro myapp

# tmpfs cho sensitive data
docker run --tmpfs /run/secrets:rw,size=64m,mode=0755 myapp

# Backup volume
docker run --rm \
  -v postgres-data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/postgres-backup-$(date +%Y%m%d).tar.gz -C /source .

# Inspect volume
docker volume inspect postgres-data

# Xóa volumes không dùng
docker volume prune
docker volume rm postgres-data
✅ Best Practice – Named Volume vs Bind Mount
Production: Dùng Named Volume – Docker quản lý hoàn toàn, dễ backup, portable giữa environments.
Development: Dùng Bind Mount – source code trực tiếp, hot reload hoạt động tốt.
Không bao giờ store database data trong writable layer của container.
06 · Mạng

Docker Networking

Docker tạo ra virtual network interfaces (veth pairs) và dùng iptables/nftables để route traffic. Mỗi network mode hoạt động theo cơ chế khác nhau.

5 Network Drivers

DriverMô tảDùng khiƯu/Nhược
bridge Default Virtual switch layer 2. Containers cùng bridge network liên lạc được với nhau. NAT ra ngoài. Single host, container-to-container ✅ Cô lập tốt ❌ Không scale multi-host
host Container dùng network stack của host trực tiếp. Không có NAT, không có veth. Performance cực cao, monitoring agent ✅ Zero overhead ❌ Không cô lập network
overlay Mạng ảo across nhiều Docker hosts. Dùng VXLAN tunnel. Cần Docker Swarm hoặc Kubernetes. Multi-host cluster, Docker Swarm ✅ Multi-host ❌ Phức tạp, overhead
macvlan Container có MAC address riêng, xuất hiện như device vật lý trên mạng. Cần promiscuous mode. Legacy apps cần IP trên LAN, monitoring ✅ Trực tiếp trên LAN ❌ Phức tạp setup
none Vô hiệu hóa hoàn toàn networking. Chỉ có loopback (127.0.0.1). Batch processing không cần network, security ✅ Cô lập tuyệt đối ❌ Không ra ngoài được

Bridge Network – Hoạt động thế nào?

🌐 Internet Host eth0: 192.168.1.100 iptables MASQUERADE (NAT) docker0 bridge / custom bridge: 172.17.0.1 veth pairs ↔ container eth0 Container A 172.17.0.2 eth0 ↔ veth0 port 80 → host:8080 Container B 172.17.0.3 eth0 ↔ veth1 DNS: container-b → 172.17.0.3 Container C KHÁC network ⛔ Không thấy A,B network isolation

Network Commands

bash · Network Management
# Tạo custom bridge network
docker network create \
  --driver bridge \
  --subnet 172.20.0.0/16 \
  --ip-range 172.20.100.0/24 \
  --gateway 172.20.0.1 \
  myapp-net

# Kết nối container vào network
docker network connect myapp-net container-name
docker network disconnect myapp-net container-name

# Liệt kê và inspect network
docker network ls
docker network inspect myapp-net

# Container-to-container DNS (cùng custom network)
# Container A có thể ping Container B bằng tên!
docker run --name db --network myapp-net postgres
docker run --name api --network myapp-net -e DB_HOST=db myapi
# api container có thể connect đến host 'db', port 5432

# DNS không hoạt động trên default bridge!
# Phải dùng --link (deprecated) hoặc tạo custom network

# Host network (performance max)
docker run --network host nginx
# Container dùng port 80 trực tiếp trên host, không NAT

# Kiểm tra connectivity từ bên trong container
docker exec api ping db
docker exec api curl http://db:5432
⚠️ Default Bridge vs Custom Bridge
Default bridge (docker0): Containers KHÔNG resolve được nhau bằng tên. Phải dùng IP hoặc --link (deprecated).
Custom bridge network: Tự động có embedded DNS server – containers resolve nhau bằng container name. Luôn dùng custom network!
07 · Docker Compose – Giao tiếp

Compose – Container giao tiếp

Docker Compose tự động tạo một custom bridge network cho mỗi project. Tất cả services trong file compose có thể gọi nhau bằng service name như DNS.

yaml · docker-compose.yml – Giao tiếp nội bộ
version: '3.9'

services:
  # ── FRONTEND ────────────────────────────────
  frontend:
    image: myapp-frontend:latest
    ports:
      - "3000:3000"          # host:container – expose ra ngoài
    environment:
      # Gọi backend bằng service name 'backend'
      - API_URL=http://backend:8000
    networks:
      - frontend-net         # Chỉ ở frontend-net
    depends_on:
      backend:
        condition: service_healthy

  # ── BACKEND API ─────────────────────────────
  backend:
    build: ./backend
    # KHÔNG expose ports ra ngoài – chỉ giao tiếp nội bộ
    expose:
      - "8000"               # Chỉ cho containers khác biết port
    environment:
      # Gọi postgres bằng service name 'postgres'
      - DATABASE_URL=postgresql://user:pass@postgres:5432/mydb
      # Gọi redis bằng service name 'redis'
      - REDIS_URL=redis://redis:6379
    networks:
      - frontend-net         # Frontend gọi được
      - backend-net          # Thấy DB và Redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # ── DATABASE ────────────────────────────────
  postgres:
    image: postgres:16-alpine
    # KHÔNG expose ra ngoài – chỉ backend mới gọi được
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    networks:
      - backend-net          # Frontend KHÔNG thấy DB!

  # ── REDIS ───────────────────────────────────
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass myredispass
    networks:
      - backend-net

  # ── NGINX REVERSE PROXY ─────────────────────
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    networks:
      - frontend-net
    depends_on:
      - frontend
      - backend

networks:
  frontend-net:   # nginx ↔ frontend ↔ backend
    driver: bridge
  backend-net:    # backend ↔ postgres ↔ redis (cô lập)
    driver: bridge
    internal: true  # Không có route ra Internet!

volumes:
  postgres-data:   # Named volume – persist sau khi compose down

Giao tiếp ra ngoài – Port Mapping

Cú phápÝ nghĩaKhi nào dùng
"8080:80"host:8080 → container:80, bind 0.0.0.0Public service, tất cả interface
"127.0.0.1:8080:80"Chỉ localhost:8080, không expose publicDev local, bảo mật hơn
"80"Random host port → container:80Scale nhiều instance
expose: ["8000"]Chỉ thông báo cho containers khác, không bind hostInternal service
🌐 Internet / User nginx :80/:443 public port exposed frontend-net frontend :3000 (internal) backend :8000 (internal) backend-net (internal:true → no internet) postgres:5432 redis:6379 ⛔ blocked
08 · Dockerfile

Dockerfile – Mọi Instruction

InstructionTạo Layer mới?Mô tảVí dụ
FROMBase image. Phải là instruction đầu tiên.FROM node:20-alpine AS builder
RUNChạy lệnh shell trong quá trình build. Tạo layer mới mỗi lần.RUN apt-get update && apt-get install -y curl
COPYCopy file từ build context vào image. Preferred hơn ADD.COPY package*.json ./
ADDNhư COPY nhưng hỗ trợ URL và auto-extract .tar. Tránh dùng.ADD https://example.com/file.tar.gz /
WORKDIRĐặt working directory. Tạo dir nếu chưa có.WORKDIR /app
ENVSet environment variable, available cả lúc build và runtime.ENV NODE_ENV=production PORT=3000
ARGBuild-time variable. Không available ở runtime.ARG VERSION=1.0.0
EXPOSEDocumentation – khai báo port container lắng nghe. Không thực sự mở port.EXPOSE 3000
CMDDefault command khi container start. Có thể override bằng docker run ... <cmd>.CMD ["node", "server.js"]
ENTRYPOINTCommand không thể override (chỉ override bằng --entrypoint). CMD trở thành args.ENTRYPOINT ["./entrypoint.sh"]
VOLUMEKhai báo mount point. Docker tự tạo anonymous volume nếu không mount.VOLUME ["/data", "/logs"]
USERSwitch user cho RUN/CMD/ENTRYPOINT tiếp theo.USER node
LABELMetadata key-value cho image.LABEL version="1.0" maintainer="team"
HEALTHCHECKLệnh kiểm tra container health.HEALTHCHECK CMD curl -f http://localhost/
SHELLOverride default shell (default: /bin/sh -c).SHELL ["/bin/bash", "-c"]
STOPSIGNALSignal gửi khi container stop.STOPSIGNAL SIGTERM
ONBUILDTrigger thực thi khi image này được dùng làm base.ONBUILD COPY . /app

CMD vs ENTRYPOINT – Hiểu rõ sự khác biệt

CMD – Default command có thể override
  • Nếu docker run img echo hello → CMD bị replace bởi echo hello
  • Nếu có cả ENTRYPOINT và CMD → CMD là arguments cho ENTRYPOINT
  • Chỉ CMD cuối cùng trong Dockerfile có hiệu lực
  • Dùng khi muốn cho user tùy chỉnh command
ENTRYPOINT – Fixed executable
  • Không bị override bởi arguments sau image name
  • Override bằng --entrypoint /bin/sh
  • Dùng khi container là một "executable tool"
  • Best practice: ENTRYPOINT + CMD kết hợp
Dockerfile · ENTRYPOINT + CMD pattern
# Pattern phổ biến nhất cho production
ENTRYPOINT ["./entrypoint.sh"]   # Script xử lý init, migrations, etc.
CMD ["node", "server.js"]         # Default command, có thể override

# docker run myapp                  → ./entrypoint.sh node server.js
# docker run myapp node worker.js   → ./entrypoint.sh node worker.js
# docker run myapp /bin/sh          → ./entrypoint.sh /bin/sh

# entrypoint.sh ví dụ
#!/bin/sh
set -e
# Chạy database migration trước
if [ "$RUN_MIGRATIONS" = "true" ]; then
  node migrate.js
fi
# Exec command truyền vào (replace process – PID 1)
exec "$@"
09 · Cache Strategy

Thứ tự viết Dockerfile

Docker build cache hoạt động từ trên xuống – khi một layer thay đổi, tất cả layers phía dưới bị invalidate. Sắp xếp đúng thứ tự giúp rebuild nhanh hơn nhiều lần.

🔑 Nguyên tắc vàng
Thứ tự = Tần suất thay đổi tăng dần: Thứ gì ít thay đổi → đặt trên cùng. Thứ gì thay đổi thường xuyên → đặt dưới cùng.
1
FROM – Base image (thay đổi rất hiếm)
Dùng version cụ thể, không dùng :latest. Chọn alpine hoặc slim để nhỏ hơn.
2
ARG và ENV build-time (thay đổi rất hiếm)
ARG trước, ENV sau. ARG trước FROM chỉ available trong FROM. Sau FROM mới dùng được.
3
LABEL, SHELL, STOPSIGNAL (metadata, thay đổi rất hiếm)
Metadata về image – không tạo layer weight đáng kể.
4
RUN – System packages (thay đổi khi cần tool mới)
Cài OS packages. Gộp tất cả vào 1 RUN và clean cache trong cùng layer để tránh tốn dung lượng.
5
WORKDIR – Working directory
Đặt sớm để các instruction sau dùng relative path. Tạo dir nếu chưa có.
6
COPY dependency files – TRƯỚC KHI copy source code!
Đây là kỹ thuật cache quan trọng nhất. Copy package.json, requirements.txt, go.mod trước, rồi mới install.
7
RUN – Install dependencies (thay đổi khi update packages)
npm install, pip install, go mod download. Layer này được cache nếu dependency files không thay đổi → tiết kiệm 80% build time.
8
COPY – Source code (thay đổi thường xuyên nhất)
Copy phần còn lại của source code. Do thay đổi thường xuyên, đặt sau cùng để các layer trên vẫn được cache.
9
RUN – Build (compile, transpile)
npm run build, go build, mvn package... Chỉ invalidate khi source code thay đổi.
10
USER – Switch to non-root
Luôn switch sang non-root user trước CMD. Tạo user sớm hơn (bước 4) nhưng chỉ USER switch ở đây.
11
EXPOSE, HEALTHCHECK – Documentation và monitoring
Khai báo port và cách kiểm tra health. Không ảnh hưởng cache.
12
ENTRYPOINT và CMD – Runtime command
Luôn cuối cùng. Dùng JSON array form (exec form) thay vì shell form để PID1 là process thật.

Ví dụ thực tế – Node.js App đúng thứ tự

Dockerfile · Node.js Production (thứ tự chuẩn)
# ── 1. BASE IMAGE ──────────────────────────────────────
FROM node:20-alpine AS production

# ── 2. METADATA ────────────────────────────────────────
LABEL maintainer="team@company.com" version="1.0"

# ── 3. BUILD ARGS (trước ENV) ──────────────────────────
ARG NODE_ENV=production

# ── 4. ENV VARIABLES ───────────────────────────────────
ENV NODE_ENV=${NODE_ENV} \
    PORT=3000 \
    # Tắt npm update check
    NPM_CONFIG_UPDATE_NOTIFIER=false

# ── 5. SYSTEM PACKAGES (gộp 1 RUN, clean trong cùng layer) ─
RUN apk add --no-cache \
      curl \
      tini \                   # Init process (PID 1)
    && # Tạo non-root user
    addgroup -S appgroup && adduser -S appuser -G appgroup

# ── 6. WORKDIR ─────────────────────────────────────────
WORKDIR /app

# ── 7. COPY DEPENDENCY FILES TRƯỚC! ────────────────────
# KHÔNG copy toàn bộ source code – chỉ copy package files
COPY package.json package-lock.json ./

# ── 8. INSTALL DEPENDENCIES (được cache nếu package.json không đổi) ─
RUN npm ci --only=production \   # npm ci nhanh hơn npm install
    && npm cache clean --force    # Clean cache để giảm size

# ── 9. COPY SOURCE CODE (thay đổi thường xuyên → để cuối) ─
COPY --chown=appuser:appgroup . .
# --chown để file thuộc non-root user ngay từ đầu

# ── 10. SWITCH TO NON-ROOT USER ────────────────────────
USER appuser

# ── 11. EXPOSE & HEALTHCHECK ───────────────────────────
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

# ── 12. ENTRYPOINT & CMD ───────────────────────────────
# dùng tini làm PID1 để handle signals đúng
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
10 · Image Optimization

Đóng gói tiết kiệm nhất

1. Chọn Base Image nhỏ nhất có thể

ImageCompressed SizeGhi chú
node:20~380 MBDebian full – quá lớn cho production
node:20-slim~240 MBDebian slim – tốt, glibc-compatible
node:20-alpine~180 MBAlpine – nhỏ nhất, dùng musl
node:20-alpine + multi-stage~50-80 MBChỉ giữ lại production artifacts
distroless/nodejs20~180 MBKhông có shell – an toàn hơn

2. Gộp RUN commands – Tránh layer thừa

❌ SAI – Mỗi RUN là một layer
RUN apt-get update          # Layer 1: 20MB
RUN apt-get install -y curl  # Layer 2: 8MB
RUN apt-get install -y git   # Layer 3: 30MB
RUN rm -rf /var/lib/apt/lists # Layer 4: -0MB!
# Tổng: 58MB vì các layers trước đã committed
✅ ĐÚNG – Gộp vào 1 RUN + clean cùng layer
RUN apt-get update \
    && apt-get install -y \
       curl \
       git \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean
# Tổng: 1 layer, clean ngay trong cùng layer

3. .dockerignore – Giảm build context

.dockerignore
# Dependency directories
node_modules/
vendor/
.venv/
__pycache__/
*.pyc

# Version control
.git/
.gitignore
.gitattributes

# IDE & OS files
.idea/
.vscode/
.DS_Store
Thumbs.db
*.swp

# Docker files (không cần COPY vào image)
Dockerfile
Dockerfile.*
docker-compose*.yml
.dockerignore

# Test & documentation
*.test.js
*.spec.ts
**/__tests__/
coverage/
.nyc_output/
docs/
README.md
CHANGELOG.md

# Build artifacts (nếu dùng multi-stage thì không cần)
dist/
build/
*.log
.env
.env.local
.env.*.local

# CI/CD
.github/
.gitlab-ci.yml
.travis.yml

4. Xóa cache và temp files trong cùng RUN layer

Dockerfile · Clean techniques
# Node.js – Clean npm cache
RUN npm ci --only=production && npm cache clean --force

# Python – Không cache pip, no .pyc files
RUN pip install --no-cache-dir -r requirements.txt
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

# Alpine – Clean apk cache
RUN apk add --no-cache curl git

# Debian/Ubuntu – Clean apt cache
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# Go – Không cần cache (binary tự đủ)
RUN go build -ldflags="-w -s" -o /app/server .
# -w: strip debug info, -s: strip symbol table

5. Squash layers (BuildKit)

bash · BuildKit optimization
# Enable BuildKit (mặc định từ Docker 23+)
export DOCKER_BUILDKIT=1

# Build với cache mount – pip/npm cache được tái dùng giữa các builds
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# Python với pip cache
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Build secret (không để trong layer) – credentials, SSH keys
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install

docker build --secret id=npmrc,src=.npmrc .

# SSH agent forwarding cho private repos
RUN --mount=type=ssh \
    git clone git@github.com:private/repo.git

docker build --ssh default .
11 · Multi-stage Build

Multi-stage Build

Multi-stage build cho phép dùng nhiều FROM trong một Dockerfile. Giai đoạn build dùng full toolchain; giai đoạn final chỉ copy artifacts cần thiết → image production cực kỳ nhỏ.

🔨 Stage: builder FROM node:20 AS builder node:20 base (~1.1GB) npm install (dev+prod deps) npm run build → /app/dist Total: ~1.5GB (stays local only) COPY --from=builder 🚀 Stage: production FROM node:20-alpine node:20-alpine base (~180MB) COPY /app/dist ← từ builder npm ci --only=production Total: ~80MB ✅ Deploy này!

Ví dụ đầy đủ – Node.js Multi-stage

Dockerfile · Node.js Multi-stage Production
# ════════════════════════════════════════════
# STAGE 1: Dependencies (cache layer riêng)
# ════════════════════════════════════════════
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --frozen-lockfile

# ════════════════════════════════════════════
# STAGE 2: Builder (compile/transpile)
# ════════════════════════════════════════════
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build   # Output: /app/dist

# ════════════════════════════════════════════
# STAGE 3: Production (chỉ copy artifacts)
# ════════════════════════════════════════════
FROM node:20-alpine AS production

RUN apk add --no-cache tini \
    && addgroup -S app && adduser -S app -G app

WORKDIR /app
ENV NODE_ENV=production

# Chỉ copy những gì cần cho production
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=deps --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app package.json ./

USER app
EXPOSE 3000
HEALTHCHECK --interval=30s CMD curl -f http://localhost:3000/health
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]

# Build chỉ target stage production
# docker build --target production -t myapp:latest .

Go Binary – Siêu nhỏ với scratch

Dockerfile · Go – From ~1GB to ~15MB
# STAGE 1: Build Go binary
FROM golang:1.22-alpine AS builder
WORKDIR /app

# Tải dependencies trước (cache layer)
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build \
      -ldflags="-w -s -extldflags '-static'" \  # Strip debug, static binary
      -o /app/server ./cmd/server

# STAGE 2: Scratch – image rỗng hoàn toàn
FROM scratch

# Cần copy CA certs để HTTPS calls hoạt động
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy binary duy nhất
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]
# Final image: ~15MB! (Go binary ~14MB + CA certs ~1MB)

Python Multi-stage

Dockerfile · Python FastAPI
# STAGE 1: Build dependencies với wheels
FROM python:3.12-slim AS builder
WORKDIR /app

RUN pip install --upgrade pip

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# STAGE 2: Production
FROM python:3.12-slim AS production

RUN useradd --create-home --shell /bin/bash app

WORKDIR /app

# Copy chỉ installed packages
COPY --from=builder /root/.local /home/app/.local
COPY --chown=app:app . .

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PATH=/home/app/.local/bin:$PATH

USER app
EXPOSE 8000
HEALTHCHECK CMD curl -f http://localhost:8000/health
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
12 · Orchestration

Docker Compose – Đầy đủ

Docker Compose là công cụ định nghĩa và chạy multi-container applications. File docker-compose.yml là single source of truth cho toàn bộ stack.

docker-compose.yml · Production-ready Full Stack
version: '3.9'

x-logging: &default-logging        # YAML anchor – tái dùng config
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

x-common: &common
  restart: unless-stopped
  logging: *default-logging

services:

  # ── NGINX REVERSE PROXY ──────────────────────────
  nginx:
    <<: *common
    image: nginx:1.25-alpine
    container_name: prod-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-certs:/etc/letsencrypt:ro
      - nginx-logs:/var/log/nginx
    networks:
      - public-net
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 30s

  # ── BACKEND API ──────────────────────────────────
  backend:
    <<: *common
    build:
      context: ./backend
      dockerfile: Dockerfile
      target: production         # Multi-stage target
      args:
        - NODE_ENV=production
      cache_from:
        - myregistry/backend:cache  # Kéo cache từ registry
    image: myregistry/backend:${TAG:-latest}  # ENV từ .env file
    container_name: prod-backend
    expose:
      - "3000"                    # Không expose ra host
    env_file:
      - .env.production          # Load từ file
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
      - REDIS_URL=redis://:${REDIS_PASS}@redis:6379
    volumes:
      - app-uploads:/app/uploads  # Uploads persist
    networks:
      - public-net               # Nginx có thể gọi
      - private-net              # Có thể gọi DB, Redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 40s        # Đợi app khởi động
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

  # ── POSTGRES DATABASE ────────────────────────────
  postgres:
    <<: *common
    image: postgres:16-alpine
    container_name: prod-postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
      - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    env_file: .env.production
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASS}
      - POSTGRES_DB=${DB_NAME}
      - PGDATA=/var/lib/postgresql/data/pgdata
    networks:
      - private-net              # Chỉ backend thấy
    shm_size: '256mb'           # Shared memory cho Postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ── REDIS CACHE ──────────────────────────────────
  redis:
    <<: *common
    image: redis:7-alpine
    container_name: prod-redis
    command: >
      redis-server
        --requirepass ${REDIS_PASS}
        --maxmemory 256mb
        --maxmemory-policy allkeys-lru
        --save 900 1
        --save 300 10
    volumes:
      - redis-data:/data
    networks:
      - private-net
    healthcheck:
      test: ["CMD", "redis-cli", "--pass", "${REDIS_PASS}", "ping"]
      interval: 10s

  # ── WORKER (background jobs) ─────────────────────
  worker:
    <<: *common
    build:
      context: ./backend
      target: production
    command: ["node", "worker.js"]  # Override CMD
    env_file: .env.production
    networks:
      - private-net
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

networks:
  public-net:
    driver: bridge
  private-net:
    driver: bridge
    internal: true           # Không có Internet access!

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local
  app-uploads:
    driver: local
  nginx-logs:
    driver: local
  certbot-certs:
    driver: local

Compose Commands quan trọng

bash · Docker Compose CLI
# Khởi động stack
docker compose up -d                    # Detached
docker compose up -d --build            # Rebuild images trước
docker compose up -d --force-recreate   # Recreate containers

# Scale service
docker compose up -d --scale worker=3  # Chạy 3 worker containers

# Xem logs
docker compose logs -f backend          # Follow logs của 1 service
docker compose logs -f --tail=100       # 100 dòng cuối, tất cả services

# Exec vào container
docker compose exec backend /bin/sh

# Chạy one-off command
docker compose run --rm backend npm run migrate

# Stop và xóa
docker compose down                     # Stop + remove containers
docker compose down -v                  # + Xóa volumes (NGUY HIỂM!)
docker compose down --rmi all           # + Xóa images

# Config validation và xem merged config
docker compose config

# Override file (dev vs prod)
# docker-compose.yml + docker-compose.override.yml (auto merge)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Pull images mới nhất
docker compose pull

# Xem resource usage
docker compose top
docker stats $(docker compose ps -q)
13 · Bảo mật

Container Security

Security Checklist

Hạng mụcCách thực hiệnTại sao?
Non-root user Critical USER 1000:1000 hoặc tạo user trong Dockerfile Root trong container có thể escape ra host nếu có vulnerability
Read-only filesystem --read-only + tmpfs cho /tmp Ngăn malware ghi file, tăng attack surface
No new privileges --security-opt=no-new-privileges Ngăn process trong container escalate privilege qua setuid binaries
Drop capabilities --cap-drop=ALL --cap-add=NET_BIND_SERVICE Linux capabilities mặc định quá nhiều – chỉ giữ cái cần thiết
Secrets management Docker Secrets, HashiCorp Vault, env từ secret manager Không để secrets trong ENV, Dockerfile, hay image layers
Image scanning Trivy, Snyk, Docker Scout, Grype Phát hiện CVE trong base image và dependencies
Minimal base image distroless, alpine, scratch Fewer packages = smaller attack surface
Immutable tags Dùng digest @sha256:... thay :latest Đảm bảo deploy đúng image đã được review
bash · Security hardening
# Chạy container với đầy đủ security options
docker run \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --security-opt=no-new-privileges:true \
  --security-opt=seccomp=./seccomp-profile.json \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --user 1000:1000 \
  --pids-limit=100 \
  --memory=512m \
  --cpus=1.0 \
  --network=myapp-net \
  myapp:latest

# Scan image với Trivy
trivy image --severity HIGH,CRITICAL myapp:latest

# Scan với Docker Scout
docker scout cves myapp:latest

# Xem capabilities của container đang chạy
docker inspect --format='{{.HostConfig.CapAdd}}' mycontainer
docker inspect --format='{{.HostConfig.CapDrop}}' mycontainer
14 · Debug

Debug & Troubleshoot

Vấn đềLệnh kiểm traFix
Container exit ngay docker logs <id>, docker inspect Xem exit code, log error. PID1 exit → container stop.
Không connect được port docker port <id>, docker inspect -f Networks Kiểm tra port mapping, network, SG nếu trên cloud.
Container-to-container không thấy nhau docker exec <id> ping <name>, docker network inspect Phải cùng custom network. Default bridge không có DNS.
Image build chậm docker build --progress=plain Sắp xếp layer order, dùng .dockerignore, BuildKit cache mount.
Disk đầy docker system df docker system prune -af --volumes (cẩn thận!)
OOM Killed docker inspect | grep OOMKilled Tăng memory limit hoặc optimize app.
Permission denied Check user trong container vs file ownership Dùng --chown trong COPY hoặc chown trong RUN.
bash · Debug toolkit
# Exec vào container đang chạy
docker exec -it mycontainer /bin/sh    # Alpine/busybox
docker exec -it mycontainer /bin/bash   # Debian/Ubuntu

# Kiểm tra container distroless (không có shell)
# Dùng ephemeral debug container
docker debug mycontainer               # Docker Desktop 4.27+
# Hoặc
kubectl debug -it mypod --image=busybox --target=mycontainer

# Xem process tree
docker exec mycontainer ps aux
docker top mycontainer

# Xem environment variables
docker exec mycontainer env

# Test network từ trong container
docker run --rm --network myapp-net nicolaka/netshoot \
  curl -v http://backend:3000/health

# Kiểm tra DNS resolution
docker run --rm --network myapp-net nicolaka/netshoot \
  nslookup backend

# Xem layer diff của container
docker diff mycontainer

# Xem events Docker
docker events --filter container=mycontainer

# Xuất container thành image để debug
docker commit mycontainer debug-image
docker run -it --entrypoint /bin/sh debug-image
15 · Reference

CLI Cheat Sheet

bash · Docker CLI – Tất cả lệnh thường dùng
## ── IMAGE ──────────────────────────────────────────────────
docker pull nginx:alpine                    # Kéo image
docker push myregistry/myapp:v1.0           # Đẩy image
docker build -t myapp:latest .              # Build image
docker build --no-cache -t myapp:latest .   # Build không dùng cache
docker build --target production .          # Multi-stage target
docker images / docker image ls              # Liệt kê images
docker image rm nginx / docker rmi nginx     # Xóa image
docker image prune -a                        # Xóa images không dùng
docker tag myapp:latest myapp:v1.0.0       # Gán tag mới
docker history myapp:latest                 # Xem layers
docker save myapp | gzip > myapp.tar.gz      # Export image
docker load < myapp.tar.gz                   # Import image

## ── CONTAINER ──────────────────────────────────────────────
docker run -d -p 8080:80 --name web nginx   # Chạy container
docker ps                                    # Container đang chạy
docker ps -a                                 # Tất cả containers
docker start/stop/restart/kill web           # Quản lý state
docker rm web                                # Xóa container stopped
docker rm -f web                             # Force xóa (kể cả running)
docker container prune                       # Xóa stopped containers
docker exec -it web /bin/sh                  # Exec vào container
docker logs -f --tail=100 web                # Xem logs
docker inspect web                           # Chi tiết container
docker stats                                 # Resource usage realtime
docker cp web:/app/file ./file               # Copy file
docker diff web                              # Thay đổi filesystem
docker port web                              # Port mappings
docker top web                               # Processes trong container
docker pause/unpause web                     # Pause/resume
docker wait web                              # Đợi container exit
docker kill --signal SIGUSR1 web             # Gửi signal tùy chỉnh
docker update --memory 512m web             # Update resource limits live

## ── NETWORK ────────────────────────────────────────────────
docker network ls                            # Liệt kê networks
docker network create mynet                  # Tạo network
docker network inspect mynet                 # Chi tiết network
docker network connect mynet web             # Connect container vào network
docker network disconnect mynet web          # Disconnect
docker network rm mynet                      # Xóa network
docker network prune                         # Xóa unused networks

## ── VOLUME ─────────────────────────────────────────────────
docker volume ls                             # Liệt kê volumes
docker volume create mydata                  # Tạo volume
docker volume inspect mydata                 # Chi tiết volume
docker volume rm mydata                      # Xóa volume
docker volume prune                          # Xóa unused volumes

## ── SYSTEM ─────────────────────────────────────────────────
docker system df                             # Disk usage
docker system prune                          # Dọn dẹp (containers+images+networks)
docker system prune -af --volumes            # Dọn dẹp toàn bộ (CẨN THẬN!)
docker system events                         # Xem events realtime
docker system info                           # Thông tin Docker daemon
docker version                               # Version client + server
docker login myregistry.example.com          # Login registry
docker logout                                # Logout

## ── COMPOSE ────────────────────────────────────────────────
docker compose up -d --build                 # Start + build
docker compose down -v                       # Stop + remove volumes
docker compose ps                            # Status services
docker compose logs -f backend               # Logs một service
docker compose exec backend bash             # Exec vào service
docker compose run --rm backend migrate      # Run one-off command
docker compose pull                          # Pull images mới nhất
docker compose build --no-cache              # Rebuild images
docker compose config                        # Validate + xem merged config
docker compose restart backend               # Restart một service
docker compose scale worker=3               # Scale (compose v1)
docker compose up -d --scale worker=3       # Scale (compose v2)
docker compose top                           # Processes trong services
docker compose events                        # Events từ services
✅ Quick Summary – Những gì cần nhớ nhất
Build nhỏ: Multi-stage + Alpine/slim + .dockerignore + gộp RUN + clean cache
Cache nhanh: Copy dependency files TRƯỚC source code
An toàn: Non-root user + read-only + no-new-privileges + scan image
Network: Dùng custom bridge, service name làm DNS hostname
Data: Named volume cho production, bind mount cho dev
Signals: Dùng exec form CMD/ENTRYPOINT + tini để handle PID1 đúng