Chương 05 · Containers

Containers & Docker

Container thay đổi cách triển khai phần mềm. Học từ namespaces/cgroups (cốt lõi Linux), Docker architecture, Dockerfile best practice, multi-stage build, image optimization, networking, volumes, Docker Compose. Đây là nền cho Kubernetes ở chương kế tiếp.

1. Container vs VM — khác biệt cốt lõi

Trước Docker (2013), 2 cách phổ biến deploy app:

  • Bare metal — install app trực tiếp lên server. Conflict dependency cao.
  • VM (Virtual Machine) — VMware/KVM/Hyper-V. Mỗi app 1 VM với full OS. Isolation tốt nhưng nặng (mỗi VM 500MB+ RAM, vài GB disk, boot 30s).

Container giải bài toán: isolation tương tự VM nhưng nhẹ hơn 100×:

┌─────── VM ────────┐ ┌──── Container ──────┐ │ App A │ │ App A App B … │ ├───────────────────┤ ├─────────────────────┤ │ Bin / Libs │ │ Bin / Libs (mỗi) │ ├───────────────────┤ ├─────────────────────┤ │ Guest OS │ │ Container Runtime │ │ (FULL kernel) │ │ (containerd) │ ├───────────────────┤ ├─────────────────────┤ │ Hypervisor │ │ Host OS │ │ (KVM, ESXi) │ │ (kernel SHARED) │ ├───────────────────┤ ├─────────────────────┤ │ Host OS │ │ Hardware │ ├───────────────────┤ └─────────────────────┘ │ Hardware │ └───────────────────┘ 1 VM = 500MB+ 1 container = vài MB-vài chục MB Boot 30s Boot < 1s

1.1. So sánh chi tiết

Tiêu chíVMContainer
KernelRiêng (full OS)Chia sẻ với host
Boot time30s - vài phút< 1s
Sizevài GBvài MB - vài trăm MB
Density10-20 VM/host100-1000+ container/host
IsolationHardware-level (mạnh)OS-level (process)
Image formatVMDK, QCOW2 (vài GB)OCI image (vài MB+)
OS hỗ trợBất kỳ OSCùng kernel với host (Linux container chỉ chạy trên Linux host)

1.2. Khi nào VM tốt hơn?

  • Need full OS isolation (multi-tenant cloud).
  • Cross-OS (Windows app trên Linux host).
  • Hardware passthrough (GPU, custom kernel).

Trong DevOps modern: VM là infrastructure layer; container là application layer. Cloud thường chạy K8s trong VM (EKS node = EC2 instance) — combo tốt nhất.

2. Cách container hoạt động — Linux primitives

Container không phải magic. Nó là 3 Linux features cũ kết hợp:

2.1. Namespaces — isolation

Namespace tạo "view" riêng của system resource cho process:

NamespaceIsolate
PIDProcess IDs (process trong container thấy mình PID 1)
Network (NET)Interface, route, port
Mount (MNT)Filesystem mount points
UTSHostname, domain name
IPCInter-process communication (shared memory, queues)
User (USER)UIDs, GIDs (UID 0 trong container = UID 1000 ngoài)
CgroupCgroup root (Linux 4.6+)

2.2. Cgroups — resource limits

Control Groups (cgroups) giới hạn tài nguyên process:

  • cpu — share CPU time, set quota.
  • memory — limit RAM, kill nếu vượt (OOM kill).
  • blkio — limit disk I/O.
  • pids — limit số process.
  • net_cls — tag traffic cho QoS.

2.3. Capabilities + seccomp — security

Linux capability chia quyền root thành 40+ phần (CAP_NET_ADMIN, CAP_SYS_ADMIN, ...). Container chỉ giữ lại capability cần thiết.

seccomp filter syscall — block syscall nguy hiểm.

2.4. Demo container "thủ công" với unshare

# Bash + unshare = container thô sơ
sudo unshare --pid --net --mount --uts --ipc --fork bash

# Trong shell mới:
hostname container1                    # đổi hostname (UTS namespace)
mount -t proc proc /proc               # remount proc
ps aux                                  # chỉ thấy process trong namespace
ip addr                                 # chỉ thấy lo (network namespace)
exit

