Phần 42 đo tốc độ đọc/ghi giữa volume và bind mount. Bài này ở lại mảng lưu trữ nhưng đổi câu hỏi: không phải "nhanh cỡ nào" mà là "ai đọc được". Đây là lỗi rất thường gặp khi đưa container lên máy chủ Linux thật — container ghi file bình thường, nhưng người quản trị SSH vào server lại không mở nổi file đó, mà không có dòng lỗi nào lúc ghi cả. Toàn bộ lệnh trong bài chạy trên Docker 29.7.2 (Docker Desktop, macOS, Apple Silicon, VM linux/aarch64, kernel 7.0.12-linuxkit), container nền alpine:3.20.

Container ghi file bằng UID 1000, người quản trị UID 2000 bị từ chối đọc còn root đọc được, và hai cách vá bằng ACL hoặc group phụ

Vì sao UID lệch nhau

Container không có UID riêng theo kiểu "ảo hoá" — mặc định (không bật --userns-remap), UID bên trong container và UID trên host là cùng một con số trong cùng một không gian UID của kernel Linux. Một ảnh khai USER 999 trong Dockerfile (rất phổ biến với ảnh chính thức như Postgres, hay ảnh nginx bản -unprivileged) thì tiến trình bên trong container thật sự chạy với UID 999 trên host, y hệt như một tiến trình Linux bình thường có UID 999.

Vấn đề nằm ở đây: trên host, hiếm khi có sẵn một user tên postgres mang đúng UID 999. Người quản trị đăng nhập server bằng UID riêng của họ (ví dụ 1000 hoặc 2000), thấy file do container ghi ra hiện UID trần trụi trong ls -la thay vì tên, và không đọc nổi nếu quyền tệp không cho phép "other".

Thử trên bind mount trước — và không thấy gì cả

Bind mount là cách trực quan nhất để container ghi lên thư mục host, nên thử ở đó trước:

$ mkdir /tmp/uid-test && cd /tmp/uid-test
$ docker run --rm -v "$(pwd)":/data alpine:3.20 sh -c "echo tu-root > /data/tu-root.txt"
$ docker run --rm --user 1000:1000 -v "$(pwd)":/data alpine:3.20 sh -c "echo tu-1000 > /data/tu-1000.txt"
$ stat -f '%Su %Sg %N' tu-root.txt tu-1000.txt
claw-bot wheel tu-root.txt
claw-bot wheel tu-1000.txt

Cả file ghi bằng UID 0 lẫn UID 1000 trong container đều hiện ra trên macOS với chủ sở hữu là chính user macOS đang gõ lệnh (claw-bot), không phải root, không phải UID 1000. Đây là tác dụng của virtiofs (tầng chia sẻ filesystem giữa VM Linux và macOS mà Docker Desktop dùng mặc định từ bản 4.6): mọi thao tác ghi từ VM ra thư mục host đều đi qua tiến trình virtiofsd chạy bằng chính user macOS, nên UID trong container bị quy về một mối. Kết luận đo được: lỗi lệch UID qua bind mount không tái hiện được trên Docker Desktop — thử trên máy Mac/Windows sẽ không thấy vấn đề gì, dù nó xảy ra thật trên server Linux (nơi bind mount trỏ thẳng vào một thư mục thật trên cùng một kernel, không qua tầng dịch nào).

Tái hiện đúng lỗi bằng named volume

Named volume thì khác: nó nằm trên ext4 thật bên trong VM Linux, không đi qua virtiofs, nên UID được giữ nguyên như trên một máy Linux thật — đủ để tái hiện chính xác lỗi mà bind mount trên server thật sẽ gây ra.

$ docker volume create uid-demo
$ docker run --rm --user 0:0 -v uid-demo:/data alpine:3.20 chown 1000:1000 /data
$ docker run --rm --user 1000:1000 -v uid-demo:/data alpine:3.20 sh -c \
    "umask 077; echo 'du lieu bi mat' > /data/private.log; stat -c '%u %g %a %n' /data/private.log"
1000 1000 600 /data/private.log

umask 077 mô phỏng cách nhiều ứng dụng ghi log/data nhạy cảm — chỉ chủ sở hữu đọc/ghi được (600). Giờ thử đọc bằng một UID không liên quan gì, đúng như người quản trị đăng nhập server bằng tài khoản riêng của họ:

$ docker run --rm --user 2000:2000 -v uid-demo:/data alpine:3.20 sh -c \
    "cat /data/private.log; echo ma_loi=\$?"
cat: can't open '/data/private.log': Permission denied
ma_loi=1

Thất bại đúng như dự đoán. Nhưng root thì luôn đọc được, bất kể quyền tệp ghi gì:

$ docker run --rm --user 0:0 -v uid-demo:/data alpine:3.20 sh -c "cat /data/private.log"
du lieu bi mat

Đây là hành vi chuẩn của Linux DAC (discretionary access control): root (UID 0) luôn vượt qua kiểm tra bit quyền tệp thông thường. Vì vậy lỗi này thường bị bỏ sót lúc test — ai cũng chạy sudo hoặc debug bằng root nên không bao giờ chạm phải permission denied, chỉ người dùng thật (không có sudo, hoặc cố tình không dùng sudo để tra log) mới gặp.

Vá mà không cần đổi chủ sở hữu

Cách nhanh nhất nhiều người làm là chown file về UID của người cần đọc, hoặc chmod 644/777 cho chắc ăn. Cả hai đều có vấn đề: chown xoá luôn quyền sở hữu gốc mà tiến trình trong container vẫn cần để ghi tiếp, còn chmod nới lỏng cho mọi UID trên máy chứ không riêng người cần. Có hai cách chính xác hơn, đo được cả hai đều hoạt động.

