Mười một phần trước của sê-ri này mổ xẻ container Linux từ bên trong, mỗi phần một cơ chế và mỗi kết luận một phép đo thật: namespace cô lập tiến trình và mạng, cgroups giới hạn CPU, bộ nhớ, I/O và số tiến trình, OverlayFS xếp lớp image, capabilities và seccomp siết quyền, file descriptor và RLIMIT chặn rò rỉ, PID 1 xử lý tín hiệu và thu dọn zombie, rồi bộ nhớ với sự khác biệt giữa RSS và page cache. Bài cuối này không đo một cơ chế mới mà làm một việc khác: gom tất cả thành một checklist cấu hình thực chiến, rồi kiểm chứng nó bằng cách áp hết lên một container và đọc lại từng nút điều chỉnh để chắc chắn mọi cờ đều có tác dụng. Mục tiêu là biến hiểu biết về cơ chế thành những quyết định cụ thể bạn áp dụng được ngay cho Dockerfile và lệnh chạy của mình.

Checklist: từ mỗi phép đo thành một quyết định cấu hình

Ảnh chụp đoạn mã nền tối checklist chạy app backend trong container từ 11 phép đo với Docker 29 tổng kết sê-ri Container nội bộ mỗi dòng nối tới một bài đã đo thật, cô lập và tài nguyên đặt trần đừng để max --memory=N --memory-swap=N trần RAM tắt swap OOM rõ ràng tại trần P4 P11 đặt theo đỉnh anon RSS không theo memory.current bị page cache thổi phồng --cpus=N quota CPU qua cpu.max vượt thì bị throttle không bị giết P3 --pids-limit=N chặn fork bomb mặc định max là một vòng lặp fork hạ cả host P5 --ulimit nofile=S:H trần fd phân biệt EMFILE 24 per-process vs ENFILE 23 cả máy P9, quyền mặc định đã siết siết thêm gần như miễn phí --cap-drop ALL rồi --cap-add đúng cái cần root container còn 14 trên 40 cap drop hết CapEff 0 P7 --security-opt no-new-privileges chặn leo quyền qua setuid binary giữ seccomp mặc định đừng unconfined lọc 40-50 syscall nguy hiểm cộng 24ns mỗi syscall P8 --read-only cộng --tmpfs tmp rootfs bất biến chỉ ghi vào volume khai rõ, vòng đời tiến trình PID 1 phải cư xử như init --init hoặc app tự bắt SIGTERM PID1 không bắt SIGTERM docker stop chờ hết giờ rồi SIGKILL P10 --init cho tini thu dọn zombie mồ côi cộng chuyển tín hiệu dùng exec form CMD app shell form sh làm PID1 nuốt tín hiệu app không thấy image gọn qua OverlayFS lớp chia sẻ copy-on-write ghi mới tốn chỗ không sửa lớp dưới P6 mỗi dòng là một quyết định cấu hình rút ra từ một phép đo thật trong sê-ri P là phần không cái nào là lý thuyết suông tất cả đã kiểm bằng proc sys fs cgroup exit code trên Docker 29

Hình 1: Checklist ba nhóm — cô lập và tài nguyên, quyền, vòng đời tiến trình — mỗi dòng rút ra từ một phép đo trong sê-ri.

Checklist chia thành ba nhóm theo đúng mạch của sê-ri. Nhóm thứ nhất là cô lập và tài nguyên, nguyên tắc chung là đặt trần, đừng để "max". Đặt --memory=N cùng --memory-swap=N để có trần RAM và tắt swap, cho OOM xảy ra rõ ràng tại trần như phần 4 và 11 đo; và nhớ bài học phần 11, đặt giới hạn theo đỉnh anon (RSS) chứ không theo memory.current, vì con số đó bị page cache thổi phồng. Đặt --cpus=N để giới hạn CPU qua cpu.max; phần 3 cho thấy vượt trần CPU chỉ bị throttle chậm lại, không bị giết. Đặt --pids-limit=N để chặn fork bomb; phần 5 cho thấy mặc định "max" nghĩa là một vòng lặp fork có thể hạ cả host. Và đặt --ulimit nofile=S:H cho trần file descriptor, với lưu ý phần 9 rằng EMFILE (errno 24) là trần per-process còn ENFILE (errno 23) là trần cả máy, hai lỗi cần sửa ở hai nơi khác nhau.

Nhóm thứ hai là quyền, nơi tin tốt là mặc định đã siết sẵn một phần, siết thêm gần như miễn phí. Dùng --cap-drop ALL rồi --cap-add đúng những capability cần: phần 7 đo root trong container mặc định chỉ có 14 trong hơn 40 capability, và drop hết đưa CapEff về 0. Thêm --security-opt no-new-privileges để chặn leo quyền qua các binary setuid. Giữ nguyên seccomp mặc định và đừng chạy unconfined: phần 8 đo profile mặc định lọc khoảng 40 đến 50 syscall nguy hiểm với chi phí chỉ 24 nano giây mỗi syscall. Và thêm --read-only cùng --tmpfs /tmp để rootfs bất biến, chỉ cho ghi vào những volume khai báo rõ, thu hẹp thiệt hại nếu ứng dụng bị chiếm.

