Trong container, khi bạn ls / bạn thấy một cây thư mục hoàn chỉnh — /bin, /etc, /usr... — trông y như một hệ Linux đầy đủ, và không hề thấy file của host hay của container khác. Nhưng container dùng chung kernel host, chung ổ đĩa vật lý. Vậy làm sao mỗi container lại có một / riêng? Câu trả lời là hai cơ chế kernel ghép lại: mount namespace (cô lập bảng mount) và pivot_root/chroot (đổi thư mục gốc). Bài này (phần 3 loạt Docker) chạy thật để dựng lại từng tầng — kể cả tự tay tạo một rootfs tối giản bằng chroot.

Container thấy rootfs riêng, không phải / của host

Điều đầu tiên: / trong container không phải / của host. Nó là rootfs lấy từ image (alpine, ubuntu...). Đo thật, so / của môi trường host với / của container alpine:

# / của host: bin boot dev etc go home ... work   (có thư mục go, work của mình)
docker run alpine ls /   # bin dev etc home lib ... var   (rootfs alpine, KHÔNG có go/work)

Hai cây thư mục khác hẳn nhau, dù chạy trên cùng một kernel và cùng một máy. Container không "nhìn thấy" file của host — nó có hệ thống file của riêng mình.

Ảnh chụp đoạn mã nền tối minh hoạ mount namespace vì sao mỗi container có cây thư mục gốc riêng, container thấy rootfs riêng không phải của host trong container không phải của host nó là rootfs lấy từ image alpine ubuntu chung kernel nhưng khác cây file docker run alpine ls ra bin dev etc lib của image alpine không thấy file thư mục của host hay container khác, mount namespace mỗi container một bảng mount riêng unshare --mount sh tạo bảng mount độc lập mount tmpfs bên trong chỉ ns đó thấy tiến trình ngoài ns không thấy mount đó cô lập hệ thống file, chroot pivot_root đổi thư mục gốc chroot newroot bin sh đổi của tiến trình thành newroot sau đó tiến trình chỉ thấy cây file dưới newroot nền tảng của rootfs container Docker dùng pivot_root an toàn hơn chroot chroot thoát được, ghép lại thành hệ thống file của container mount namespace cô lập bảng mount cộng pivot_root sang rootfs của image cộng mount proc sys dev riêng bind mount volume bằng container thấy một cây hoàn chỉnh của riêng nó

Hình 1: / container là rootfs từ image, khác / host; mount namespace cô lập bảng mount; chroot/pivot_root đổi thư mục gốc; ghép lại thành hệ thống file hoàn chỉnh, cô lập của container.

Đo thật: ba tầng của filesystem container

Ảnh chụp bảng kết quả chạy thật mount namespace cộng chroot output thật, một của container khác của host host bin boot dev etc go home work container bin dev etc home lib var không có go work cùng kernel khác hoàn toàn cây thư mục rootfs từ image, hai mount namespace cô lập bảng mount unshare --mount trong mount ns mount tmpfs tmp rieng tạo f.txt ls tmp rieng ra f.txt mount ra none on tmp rieng type tmpfs ngoài mount ns ls tmp rieng ra rỗng mount kia vô hình, ba chroot đổi thành rootfs tối giản tự dựng tạo newroot bin lib copy busybox cộng musl loader chroot newroot bin sh ls ra bin lib không còn thấy etc proc usr của rootfs cũ mới là newroot, kết container bằng mount namespace cộng pivot_root sang rootfs của image vì thế mỗi container có một cây hoàn chỉnh cô lập với host

Hình 2: Chạy thật — / host có go/work, / container không (rootfs riêng); trong mount namespace mount tmpfs /tmp/rieng chỉ ns đó thấy (f.txt), ngoài ns thư mục rỗng; chroot /newroot đổi / thành rootfs tối giản chỉ có bin/lib.

  • Rootfs riêng: / host chứa go, work (thư mục của mình); / container alpine chỉ có rootfs của image, không hề có chúng. Đây là cây thư mục độc lập.
  • Mount namespace cô lập bảng mount: unshare --mount tạo một bảng mount riêng. Bên trong, mount -t tmpfs none /tmp/rieng và tạo f.txt — ls thấy file, mount thấy tmpfs. Nhưng ngoài namespace, /tmp/rieng rỗng — mount đó vô hình với tiến trình khác. Đây là cách mỗi container có bảng mount (và các mount /proc, /sys, volume) của riêng nó.
  • chroot đổi thư mục gốc: mình dựng một rootfs tối giản (/newroot với busybox + musl loader), rồi chroot /newroot /bin/sh. Sau đó ls / chỉ thấy bin và lib — không còn /etc, /proc, /usr của rootfs cũ. / của tiến trình đã bị đổi. Đây chính là tiền thân của cách container "chuyển" vào rootfs của image.

Ghép lại thành hệ thống file container

Một container thật ghép cả ba: tạo mount namespace (để mọi mount tiếp theo cô lập), rồi pivot_root (Docker dùng cái này thay chroot) sang rootfs của image, rồi mount /proc, /sys, /dev riêng và bind mount các volume. Kết quả: container thấy một cây / hoàn chỉnh, độc lập, dù thực chất chỉ là vài lời gọi kernel trên chính hệ thống file của host.

Đánh đổi cần cân nhắc

chroot không phải ranh giới bảo mật; pivot_root an toàn hơn. chroot chỉ đổi / nhưng tiến trình có quyền (root) thoát ra được bằng vài kỹ thuật kinh điển. Docker dùng pivot_root kết hợp mount namespace để bỏ hẳn rootfs cũ khỏi tầm với. Đừng coi chroot là cô lập bảo mật — nó là công cụ tổ chức file, không phải hàng rào chống thoát.

Mount namespace giải thích vì sao volume/bind mount hoạt động. Vì container có bảng mount riêng, Docker "chèn" một thư mục host vào container bằng một bind mount trong namespace của container — host và container thấy cùng dữ liệu ở hai đường dẫn khác nhau. Hiểu mount namespace là hiểu vì sao thay đổi trong bind mount phản ánh ngay hai phía (chủ đề bài volume).

Rootfs của image là chỉ đọc + lớp ghi riêng. Bạn không sửa trực tiếp rootfs của image; container thêm một lớp ghi (writable layer) chồng lên bằng overlayfs (chủ đề bài sau). Vì thế nhiều container chạy cùng image chia sẻ rootfs chỉ-đọc mà vẫn ghi riêng — tiết kiệm đĩa lớn. Mount namespace và overlayfs phối hợp để làm điều đó.

Ba ý mang về

  1. Container có rootfs riêng, không phải / của host: đo thật, / host có go/work còn / container alpine chỉ có rootfs của image — chung kernel nhưng khác hoàn toàn cây thư mục.
  2. Mount namespace cô lập bảng mount: đo thật, tmpfs mount trong unshare --mount chỉ ns đó thấy (f.txt), ngoài ns thư mục rỗng — nền tảng để mỗi container có mount /proc/volume riêng.
  3. chroot/pivot_root đổi thư mục gốc: đo thật chroot sang rootfs tối giản khiến / chỉ còn bin/lib; Docker dùng pivot_root (an toàn hơn chroot) sang rootfs của image — nhưng chroot không phải hàng rào bảo mật.

Nguồn

Phần sau ta xem container có mạng riêng thế nào: network namespace, cặp veth nối container với host, và vì sao mỗi container có địa chỉ IP của riêng nó.