Part 11 · Docker · 11.3

8 lab Docker và Nginx

Bộ lab này chuyển kiến thức runtime, build, security, storage, network, Compose và reverse proxy thành evidence có thể kiểm tra. Với mỗi lab, lưu image digest, command đã chạy, thời gian, log trước/sau failure và bằng chứng recovery.

Nguyên tắc làm lab: không chỉ chứng minh happy path. Mỗi bài phải chủ động tạo lỗi, quan sát đúng signal, giải thích root cause và xác nhận recovery. Không dùng chmod 777, không nhúng secret vào image, và không coi “container đang Up” là đủ để kết luận service khỏe.

Lab 01 · Engine lifecycle

Mục tiêu: nối image → container → process trên host → namespace/cgroup, đồng thời quan sát create/start/stop/restart/remove.

Prerequisites

Steps

  1. Pull image và lưu digest bằng docker image inspect; kiểm tra RepoDigests, config và layer metadata.
  2. Tạo container nhưng chưa start; so sánh docker psdocker ps -a.
  3. Start container, lấy container PID bằng docker inspect -f '{{.State.Pid}}', rồi đối chiếu PID đó với process trên host.
  4. Inspect namespace/cgroup của process bằng công cụ host phù hợp như ps, lsns, /proc/<pid>/ns/proc/<pid>/cgroup.
  5. Stop, start, restart rồi remove container. Ghi lại container ID, PID cũ/mới và state transition.
docker pull nginx:alpine
docker image inspect nginx:alpine
docker create --name lab-engine -p 8080:80 nginx:alpine
docker start lab-engine
docker inspect -f '{{.State.Pid}} {{.State.Status}}' lab-engine
docker stop lab-engine
docker start lab-engine
docker restart lab-engine
docker rm -f lab-engine

Failure injection

Start một container với published port đã bị chiếm hoặc command cố tình exit non-zero. Phân biệt lỗi create/start với lỗi application runtime.

Verification & expected evidence

Production note: restart policy không thay thế health check hay root-cause analysis. Một process crash-loop có thể được tự khởi động lại nhưng vẫn liên tục không phục vụ traffic.

Lab 02 · BuildKit cache

Mục tiêu: Java multi-stage build; đo cold/warm cache khi đổi source, POM và base image; dùng cache mount, .dockerignore, secret mount và xem multi-platform metadata.

Prerequisites

Steps

  1. Viết Dockerfile multi-stage: stage Maven build artifact, stage runtime chỉ chứa JRE + JAR cần thiết.
  2. Tách copy pom.xml khỏi source để dependency-resolution layer có thể reuse.
  3. Thêm cache mount cho Maven repository và .dockerignore để loại .git, target, log và file tạm.
  4. Đo ít nhất bốn lần: cold build; warm build không đổi; đổi một file source; đổi pom.xml. Ghi layer nào cache hit/miss và thời gian.
  5. Thử build secret bằng RUN --mount=type=secret; xác nhận secret không xuất hiện trong final image/history.
  6. Dùng docker buildx imagetools inspect hoặc metadata tương đương để quan sát platform manifest nếu build multi-platform.