Nhóm thứ ba là vòng đời tiến trình, với nguyên tắc PID 1 phải cư xử như init. Dùng --init, hoặc để ứng dụng tự bắt SIGTERM: phần 10 đo rằng PID 1 không bắt SIGTERM khiến docker stop chờ hết timeout rồi SIGKILL, còn --init cho tini thu dọn zombie mồ côi và chuyển tiếp tín hiệu. Viết CMD dạng exec ["app"] thay vì dạng shell, vì dạng shell khiến sh làm PID 1, nuốt tín hiệu và ứng dụng không bao giờ thấy. Và giữ image gọn nhờ hiểu OverlayFS từ phần 6: các lớp chia sẻ theo cơ chế copy-on-write, ghi mới tốn chỗ riêng nhưng không sửa lớp dưới. Mỗi dòng trong checklist là một quyết định rút ra từ một phép đo thật, không phải lý thuyết suông, tất cả đã kiểm bằng /proc, /sys/fs/cgroup và exit code trên Docker 29.

Áp hết checklist rồi đọc lại từng knob

Ảnh chụp output thật nền tối mặc định so với đóng gói cẩn thận đọc lại từng knob với đọc từ proc cộng sys fs cgroup trong hai container python 3.12 Docker 29, đọc lại từng nút điều chỉnh chứng minh cờ đã ăn knob mặc định không cờ so với đóng gói cẩn thận áp hết checklist PID 1 sh so với docker-init tini no_new_privs 0 so với 1 CapEff a80425fb 14 cap so với 0000000000000000 cap-drop ALL Seccomp 2 lọc so với 2 lọc giữ nguyên memory.max max không trần so với 268435456 256 MB memory.swap.max max so với 0 tắt swap cpu.max max 100000 so với 100000 100000 1 CPU pids.max max fork bomb được so với 100 RLIMIT_NOFILE 1048576 trên 1048576 so với 1024 trên 1024 rootfs ghi được so với read-only touch probe bị chặn, lệnh chạy đã đo mọi giá trị trên là đọc lại từ chính container này docker run --init --read-only --tmpfs tmp --cap-drop ALL --security-opt no-new-privileges --memory=256m --memory-swap=256m --cpus=1.0 --pids-limit=100 --ulimit nofile=1024:1024 app latest nói thẳng mặc định Docker đã siết sẵn một phần seccomp bật 14 trên 40 cap nhưng tài nguyên để max mỗi cờ trên đổi đúng một dòng đọc được không có bước nào là niềm tin tất cả kiểm chứng được bằng proc và cgroup

Hình 2: Áp hết checklist lên một container rồi đọc lại từng knob từ /proc và cgroup, so với container mặc định.

knob mặc định đóng gói cẩn thận
PID 1 sh docker-init (tini)
no_new_privs 0 1
CapEff a80425fb (14 cap) 0000000000000000
Seccomp 2 (lọc) 2 (lọc, giữ nguyên)
memory.max max 268435456 (256 MB)
memory.swap.max max 0 (tắt swap)
cpu.max max 100000 100000 100000 (1 CPU)
pids.max max 100
RLIMIT_NOFILE 1048576 / 1048576 1024 / 1024
rootfs ghi được read-only

Mọi cờ trong checklist đổi đúng một dòng đọc được, không có bước nào là niềm tin. Thí nghiệm cuối chạy hai container cùng image python:3.12: một không cờ gì, một áp hết checklist, rồi đọc cùng một bộ nút điều chỉnh từ /proc và /sys/fs/cgroup của chính chúng. Kết quả xác nhận từng cờ có tác dụng thật. --init đổi PID 1 từ sh thành docker-init, tức tini. --security-opt no-new-privileges bật NoNewPrivs từ 0 lên 1. --cap-drop ALL đưa CapEff từ a80425fb, tức 14 capability, về 0000000000000000, không còn quyền đặc biệt nào. --memory=256m đặt memory.max thành 268435456 byte, và --memory-swap=256m đưa memory.swap.max về 0, tắt swap. --cpus=1.0 đặt cpu.max thành 100000 100000, đúng một CPU. --pids-limit=100 đặt pids.max thành 100. --ulimit nofile=1024:1024 hạ RLIMIT_NOFILE từ hơn một triệu xuống 1024. Và --read-only khiến touch /probe bị chặn với "Read-only file system".

