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.
So sánh VM vs Container
- 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
- 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
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
| Namespace | Cô lập | Ý nghĩa thực tế |
|---|---|---|
| PID | Process IDs | Container 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. |
| NET | Network interfaces | Container có network stack riêng: eth0, lo, routing table, iptables riêng. Mặc định không thấy mạng host. |
| MNT | Mount points | Container có filesystem tree riêng. Không thấy disk của host trừ khi mount volume. |
| UTS | Hostname | Container có hostname riêng (thường là container ID). Không ảnh hưởng hostname host. |
| IPC | Inter-Process Communication | Shared memory, semaphore, message queue cô lập giữa các container. |
| USER | User/Group IDs | UID 0 trong container có thể map sang UID khác trên host (user namespace remapping). |
| CGROUP | Cgroup hierarchy | Container có cgroup namespace riêng để quản lý resource limits. |
| TIME | System clock | Container 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:
# 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:
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ế độ | Backend | Performance | Khuyến nghị |
|---|---|---|---|
| WSL2 Backend | Windows Subsystem for Linux 2 | ⭐⭐⭐⭐⭐ Tốt nhất | Dùng cho dev, gần giống Linux native |
| Hyper-V Backend | Hyper-V VM | ⭐⭐⭐ | Legacy, ít dùng hơn |
| Windows Container | Windows kernel trực tiếp | ⭐⭐⭐⭐ | Chỉ cho .NET Framework apps cũ |
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
# 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 đầ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
: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 Image | Size | Dùng khi | Lưu ý |
|---|---|---|---|
| scratch | 0 MB | Go binary, static binary | Không có shell, không có libc – chỉ copy binary vào |
| alpine:3.x | ~7 MB | Production nhỏ gọn nhất | Dùng musl libc – một số C libs không tương thích |
| distroless | ~20 MB | Security tối đa (Google) | Không có shell, không có package manager |
| debian:slim | ~80 MB | Khi cần glibc, Python, Ruby | Đủ package, nhỏ hơn debian full |
| ubuntu:22.04 | ~77 MB | Dev env, cần apt package đầy đủ | Không nên dùng cho production image |
| node:20-alpine | ~180 MB | Node.js apps production | Alpine-based, nhỏ hơn node:20 (~1GB) |
| node:20-slim | ~240 MB | Node.js cần glibc | Debian slim, an toàn hơn Alpine với native modules |
| python:3.12-slim | ~130 MB | Python production | Slim Debian, đủ để pip install hầu hết packages |
Container Lifecycle
Restart Policies
| Policy | Hành vi | Khi nào dùng |
|---|---|---|
no | Khô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ần | Batch jobs có thể retry |
always | Luôn restart, kể cả sau docker stop rồi daemon restart | Services production |
unless-stopped | Restart trừ khi tự tay stop | Services cần control thủ công |
# 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
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.
So sánh 3 loại Storage
| Loại | Persist | Performance | Dùng cho | Chia 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ông | Chậm (CoW overhead) | Không nên dùng cho data quan trọng | Không |
Volume Commands & Best Practices
# 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
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.
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
| Driver | Mô 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?
Network Commands
# 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
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!
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.
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ĩa | Khi nào dùng |
|---|---|---|
"8080:80" | host:8080 → container:80, bind 0.0.0.0 | Public service, tất cả interface |
"127.0.0.1:8080:80" | Chỉ localhost:8080, không expose public | Dev local, bảo mật hơn |
"80" | Random host port → container:80 | Scale nhiều instance |
expose: ["8000"] | Chỉ thông báo cho containers khác, không bind host | Internal service |
Dockerfile – Mọi Instruction
| Instruction | Tạo Layer mới? | Mô tả | Ví dụ |
|---|---|---|---|
| FROM | ✅ | Base image. Phải là instruction đầu tiên. | FROM node:20-alpine AS builder |
| RUN | ✅ | Chạ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 |
| COPY | ✅ | Copy file từ build context vào image. Preferred hơn ADD. | COPY package*.json ./ |
| ADD | ✅ | Như 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 |
| ENV | ❌ | Set environment variable, available cả lúc build và runtime. | ENV NODE_ENV=production PORT=3000 |
| ARG | ❌ | Build-time variable. Không available ở runtime. | ARG VERSION=1.0.0 |
| EXPOSE | ❌ | Documentation – khai báo port container lắng nghe. Không thực sự mở port. | EXPOSE 3000 |
| CMD | ❌ | Default command khi container start. Có thể override bằng docker run ... <cmd>. | CMD ["node", "server.js"] |
| ENTRYPOINT | ❌ | Command không thể override (chỉ override bằng --entrypoint). CMD trở thành args. | ENTRYPOINT ["./entrypoint.sh"] |
| VOLUME | ❌ | Khai báo mount point. Docker tự tạo anonymous volume nếu không mount. | VOLUME ["/data", "/logs"] |
| USER | ❌ | Switch user cho RUN/CMD/ENTRYPOINT tiếp theo. | USER node |
| LABEL | ❌ | Metadata key-value cho image. | LABEL version="1.0" maintainer="team" |
| HEALTHCHECK | ❌ | Lệnh kiểm tra container health. | HEALTHCHECK CMD curl -f http://localhost/ |
| SHELL | ❌ | Override default shell (default: /bin/sh -c). | SHELL ["/bin/bash", "-c"] |
| STOPSIGNAL | ❌ | Signal gửi khi container stop. | STOPSIGNAL SIGTERM |
| ONBUILD | ❌ | Trigger 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
- Nếu
docker run img echo hello→ CMD bị replace bởiecho 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
- 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
# 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 "$@"
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.
:latest. Chọn alpine hoặc slim để nhỏ hơn.package.json, requirements.txt, go.mod trước, rồi mới install.Ví dụ thực tế – Node.js App đúng thứ tự
# ── 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"]
Đóng gói tiết kiệm nhất
1. Chọn Base Image nhỏ nhất có thể
| Image | Compressed Size | Ghi chú |
|---|---|---|
node:20 | ~380 MB | Debian full – quá lớn cho production |
node:20-slim | ~240 MB | Debian slim – tốt, glibc-compatible |
node:20-alpine | ~180 MB | Alpine – nhỏ nhất, dùng musl |
node:20-alpine + multi-stage | ~50-80 MB | Chỉ giữ lại production artifacts |
distroless/nodejs20 | ~180 MB | Không có shell – an toàn hơn |
2. Gộp RUN commands – Tránh layer thừa
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
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
# 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
# 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)
# 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 .
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ỏ.
Ví dụ đầy đủ – Node.js Multi-stage
# ════════════════════════════════════════════ # 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
# 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
# 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"]
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.
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
# 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)
Container Security
Security Checklist
| Hạng mục | Cách thực hiện | Tạ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 |
# 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
Debug & Troubleshoot
| Vấn đề | Lệnh kiểm tra | Fix |
|---|---|---|
| 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. |
# 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
CLI Cheat Sheet
## ── 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
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