# syntax=docker/dockerfile:1
FROM maven:3-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests package

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/target/*.jar app.jar
USER 10001:10001
ENTRYPOINT ["java","-jar","/app/app.jar"]

Failure injection

Đặt COPY . . quá sớm rồi thay một file không liên quan; quan sát cache bị invalidate rộng. Sau đó sửa Dockerfile để dependency layer ổn định hơn và đo lại.

Verification & expected evidence

Trade-off: cache hiệu quả phụ thuộc thứ tự layer và build context. Cache mount giúp package manager reuse download, nhưng cache là optimization; build vẫn phải đúng khi chạy cold.

Lab 03 · Non-root hardening

Mục tiêu: chạy với fixed UID, read-only rootfs, tmpfs, drop capabilities, pids/memory/CPU limits; tạo permission failure và sửa đúng ownership thay vì mở quyền quá mức.

Prerequisites

Steps

  1. Tạo user/group cố định, ví dụ UID/GID 10001; copy artifact với ownership phù hợp và đặt USER 10001:10001.
  2. Run với --read-only; mount tmpfs chỉ cho path thực sự cần ghi.
  3. Drop capabilities mặc định bằng --cap-drop=ALL, chỉ add capability nếu có evidence bắt buộc.
  4. Áp dụng --pids-limit, memory và CPU limits; quan sát metrics/stats khi load.
  5. Xuất SBOM/scan bằng công cụ có sẵn trong môi trường và lưu report.
docker run --rm --name lab-sec \
  --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop=ALL --pids-limit=128 --memory=256m --cpus=1 \
  -p 8080:8080 myapp:lab

Failure injection

Cho app ghi vào thư mục rootfs không writable hoặc mount volume có owner sai UID. Quan sát Permission denied. Sửa bằng ownership/UID/mount path đúng; không dùng chmod 777.

Verification & expected evidence

Failure window: limit quá thấp có thể biến overload thành OOM kill hoặc thread/process starvation. Theo dõi OOM events, restart count, CPU throttling, pids usage và latency trước khi chọn production limit.

Lab 04 · Storage recovery

Mục tiêu: phân biệt writable layer, bind mount, named volume, tmpfs; chứng minh persistence và backup/restore application-consistent.

Steps

  1. Ghi cùng một file test vào bốn kiểu storage: writable layer, bind mount, named volume và tmpfs.
  2. Remove/recreate container; ghi lại dữ liệu nào còn, dữ liệu nào mất và ownership trên host/volume.
  3. Với một database hoặc app state nhỏ, tạo backup khi ứng dụng ở trạng thái consistent: stop/quiesce hoặc dùng cơ chế snapshot/backup chính thức của ứng dụng.
  4. Xóa/recreate volume test, restore backup, khởi động app và kiểm tra checksum/record count.
docker volume create lab-data
docker run --rm -v lab-data:/data alpine sh -c 'date > /data/evidence.txt'
docker run --rm -v lab-data:/data alpine cat /data/evidence.txt

Failure injection

Thực hiện backup trong lúc workload đang ghi liên tục rồi so sánh với backup đã quiesce. Nếu app có nhiều file/state, ghi nhận nguy cơ crash-consistent nhưng không application-consistent.

Verification & expected evidence

Recovery rule: backup chưa được coi là hoàn thành cho tới khi restore được kiểm thử. Capacity production cần theo dõi volume usage, inode usage, write latency và tốc độ restore/catch-up.

Lab 05 · Network forensic

Mục tiêu: hai service trên user-defined bridge; tái hiện localhost, wrong bind, DNS, published port và firewall issue; debug từ đúng network namespace.

Steps

  1. Tạo user-defined bridge và chạy api + client cùng network. Dùng service/container name để gọi qua DNS nội bộ.
  2. Inspect network, IP, aliases và published ports. So sánh container port với host published port.
  3. Kiểm tra listener trong container bằng ss -lntp hoặc tool tương đương; xác nhận app bind 0.0.0.0 khi cần nhận kết nối từ container khác.
  4. Thực hiện debug theo tầng: DNS → route/network membership → listener/bind address → application response → published port/firewall từ host.
docker network create lab-net
docker run -d --name api --network lab-net my-api:lab
docker run --rm --network lab-net curlimages/curl:latest http://api:8080/health
docker network inspect lab-net

Failure injection

Verification & expected evidence

Production note: debug từ host có thể cho kết quả khác pod/container namespace. Luôn xác định source và destination của flow trước khi kết luận firewall, DNS hay application lỗi.

Lab 06 · Compose full stack

Mục tiêu: Spring Boot + PostgreSQL + Redis với health, profiles, networks, named volumes, không nhúng secret và startup retry có giới hạn.

Prerequisites

Steps

  1. Tạo Compose file gồm app, db, redis; chỉ publish port thật sự cần cho host.
  2. Đặt DB/Redis trên internal network, app trên network phù hợp; dùng named volume cho PostgreSQL.
  3. Thêm healthcheck thực sự phản ánh readiness dependency. Nếu dùng depends_on condition, vẫn giữ retry/backoff trong app vì runtime failure có thể xảy ra sau startup.
  4. Tách profile cho debug/admin tooling; không bật mặc định production-like path.
  5. Cấp secret qua environment/file secret của môi trường lab, không bake vào image hoặc commit plaintext.
services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks: [appnet]
  db:
    image: postgres:17
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 12
    volumes: [pgdata:/var/lib/postgresql/data]
    networks: [appnet]
volumes:
  pgdata:
networks:
  appnet:

Failure injection

Stop PostgreSQL sau khi stack đã healthy; quan sát app behavior. Khởi động lại DB và xác nhận connection pool/retry phục hồi mà không cần rebuild image. Sau đó thử sai credential và phân biệt authentication failure với readiness delay.

Verification & expected evidence

Operational signals: startup duration, health transitions, restart count, DB connection saturation, Redis errors, disk usage và request error rate. Recovery phải được kiểm tra cả khi dependency fail sau startup.

Lab 07 · PID 1/SIGTERM

Mục tiêu: so shell/exec ENTRYPOINT, child zombie, shutdown hook, stop timeout và exit 137/143; đo drain request đang xử lý.

Steps

  1. Tạo hai image/container: shell form và exec form ENTRYPOINT/CMD. Inspect process tree để xem process nào là PID 1.
  2. Thêm shutdown hook trong app để log thời điểm nhận SIGTERM và hoàn thành graceful shutdown.
  3. Gửi request chậm/in-flight, sau đó chạy docker stop; đo thời gian từ SIGTERM đến app exit và request có hoàn tất hay bị cắt.
  4. Thử stop timeout ngắn hơn thời gian drain; quan sát escalation sang SIGKILL và exit code.
  5. Nếu app spawn child process, tạo case child exit và kiểm tra zombie/reaping. Dùng init nhỏ khi workload thực sự cần reaping/forward signal.
docker stop --time 20 lab-app
docker inspect -f '{{.State.ExitCode}} {{.State.FinishedAt}}' lab-app

Failure injection

Dùng shell wrapper không exec tiến trình Java/Nginx rồi stop container. Quan sát signal có tới đúng process hay không. Sau đó sửa wrapper dùng exec "$@" hoặc exec-form ENTRYPOINT.

Verification & expected evidence

Failure window: orchestrator/host chỉ chờ tới grace period. Nếu app drain lâu hơn, SIGKILL có thể cắt transaction/request. Grace period phải lớn hơn shutdown path hợp lý nhưng không quá dài tới mức kéo chậm rollout/recovery.

Lab 08 · React/Nginx incident

Mục tiêu: multi-stage static image, non-root Nginx, SPA cache policy và reverse proxy; drill 403/404/502/504, header/TLS scheme và access/error logs.

Steps

  1. Build React bằng Node stage, copy artifact tĩnh sang Nginx runtime stage; không mang node_modules vào final image.
  2. Chạy Nginx non-root trên unprivileged port; đảm bảo cache/static/temp paths có quyền phù hợp.
  3. Cấu hình SPA fallback: route client-side hợp lệ trả index.html, nhưng asset bị thiếu vẫn phải có tín hiệu 404 phù hợp thay vì che mọi lỗi.
  4. Thiết lập cache policy: asset fingerprinted có cache dài/immutable; HTML shell cache ngắn hoặc revalidate để tránh giữ release cũ.
  5. Reverse proxy /api/ tới backend; log upstream status, request time và upstream response time.
location / {
  try_files $uri $uri/ /index.html;
}
location /api/ {
  proxy_pass http://api:8080/;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_read_timeout 30s;
}

Incident drills

SymptomFailure injectionEvidence cần thuRecovery
403Sai permission/owner static file hoặc directory traversal permissionNginx error log + ls -lnSửa ownership/mode tối thiểu cần thiết
404Asset path sai hoặc thiếu SPA fallbackAccess log URI/status + filesystem pathSửa build path/location/try_files
502Backend down, wrong host/port, connection refusedError log upstream connect + backend healthKhôi phục backend/config DNS/port
504Backend chậm hơn proxy timeoutrequest_time, upstream_response_timeFix latency/root cause; chỉ tăng timeout khi có SLO rõ
Wrong scheme/headerThiếu hoặc sai forwarded headers sau TLS terminationRequest headers + app generated redirectsChuẩn hóa trusted proxy/header chain

Verification & expected evidence

Capacity signals: active connections, request rate, 4xx/5xx, upstream latency, timeout count và container memory/CPU. Tăng timeout không giải quyết backend saturation; nó có thể giữ connection lâu hơn và làm failure lan rộng.

Rubric

MứcTiêu chíEvidence tối thiểu
1Images/Compose chạy happy path.Build/run commands, health/output cơ bản, image digest.
2Repeatable, non-root, persistent và graceful.Recreate/restore, fixed UID, resource/storage config, SIGTERM drain và failure/recovery logs.
3 · SeniorGiải thích runtime internals, security và failure evidence.Namespace/cgroup/process reasoning; cache/security trade-off; forensic signal; measured RPO/RTO/timeout/capacity; rollback được kiểm thử.
Definition of done: mỗi lab có command/config có thể chạy lại, failure được chủ động tạo, evidence trước/sau fix, và kết luận nêu rõ điều gì được chứng minh — không suy rộng quá phạm vi lab.

Tài liệu tham khảo

Docker Docs · Optimize cache usage in builds
Docker Docs · Build secrets
Docker Docs · Building best practices
Docker Docs · Dockerfile reference
Docker Docs · Restart policies
Docker Docs · Volumes
Docker Docs · Bridge network driver
Docker Docs · Compose startup order
NGINX · HTTP proxy module
NGINX · Error log