Mặc định Docker đã siết sẵn một phần, nhưng để tài nguyên là "max". So sánh hai cột làm rõ điểm xuất phát. Về quyền, mặc định không phải là trống trơn: seccomp đã bật và lọc, và root chỉ có 14 trong hơn 40 capability, như phần 7 và 8 đã đo. Đây là lớp bảo vệ Docker cho sẵn mà nhiều người không biết mình đang có. Nhưng về tài nguyên, mặc định để tất cả ở "max": không trần bộ nhớ, không trần CPU, không trần số tiến trình, và RLIMIT_NOFILE rất cao. Nghĩa là một container mặc định có thể dùng hết RAM của host, chạy fork bomb, hoặc ngốn trọn CPU, không có gì chặn. Checklist này lấp đúng khoảng trống đó: giữ những lớp bảo vệ mặc định, thêm các trần tài nguyên và siết quyền tới mức tối thiểu cần thiết. Và vì mọi giá trị đều đọc lại được từ /proc và cgroup, không có bước nào phải tin suông, tất cả kiểm chứng được.

Những điều cần nhớ khi áp dụng

Áp checklist theo mức rủi ro, không máy móc toàn bộ. Không phải container nào cũng cần cả mười cờ. Trần tài nguyên, --memory, --cpus, --pids-limit, nên có cho gần như mọi dịch vụ chạy production, vì chúng ngăn một container kéo sập cả host. Siết quyền, --cap-drop ALL và no-new-privileges, đáng làm cho dịch vụ tiếp xúc mạng hoặc xử lý dữ liệu không tin cậy. --read-only mạnh về bảo mật nhưng đòi ứng dụng không ghi vào rootfs, nên cần kiểm trước xem app có cần thư mục ghi tạm nào ngoài /tmp không. Điểm chung là hiểu từng cờ chặn gì để quyết định có cần, chứ không bật tất cả một cách máy móc rồi gỡ dần khi có cái gãy.

Dùng Compose hoặc Kubernetes để cấu hình này thành khai báo, không gõ tay mỗi lần. Một lệnh docker run dài mười cờ khó nhớ và dễ sót khi gõ tay. Trong thực tế, các giá trị này thuộc về file khai báo: docker-compose.yml có mem_limit, cpus, pids_limit, cap_drop, read_only, init, security_opt; Kubernetes có resources.limits, securityContext với capabilities, readOnlyRootFilesystem, allowPrivilegeEscalation, và runAsNonRoot. Chuyển checklist thành khai báo giúp cấu hình được rà soát, lưu phiên bản, và áp đồng nhất cho mọi lần triển khai, thay vì phụ thuộc vào trí nhớ của người gõ lệnh.

Chạy app dưới user không phải root nếu được, lớp phòng thủ cuối. Sê-ri tập trung vào các cơ chế kernel, nhưng một bước đơn giản bổ sung cho tất cả là không chạy ứng dụng dưới uid 0 ngay từ đầu. Thêm USER trong Dockerfile để tiến trình chạy dưới user thường kết hợp tốt với mọi cờ trong checklist: ngay cả khi một capability lọt qua hay một cấu hình bị sai, một tiến trình không phải root đã bị giới hạn sẵn. Phần 7 nhắc rằng user namespace là lớp bổ sung cho capability; chạy non-root là cách rẻ nhất tiến gần nguyên tắc đó mà không cần bật user namespace. Đây là mảnh ghép cuối của một container đóng gói cẩn thận.

Ba ý mang về từ cả sê-ri

  1. Container là tiến trình Linux thường, bị bao bởi namespace và cgroup. Mọi "phép màu" của container đều đọc được ở /proc và /sys/fs/cgroup: namespace quyết định thấy gì, cgroup quyết định dùng được bao nhiêu, và tất cả kiểm chứng được bằng số.
  2. Mặc định Docker siết quyền nhưng buông tài nguyên. Seccomp bật và 14/40 capability là có sẵn; nhưng bộ nhớ, CPU, pids, fd đều để "max". Checklist lấp khoảng trống đó bằng những trần cụ thể đo được.
  3. Đóng gói cẩn thận là mười cờ, mỗi cờ đổi đúng một thứ đọc được. --init, --cap-drop ALL, no-new-privileges, --read-only, cùng trần bộ nhớ, CPU, pids và fd. Không bước nào là niềm tin; tất cả chứng minh được bằng /proc và cgroup.

Nguồn

Sê-ri Container nội bộ dừng ở đây. Mười hai phần đã đi từ namespace và cgroup tới một lệnh docker run đóng gói cẩn thận, mỗi bước kiểm bằng phép đo thật trên Docker 29. Hy vọng bạn rời sê-ri với một mô hình tinh thần rõ ràng: container không phải máy ảo thu nhỏ hay hộp đen, mà là tiến trình Linux thường được kernel bao bởi những ranh giới bạn đọc được, đo được, và chỉnh được.