Đó là "container" — không cần Docker. Docker tự động hóa: tạo namespace, mount image filesystem, set cgroup, drop capability.

3. Docker Architecture

Docker không phải 1 binary mà là client-server architecture:

┌─────────────────────┐ ┌──────────────────────────────────┐ │ Docker CLI │ │ Docker Daemon (dockerd) │ │ (docker run …) │ ──REST→ │ ├─ image manage │ └─────────────────────┘ │ ├─ container manage │ │ ├─ network manage │ │ └─ volume manage │ │ ↓ │ │ ┌────────────────┐ │ │ │ containerd │ │ │ │ (runtime) │ │ │ │ ↓ │ │ │ │ runc / crun │ │ │ │ (low-level) │ │ │ └────────────────┘ │ │ ↓ │ │ namespaces + cgroups (kernel) │ └──────────────────────────────────┘

3.1. Components

  • Docker CLIdocker command. Gọi REST API.
  • Docker Daemon (dockerd) — service chạy nền, quản lý high-level objects.
  • containerd — high-level runtime (lifecycle container, image transfer).
  • runc — low-level OCI runtime tạo namespace+cgroup. Chuẩn OCI.

Modern alternative: Podman — daemonless (mỗi command spawn runc trực tiếp), rootless mặc định, CLI compat Docker.

3.2. OCI Standard

Open Container Initiative (2015) định nghĩa:

  • Image Spec — format image (layers, manifest).
  • Runtime Spec — cách chạy container.
  • Distribution Spec — cách push/pull registry.

Vì OCI: Docker image chạy được với Podman, BuildKit, K8s (containerd, CRI-O). Không lock-in vendor.

4. Container đầu tiên — lệnh cơ bản

# Pull image từ Docker Hub
docker pull nginx:1.25-alpine

# Run container
docker run --rm -d -p 8080:80 --name web nginx:1.25-alpine
#   --rm: xóa container khi stop
#   -d: detached (background)
#   -p host:container — port mapping
#   --name: tên container

# Verify
curl http://localhost:8080
docker ps                              # container đang chạy

# Logs
docker logs web
docker logs -f web                     # follow

# Exec vào container
docker exec -it web sh
# Trong container:
ps aux
ls /etc/nginx
exit

# Stop & remove
docker stop web
docker rm web                          # nếu không có --rm

# Liệt kê
docker ps                              # running
docker ps -a                           # all (kể cả stopped)
docker images                          # images
docker volume ls
docker network ls

4.1. Run options quan trọng

docker run \
  -d \                                  # background
  --name myapp \                        # tên
  -p 3000:3000 \                        # port
  -e NODE_ENV=production \              # env var
  --env-file .env \                     # env từ file
  -v $(pwd)/data:/app/data \            # bind mount
  --memory=512m \                       # RAM limit
  --cpus=0.5 \                           # 50% 1 core
  --restart=unless-stopped \             # auto restart
  --user 1000:1000 \                    # non-root
  --read-only \                          # filesystem read-only
  --cap-drop=ALL \                       # drop tất cả capability
  --cap-add=NET_BIND_SERVICE \           # chỉ giữ cái cần
  myapp:1.0

4.2. docker prune — dọn dẹp

docker container prune                 # xóa container stopped
docker image prune                     # xóa dangling image
docker image prune -a                  # xóa unused image
docker volume prune                    # xóa volume không reference
docker network prune
docker system prune -a --volumes       # xóa TẤT CẢ unused (nuclear)

Mẹo: kiểm tra docker system df xem disk usage trước khi prune.

5. Dockerfile — Build image declarative

5.1. Hello world

# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

USER node

CMD ["node", "server.js"]
docker build -t myapp:1.0 .
docker run -p 3000:3000 myapp:1.0

5.2. Instruction reference

InstructionMục đích
FROMBase image (bắt buộc, dòng đầu)
WORKDIRSet working directory (cd)
COPY src dstCopy file vào image (preferred)
ADDNhư COPY nhưng auto-extract tar, fetch URL (avoid)
RUN cmdChạy command tại build time
ENV KEY=valueSet env var (cả build + runtime)
ARG nameBuild-time variable (không persist runtime)
EXPOSE portDocument port (chỉ doc, không thực sự open)
VOLUME /pathMount point declaration
USER nameSet user chạy command sau
HEALTHCHECKDefine health check command
ENTRYPOINTExecutable mặc định (khó override)
CMDDefault args (override bằng docker run … cmd)