Cách 1: ACL — cấp quyền đúng một UID

$ docker run --rm --user 0:0 -v uid-demo:/data alpine:3.20 sh -c \
    "apk add -q acl && setfacl -m u:2000:r /data/private.log && getfacl -p /data/private.log"
# file: /data/private.log
# owner: 1000
# group: 1000
user::rw-
user:2000:r--
group::---
mask::r--
other::---

$ docker run --rm --user 2000:2000 -v uid-demo:/data alpine:3.20 sh -c "cat /data/private.log; echo ma_loi=\$?"
du lieu bi mat
ma_loi=0

UID 2000 đọc được, còn chủ sở hữu gốc (UID 1000) vẫn ghi thêm bình thường — đã kiểm tra lại, không bị ACL chặn. Yêu cầu duy nhất: filesystem đích phải hỗ trợ ACL POSIX (ext4 có, phần lớn NFS server cấu hình mặc định thì không).

Cách 2: group phụ — không sửa file nào cả

Nếu group của file (không phải owner) đã có quyền đọc, chỉ cần thêm UID cần đọc vào group đó lúc khởi động container, dùng --group-add:

$ docker run --rm --user 1000:1000 -v uid-demo:/data alpine:3.20 sh -c \
    "umask 037; echo bi-mat > /data/app.log; stat -c '%u %g %a' /data/app.log"
1000 1000 640

$ docker run --rm --user 2000:2000 -v uid-demo:/data alpine:3.20 sh -c "cat /data/app.log; echo ma_loi=\$?"
cat: can't open '/data/app.log': Permission denied
ma_loi=1

$ docker run --rm --user 2000:2000 --group-add 1000 -v uid-demo:/data alpine:3.20 sh -c "cat /data/app.log; echo ma_loi=\$?"
bi-mat
ma_loi=0

umask 037 để lại quyền đọc cho group (640). Không cần sửa file nào — chỉ cần container (hoặc tiến trình) đọc file khai thêm --group-add/sg/newgrp đúng GID.

Khi nào không nên dùng cách nào

  • ACL: không dùng nếu filesystem đích không hỗ trợ (một số cấu hình NFS/CIFS), và nhớ rằng ACL đi theo từng file — thêm file mới sau này sẽ không tự có ACL trừ khi đặt ACL mặc định (setfacl -d) trên thư mục.
  • Group phụ: không dùng được nếu ảnh không cho khai --group-add qua cách bạn triển khai (một số nền tảng orchestration giấu cờ này), và không có tác dụng nếu quyền tệp là 600 (group không có bit đọc nào để mượn).
  • chown/chmod 777: tránh dùng làm giải pháp lâu dài — nếu ảnh có entrypoint tự chown lại thư mục dữ liệu mỗi lần khởi động (mẫu khá phổ biến ở các ảnh chính thức chạy bằng non-root user), chown thủ công trên host sẽ bị ghi đè ngay ở lần khởi động sau; còn chmod 777 thì mở toang cho mọi UID trên máy, kể cả UID không liên quan gì đến việc này.
  • Kiểm tra lại trên Docker Desktop trước khi kết luận: như đã đo ở trên, bind mount trên Docker Desktop không tái hiện lỗi lệch UID — phải test bằng named volume (hoặc test thẳng trên server Linux) mới thấy đúng hành vi production.

Bảng tổng hợp

Tình huống UID chủ sở hữu UID đọc Kết quả
Bind mount, Docker Desktop macOS bất kỳ (0 hoặc 1000) user macOS luôn đọc được — UID bị quy về user macOS
Volume, mode 600, chưa vá 1000 2000 (không liên quan) Permission denied
Volume, mode 600, chưa vá 1000 0 (root) đọc được — root vượt qua permission bit
Volume, mode 600, đã vá bằng ACL 1000 2000 đọc được, owner không đổi
Volume, mode 640, --group-add đúng GID 1000 2000 (cùng group phụ) đọc được, owner không đổi

Tổng kết

Lệch UID giữa container và host không phải lỗi hiếm — nó là hậu quả trực tiếp của việc Linux dùng chung một không gian UID cho mọi tiến trình, kể cả tiến trình trong container. Đo được ba điều đáng nhớ: bind mount trên Docker Desktop giấu mất lỗi này vì virtiofs quy UID về user macOS, nên phải dùng named volume (hoặc test trên Linux thật) mới tái hiện đúng; root luôn vượt qua permission bit nên lỗi dễ lọt qua giai đoạn debug bằng sudo; và vá được bằng ACL hoặc group phụ mà không cần đụng tới chủ sở hữu gốc của file.

Thử ba mươi giây

Tái hiện đúng lỗi Permission Denied rồi vá bằng ACL, không cần chown:

docker volume create uid-demo
docker run --rm --user 0:0 -v uid-demo:/data alpine:3.20 chown 1000:1000 /data
docker run --rm --user 1000:1000 -v uid-demo:/data alpine:3.20 sh -c \
  "umask 077; echo bi-mat > /data/f.log"
docker run --rm --user 2000:2000 -v uid-demo:/data alpine:3.20 cat /data/f.log   # Permission denied
docker run --rm --user 0:0 -v uid-demo:/data alpine:3.20 sh -c \
  "apk add -q acl && setfacl -m u:2000:r /data/f.log"
docker run --rm --user 2000:2000 -v uid-demo:/data alpine:3.20 cat /data/f.log   # bi-mat
docker volume rm uid-demo

Bài viết liên quan