5.3. Layer system

Mỗi instruction tạo 1 layer (filesystem diff). Image = stack of layers, immutable, cached.

┌──────────────────────────────────┐ ← container (writable) ├──────────────────────────────────┤ │ Layer 5 (CMD ["node", ...]) │ ├──────────────────────────────────┤ │ Layer 4 (COPY . .) │ ├──────────────────────────────────┤ │ Layer 3 (RUN npm ci) │ ├──────────────────────────────────┤ │ Layer 2 (COPY package*.json) │ ├──────────────────────────────────┤ │ Layer 1 (WORKDIR /app) │ ├──────────────────────────────────┤ │ Base layers (node:20-alpine) │ └──────────────────────────────────┘

5.4. Cache mechanism

Khi build lại, Docker check từng instruction:

  • Instruction giống và input giống → cache hit, skip.
  • Bất kỳ thay đổi → invalidate cache từ đó về sau.

Quy tắc vàng: đặt instruction ít thay đổi ở đầu, nhiều thay đổi ở cuối.

# ❌ KÉM — COPY trước npm ci
FROM node:20-alpine
WORKDIR /app
COPY . .                               # mỗi lần code đổi → invalidate
RUN npm ci                             # → rebuild cả npm ci

# ✓ TỐT — separate package.json copy
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./                  # ít thay đổi
RUN npm ci                             # cache khi package.json không đổi
COPY . .                               # nhiều thay đổi, đặt cuối

5.5. .dockerignore

# .dockerignore — file/folder không vào build context
node_modules
.git
.env
*.log
dist
build
coverage
.next
.cache
.vscode
.idea
README.md

Lợi ích: build context nhỏ → upload nhanh, không leak secret (.env), không dirty cache (node_modules thay đổi).

6. Multi-stage Build — image gọn hơn 10×

Vấn đề: build time cần dev tool (compiler, npm devDep) — runtime không cần. Single-stage image chứa cả → lớn + nhiều attack surface.

6.1. Single-stage (BAD)

FROM node:20
WORKDIR /app
COPY . .
RUN npm ci                              # cài cả devDep
RUN npm run build                       # cần TypeScript, webpack, ...
CMD ["npm", "start"]

# Kết quả: ~1.5 GB

6.2. Multi-stage (GOOD)

# Stage 1: build
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci                              # cài tất cả (kể cả devDep)
COPY . .
RUN npm run build                       # tạo dist/

# Stage 2: production runtime
FROM node:20-alpine AS runner

WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --only=production            # chỉ prod dep

COPY --from=builder /app/dist ./dist    # copy artifact từ stage 1

USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

# Kết quả: ~150 MB (10× nhỏ hơn)

6.3. Build target cụ thể

docker build --target builder -t myapp:dev .
docker build --target runner -t myapp:prod .

6.4. Multi-stage cho compiled language

# Go example
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app -ldflags="-s -w" ./cmd/server

# Stage 2: distroless (no shell, no apt)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

# Result: ~20 MB total

Distroless images (Google) chỉ chứa binary + minimal runtime, không có shell/package manager → attack surface cực thấp.

7. Image Optimization — Best Practices

7.1. Chọn base image phù hợp

BaseSizeUse case
scratch0 MBStatic binary (Go, Rust)
distroless/static2 MBStatic binary, có CA cert
distroless/base20 MBDynamic linked binary
alpine:3.195 MBCần shell, apk
debian:slim28 MBCần glibc (Node native module)
ubuntu:22.0478 MBCần apt ecosystem
UBI (RHEL)200 MBEnterprise compliance

7.2. Combine RUN để giảm layer

# ❌ Mỗi RUN = 1 layer
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*

# ✓ Combine + cleanup trong cùng layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl vim && \
    rm -rf /var/lib/apt/lists/* && \
    apt-get clean

Quan trọng: cleanup phải trong cùng RUN, không thì cleanup ở layer sau không xóa được data từ layer trước (immutable).

7.3. BuildKit cache mount

# syntax=docker/dockerfile:1.7

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Mount cache cho npm — không vào image, persistent giữa builds
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline
COPY . .
CMD ["node", "server.js"]
# Enable BuildKit (default Docker 23+)
DOCKER_BUILDKIT=1 docker build -t myapp .

# Hoặc dùng buildx
docker buildx build --cache-from type=registry,ref=myapp:cache \
                    --cache-to   type=registry,ref=myapp:cache,mode=max \
                    -t myapp:1.0 .

7.4. Đo và phân tích image

# Size
docker images myapp

# Layer breakdown
docker history myapp:1.0

# Detail (file system, layer wasted)
dive myapp:1.0                          # https://github.com/wagoodman/dive

7.5. Reproducible build

# ❌ Không reproducible
FROM node:20                            # latest 20 — thay đổi khi rebuild

# ✓ Pin version cụ thể
FROM node:20.11.1-alpine3.19

# ✓ Pin theo digest (mạnh nhất)
FROM node:20-alpine@sha256:5c7bd23a4...

8. Container Registry & Distribution

8.1. Public registries

RegistryURLNotes
Docker Hubdocker.ioDefault, rate limit 100 pull/6h anonymous
GitHub Container Registryghcr.ioFree for public, integrate GHA
AWS ECR Publicpublic.ecr.awsFree, no rate limit
Google Container Registrygcr.io / pkg.devMigrating to Artifact Registry
Quay.ioquay.ioRed Hat owned, security scan

8.2. Private registries

  • AWS ECR — tích hợp IAM, lifecycle policy.
  • GCP Artifact Registry — multi-format (Docker, Maven, npm, Helm).
  • Azure Container Registry (ACR).
  • Harbor — open source, self-host, có vulnerability scan, signing.
  • JFrog Artifactory — enterprise.

8.3. Push & Pull

# Login
docker login                            # Docker Hub
docker login ghcr.io -u USER -p PAT     # GitHub
aws ecr get-login-password | docker login --username AWS \
  --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

# Tag
docker tag myapp:1.0 ghcr.io/me/myapp:1.0

# Push
docker push ghcr.io/me/myapp:1.0

# Pull
docker pull ghcr.io/me/myapp:1.0

# Multi-arch (ARM + x86)
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 \
  -t ghcr.io/me/myapp:1.0 \
  --push .

8.4. Image tagging strategy

TagUse casePin level
latestDemo, dev (avoid prod)None — moves
1, 1.5, 1.5.2Tag SemVerReasonable
main-abc1234CI build (branch + commit)Specific commit
sha256:abc...Production deployImmutable

Best practice prod: pin theo SHA256 digest — không thể bị "moved" như tag.

9. Container Networking

9.1. Network drivers

  • bridge (default) — virtual network, container có IP riêng, NAT ra ngoài.
  • host — share network với host (no isolation).
  • none — không network (chỉ loopback).
  • overlay — multi-host (Swarm, K8s).
  • macvlan — container có MAC address riêng, xuất hiện như device riêng trên LAN.

9.2. Bridge network mặc định

# List network
docker network ls
# bridge, host, none đã có sẵn

# Container trên bridge default — KHÔNG resolve nhau bằng tên
docker run -d --name db postgres
docker run -d --name web --link db nginx     # --link deprecated

# Tốt hơn: tạo user-defined bridge
docker network create mynet
docker run -d --name db --network mynet postgres
docker run -d --name web --network mynet nginx
# Bây giờ web có thể curl http://db:5432 — DNS resolution tự động

9.3. Port mapping

# -p host_port:container_port
docker run -p 8080:80 nginx              # host 8080 → container 80
docker run -p 127.0.0.1:8080:80 nginx    # bind chỉ localhost
docker run -P nginx                       # auto map EXPOSE'd ports random

# Inspect
docker port web                           # xem mapping

9.4. DNS giữa container

Trong user-defined network, container resolve nhau qua tên container hoặc network alias:

docker network create app-net

docker run -d --name api --network app-net myapi
docker run -d --name web --network app-net \
  -e API_URL=http://api:3000 \
  myweb

# Network alias
docker run -d --network app-net --network-alias db postgres
# Container khác có thể nối "db" hoặc "<container-name>"

9.5. Inspect network

docker network inspect mynet           # IP range, container, options
docker exec web ip addr                # interface trong container
docker exec web cat /etc/resolv.conf   # DNS server (Docker daemon)
docker exec web netstat -tlnp          # ports listening

10. Volumes & Data Persistence

10.1. Storage options

TypeLifetimeUse case
Container layer (writable)Container delete = mấtTemp data
Volume (named)Persistent (docker volume)DB data, app state
Bind mountPersistent (host path)Dev: code mount, share file với host
tmpfs (RAM)Container delete = mấtSensitive data, no disk

10.2. Named volume

# Tạo volume
docker volume create pgdata

# Mount volume
docker run -d --name pg \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:15

# Inspect
docker volume inspect pgdata
# Mountpoint: /var/lib/docker/volumes/pgdata/_data

# List
docker volume ls

# Cleanup volume không reference
docker volume prune

10.3. Bind mount

# Bind host path → container path
docker run -d --name dev \
  -v $(pwd):/app \                       # mount code source vào /app
  -v /app/node_modules \                  # anonymous volume (shadow node_modules)
  node:20 npm run dev

Use case: development hot-reload (sửa code local → container thấy ngay).

10.4. tmpfs

docker run --tmpfs /tmp:size=100M nginx
# /tmp ở RAM, max 100MB. Process write nhanh, không bao giờ ra disk.

10.5. Backup volume

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

# Restore
docker run --rm \
  -v pgdata:/data \
  -v $(pwd):/backup \
  alpine \
  sh -c "cd /data && tar xzf /backup/pgdata-20240115.tar.gz"

11. Docker Compose — Multi-container

Docker Compose orchestrate nhiều container = 1 app. File compose.yaml declarative.

11.1. Compose file

# compose.yaml
services:
  web:
    build: .                             # build từ Dockerfile
    image: myapp:dev
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://app:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src                   # bind mount cho hot reload
      - /app/node_modules                # anon volume
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - cachedata:/data

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - web

volumes:
  pgdata:
  cachedata:

networks:
  default:
    name: myapp-net

11.2. Lệnh Compose

# Start all
docker compose up                       # foreground (logs)
docker compose up -d                    # background

# Stop & remove
docker compose down                     # giữ volume
docker compose down -v                  # xóa cả volume

# Logs
docker compose logs                     # all services
docker compose logs -f web              # follow service

# Exec
docker compose exec web sh
docker compose exec db psql -U app myapp

# Scale
docker compose up -d --scale web=3      # 3 instance web

# Build
docker compose build                    # build all
docker compose build web                # 1 service

# Status
docker compose ps                       # service status
docker compose top                      # processes

11.3. Multi-environment với override

# compose.yaml — base
services:
  web:
    image: myapp:1.0
    environment:
      - LOG_LEVEL=info

# compose.dev.yaml — dev override
services:
  web:
    build: .
    volumes:
      - ./src:/app/src
    environment:
      - LOG_LEVEL=debug
      - HOT_RELOAD=true

# compose.prod.yaml — prod
services:
  web:
    image: ghcr.io/me/myapp:1.5.0
    environment:
      - LOG_LEVEL=warn
    deploy:
      replicas: 3
# Dev
docker compose -f compose.yaml -f compose.dev.yaml up

# Prod
docker compose -f compose.yaml -f compose.prod.yaml up -d

Compose phù hợp dev environment, demo, single-host deploy. Production multi-host → Kubernetes.

12. Container Security — 10 nguyên tắc

  1. Run as non-root:
    RUN addgroup -S app && adduser -S app -G app
    USER app
  2. Read-only root filesystem:
    docker run --read-only --tmpfs /tmp myapp
  3. Drop capabilities:
    docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
  4. Limit resources: --memory=512m --cpus=0.5 tránh DoS.
  5. No privileged: tránh --privileged trừ khi tuyệt đối cần.
  6. Don't expose Docker socket: /var/run/docker.sock = root host.
  7. Pin base image theo digest: tag có thể bị move.
  8. Multi-stage build + distroless: minimize attack surface.
  9. Scan image: Trivy, Snyk, Grype mỗi build.
  10. Sign image: cosign — supply chain integrity.

12.1. Trivy scan

# Quick scan
trivy image myapp:1.0

# Severity filter, fail CI nếu HIGH
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.0

# Filesystem scan (Dockerfile)
trivy fs .

# IaC scan
trivy config .                          # scan terraform, k8s YAML

12.2. cosign — sign image

# Generate key pair
cosign generate-key-pair

# Sign image
cosign sign --key cosign.key ghcr.io/me/myapp:1.0

# Verify
cosign verify --key cosign.pub ghcr.io/me/myapp:1.0

Chi tiết hơn ở Chương 12 DevSecOps.

13. Bài tập

  1. First container: pull nginx, run với port 8080. Mount HTML file vào /usr/share/nginx/html. Truy cập trên browser.
  2. Build app: container hóa 1 app Node.js / Python / Go. Dockerfile multi-stage. Đo size, target < 100 MB.
  3. Layer caching: build app 2 lần. Verify cache hit ở COPY package.json. Sau đó sửa code, rebuild — verify chỉ rebuild từ COPY . trở đi.
  4. Distroless: convert Dockerfile sang multi-stage với base distroless/static. Verify image < 30 MB.
  5. Compose stack: viết compose.yaml cho stack 3 services: web (Node) + DB (Postgres) + cache (Redis). Health check, restart policy, named volume.
  6. Multi-env Compose: tạo compose.dev.yaml + compose.prod.yaml override. Dev có hot-reload, prod có replicas=3.
  7. Volume backup: backup Postgres data volume thành tar.gz, restore trên container mới.
  8. Network: tạo 2 network. Place api + db ở "backend", web + nginx ở "frontend". Web nối api qua "frontend" only — verify db không reachable từ web.
  9. Security harden: harden 1 image: non-root user, read-only fs, drop ALL caps, memory limit. Verify app vẫn chạy. Xác minh bằng docker inspect.
  10. Trivy scan: scan image của bạn. Sửa CVE HIGH bằng cách upgrade base image. Re-scan.
  11. Multi-arch: build image cho cả x86 và ARM (M1/M2). Push lên ghcr.io, verify cả 2 platform pull được.
  12. Compare: cùng app, build với (a) Ubuntu base, (b) Alpine base, (c) Distroless. So sánh size, build time, startup time, RAM.

14. Quiz

Quiz cuối Chương 5

Container KHÁC VM ở điểm cốt lõi nào?

  • Container chỉ chạy được Linux
  • VM nhanh hơn
  • Container chia sẻ kernel với host (lightweight); VM có guest OS riêng (full kernel) — nặng hơn nhiều lần
  • Hai cái giống nhau
VM: hypervisor virtualize hardware → guest OS có kernel riêng (vài GB, boot 30s). Container: kernel host shared, dùng namespaces+cgroups isolate process (vài MB, boot < 1s). Hệ quả: 1 host chạy 100-1000 container, chỉ 10-20 VM. Container có isolation yếu hơn VM (chia sẻ kernel) — không phù hợp multi-tenant untrusted.

Container isolation dựa trên Linux primitives nào?

  • Namespaces (isolation), Cgroups (resource limits), Capabilities + seccomp (security)
  • VirtualBox, KVM
  • Hyper-V
  • SELinux only
3 components: (1) Namespaces — PID/NET/MNT/UTS/IPC/USER tạo "view" riêng; (2) Cgroups — limit CPU/RAM/IO; (3) Capabilities (chia quyền root) + seccomp (filter syscall) cho security. Docker, Podman đều dùng cùng primitives — không phải Docker phát minh ra container, chỉ làm UX dễ.

Quy tắc cache Dockerfile để optimize build:

  • Đặt COPY . . ở đầu
  • Đặt instruction ÍT thay đổi ở ĐẦU, NHIỀU thay đổi ở CUỐI — vd COPY package.json + RUN npm ci trước, COPY . . sau
  • Mọi RUN nên trên 1 dòng
  • Không quan trọng
Mỗi instruction tạo layer. Khi rebuild, Docker check input giống → cache hit. Bất kỳ thay đổi → invalidate từ đó về sau. Code thay đổi nhiều hơn package.json → tách ra để cache npm ci. Lợi: rebuild khi sửa code chỉ tốn 5s thay vì 2 phút.

Multi-stage build mang lại lợi ích gì?

  • Build nhanh hơn
  • Tăng cache hit
  • Cho phép code TypeScript
  • Final image chỉ chứa runtime + artifact build, không có compiler/devDep — image nhỏ 10× và attack surface thấp
Single-stage: image cuối có cả Node devDep (TypeScript, webpack, jest) + source code → 1.5GB. Multi-stage: stage build có tool, stage runner chỉ COPY --from=builder dist/ → 150MB. Distroless còn nhỏ hơn nữa (~20MB cho Go). Ngoài ra: ít attack surface, không có shell/apt cho attacker exploit.

Pin image theo gì để reproducible build?

  • latest tag
  • node:20
  • SHA256 digest (vd node:20-alpine@sha256:abc...) — immutable
  • Major version
Tag node:20 hoặc node:20-alpine có thể được maintainer rebuild với patch khác. Build hôm nay khác build tuần sau. Digest SHA256 là content-addressable, immutable. Trade-off: phải update digest thủ công khi muốn nâng version (Dependabot có thể tự động).

Container ở user-defined bridge network có thể resolve nhau bằng:

  • IP address only
  • Tên container (DNS resolution tự động) hoặc network alias
  • Phải config /etc/hosts
  • Phải dùng --link
Default bridge (network "bridge"): không có DNS giữa container, phải dùng --link (deprecated). User-defined bridge (do docker network create): có DNS tự động, container resolve nhau bằng tên. Best practice: luôn tạo user-defined network thay vì dùng default.

Để app trong container persist data sau khi container delete:

  • Mount volume (named volume hoặc bind mount) vào path data
  • Dùng latest tag
  • Set environment variable
  • Restart container
Container layer (writable) bị xóa khi container remove. Volume tách biệt khỏi container lifecycle: docker run -v pgdata:/var/lib/postgresql/data postgres. Named volume managed by Docker; bind mount = path host. Lưu ý: anonymous volume (chỉ -v /path) cũng tạo nhưng tên random, khó manage.

Container security best practice:

  • Run as root để có quyền cao
  • Mount /var/run/docker.sock để container quản lý container khác
  • Run as non-root user, drop capabilities, read-only filesystem, scan vuln, pin digest
  • Disable AppArmor/SELinux
Defense in depth: (1) USER non-root trong Dockerfile, (2) --cap-drop=ALL --cap-add=needed, (3) --read-only --tmpfs /tmp, (4) Trivy/Snyk scan trong CI, (5) pin SHA256, (6) cosign sign. Mount Docker socket = full host root (container có thể spawn privileged container ra escape).

Docker Compose phù hợp khi:

  • Production multi-region multi-host
  • Replace Kubernetes
  • Stateless app horizontal scale
  • Local dev environment, demo, CI integration test, single-host deploy nhỏ
Compose là tool single-host. Production multi-host cần Kubernetes/Nomad/Swarm. Compose vẫn cực hữu ích: dev environment với DB/cache mà không cần install local; integration test trong CI (service container thay thế); demo onboarding nhanh.

Container "scratch" (FROM scratch) phù hợp khi:

  • Cần shell debug
  • Static binary (Go, Rust) — không cần shell, libc, hoặc bất kỳ runtime nào → image cực nhỏ (vài MB), attack surface gần zero
  • Node.js app
  • App có Python script
FROM scratch: image rỗng tuyệt đối, 0 byte. Chỉ chạy được static-linked binary (Go với CGO_ENABLED=0, Rust). Lợi: tiny image, không có CVE từ base. Nhược: không debug được (không shell), không tool. Khuyến cáo dùng distroless/static thay vì scratch — có CA cert, tzdata, etc., vẫn rất nhỏ.

Hoàn thành Chương 5. Tiếp theo: Chương 6 — Kubernetes →