PUID và PGID trong Docker Compose là gì?
PUID và PGID không phải thiết lập của Docker mà là quy ước của linuxserver.io. Tìm hiểu cách khắc phục lỗi file bind mount bị sở hữu bởi user 911 trên host của bạn.
PUID và PGID thực chất là gì
PUID và PGID là hai biến môi trường mà một số image container đọc khi khởi động. Bản thân Docker không bao giờ kiểm tra chúng. Đây là một quy ước được sử dụng bởi các image từ linuxserver.io và một vài nguồn khác, vì vậy một image không được viết để đọc các biến này sẽ bỏ qua chúng một cách âm thầm.
Bên trong một image của linuxserver.io có một người dùng tên là abc, được tạo tại thời điểm build với UID (user ID) 911 và GID (group ID) 911. Container khởi động dưới quyền root, chạy các script khởi tạo, và một trong những script đó sẽ đánh số lại người dùng này trước khi bất kỳ tiến trình nào khác diễn ra:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abcCờ -o cho phép sử dụng một ID đã được dùng ở nơi khác. Sau đó, tiến trình khởi tạo sẽ hạ quyền và chạy ứng dụng dưới quyền abc. Vì vậy, PUID=1000 không bao giờ truyền đến Docker. Biến này đánh số lại người dùng bên trong container trước khi ứng dụng khởi động, nghĩa là mọi file mà ứng dụng đó ghi ra sẽ nằm trên ổ đĩa của bạn với quyền sở hữu là 1000. Nếu để PUID trống, abc sẽ giữ nguyên giá trị 911, đó là lý do tại sao một bind mount chưa được cấu hình sẽ chứa đầy các file thuộc sở hữu của 911:911.
Lấy hai số định danh của bạn với id
Chạy lệnh này trên host, với tư cách là người dùng sở hữu các thư mục dữ liệu:
iduid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),988(docker)uid là PUID của bạn và gid là PGID của bạn. Đối với script, id -u và id -g sẽ in ra các con số thuần túy. Trên hầu hết các bản cài đặt VPS mới, tài khoản người dùng đầu tiên thường là 1000:1000, nhưng đừng mặc định như vậy. Một máy chủ được cài lại, hoặc tài khoản thứ hai được thêm vào sau đó, sẽ có ID từ 1001 trở lên, và sai số ở đây là nguyên nhân gây ra lỗi. Nếu các dịch vụ của bạn chạy dưới một tài khoản dịch vụ chuyên dụng thay vì tài khoản đăng nhập của bạn, hãy chạy id thatuser và lấy các con số từ đó.
Tại sao file của bạn hiển thị là 911:911
ls -l in ra ID dạng số thay vì tên khi không có tài khoản hệ thống nào khớp với ID đó. Không có gì trên server của bạn có UID là 911, nên không có tên để hiển thị. Hãy dùng ls -ln để luôn thấy các con số và loại bỏ sự mơ hồ:
ls -ln /srv/appdata/sonarrdrwxr-xr-x 2 911 911 4096 Aug 7 09:12 Backups
-rw-r--r-- 1 911 911 512 Aug 7 09:12 config.xmlKết quả đó cho biết container đã chạy với các thiết lập mặc định có sẵn. Hãy xác nhận từ bên trong container thay vì đoán:
docker exec sonarr id abc
docker compose logs sonarr | head -n 25Init của linuxserver in kết quả vào log khởi động dưới dạng hai dòng:
User UID: 911
User GID: 911Nếu các dòng đó hiển thị 911 sau khi bạn đã thiết lập PUID=1000 trong file Compose, thì biến đó chưa bao giờ truyền được vào container. Nguyên nhân thường gặp là bạn đã sửa docker-compose.yml rồi chạy docker compose restart, lệnh này tái sử dụng container hiện có với môi trường cũ. Các thay đổi về biến môi trường cần dùng docker compose up -d để tạo lại container.
Tại sao bạn không thể xóa file do container tạo ra
Kernel so sánh các con số, không phải tên. Shell của bạn chạy với UID 1000. File đó thuộc về UID 911. Thư mục chứa nó là drwxr-xr-x và cũng thuộc về 911, vì vậy nhóm (group) và người dùng khác (other) chỉ có quyền đọc và thực thi chứ không có quyền ghi. Việc xóa một file yêu cầu quyền ghi trên thư mục chứa nó, chứ không phải trên chính file đó, vì vậy bạn sẽ gặp lỗi này ngay cả khi file đó trông có vẻ bình thường:
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission deniedMột container đang ghi dữ liệu cũng gặp rào cản tương tự từ phía ngược lại. Nếu thư mục trên host thuộc về người dùng của bạn với mode 755 và ứng dụng chạy dưới quyền 911, lần ghi đầu tiên sẽ thất bại với Permission denied và ứng dụng sẽ báo lỗi theo cách riêng của nó. Trong một ứng dụng .NET như Sonarr hoặc Radarr, lỗi này xuất hiện dưới dạng UnauthorizedAccessException: Access to the path '/data/downloads' is denied. Chuỗi quyền ở phía trước file cho bạn biết bạn đang bị đánh giá bởi tập hợp quyền nào trong ba tập hợp quyền, và việc đọc đúng drwxr-xr-x là thứ biến lỗi đó từ bí ẩn trở nên rõ ràng.
Đây là vấn đề cụ thể của bind mount. Khi Docker tạo một named volume trống và mount nó đè lên một đường dẫn đã tồn tại trong image, nó sẽ sao chép nội dung của đường dẫn đó vào volume, bao gồm cả quyền sở hữu và các bit quyền, để ứng dụng tìm thấy một thư mục mà nó đã sở hữu. Bind mount không nhận được sự xử lý đó: Docker mount thư mục trên host của bạn chính xác như những gì nó đang có. Sự khác biệt đó là một trong những lý do thực tế để biết khi nào bind mount tốt hơn named volume và khi nào thì không.
Sửa lỗi thư mục đã bị sai quyền
Việc thiết lập PUID và PGID chỉ thay đổi cách ứng dụng hoạt động từ thời điểm này trở đi. Nó không tự động sửa lại các file đã tồn tại trên ổ đĩa. Hãy dừng stack, tự sửa quyền sở hữu, sau đó khởi động lại:
docker compose down
sudo chown -R 1000:1000 /srv/appdata/sonarr
docker compose up -dSử dụng sudo chown -R "$(id -u):$(id -g)" /srv/appdata/sonarr nếu bạn không muốn nhập các con số. Hãy thực hiện việc này khi container đã dừng, vì một ứng dụng đang chạy và ghi dữ liệu trong lúc thực hiện chown đệ quy có thể dẫn đến cây thư mục chỉ được sửa một nửa và gây ra loạt lỗi khó hiểu tiếp theo.
Những vấn đề PUID và PGID không giải quyết được
Đây là phần khiến nhiều người dù làm đúng mọi bước vẫn gặp lỗi. Init script của linuxserver chỉ thực hiện chown chính xác ba đường dẫn khi khởi động: /app, /config và /defaults. Các mount chứa dữ liệu media của bạn không nằm trong danh sách này. /data, /downloads và /tv được chuyển đến ứng dụng mà không bị thay đổi quyền sở hữu. Do đó, nếu phía host của các mount này có quyền sở hữu mà user trong container không thể ghi vào, container vẫn khởi động bình thường, hiển thị đúng UID trong banner, nhưng sẽ lỗi ngay khi thực hiện import lần đầu.
Đây là hành vi đúng. Việc thực hiện chown đệ quy trên thư viện media dung lượng 12 terabyte mỗi khi container khởi động sẽ là một thảm họa. Điều này có nghĩa là bạn phải tự quản lý các thư mục media, và đây chính là các mount thường xảy ra lỗi phân quyền.
Ba cách để kiểm soát người dùng và trường hợp áp dụng cho từng cách
Các biến môi trường PUID và PGID
Cách này chỉ hoạt động trên các image có entrypoint đọc các biến này. Nó phổ biến vì container vẫn khởi động dưới quyền root, tự thực hiện thiết lập, sửa /config, và chỉ sau đó mới hạ quyền. Docker Mods và các init script tùy chỉnh vẫn hoạt động bình thường. Điểm trừ là bạn đang tin tưởng vào một quy ước thay vì một tính năng của nền tảng, và tên biến không đồng nhất giữa các dự án.
Khóa user: trong Compose
Đây là một tính năng thực sự của Docker và hoạt động trên mọi image, vì container runtime áp dụng nó trước khi code của image chạy:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"Tiến trình không bao giờ chạy dưới quyền root, dù chỉ một khoảnh khắc, đây là một lợi ích bảo mật thực sự. Nó cũng làm hỏng bất kỳ thứ gì trong entrypoint cần quyền root. Trên các image của linuxserver, dự án hỗ trợ cách này trên cơ sở nỗ lực hợp lý và chỉ cho các image đã được kiểm thử, với các lưu ý cụ thể: PUID và PGID sẽ không còn tác dụng, Docker Mods sẽ không chạy, các dịch vụ tùy chỉnh sẽ không chạy, và bạn phải chịu trách nhiệm về quyền truy cập trên mọi volume được mount. Mô hình được tài liệu hóa của họ kết hợp flag này với một /run có thể ghi:
user: 1000:1000
tmpfs:
- /run:uid=1000,gid=1000,exec
security_opt:
- no-new-privileges=trueMột tác dụng phụ về mặt hiển thị khiến nhiều người bất ngờ. Một user: dạng số không có mục tương ứng trong /etc/passwd của container, nên các công cụ bên trong sẽ báo whoami: cannot find name for user ID 1000. ID vẫn hợp lệ và việc truy cập file hoạt động bình thường. Chỉ có việc tra cứu tên là thất bại.
Rootless Docker
Rootless Docker chạy chính daemon dưới người dùng không có đặc quyền của bạn, vì vậy không có gì trên máy chạy dưới quyền root thực sự. Nó thay đổi hoàn toàn phép tính sở hữu. UID 0 của container ánh xạ tới UID host của người dùng đang chạy rootless Docker, và UID n của container cho bất kỳ n nào từ 1 trở lên sẽ ánh xạ tới subuid + (n - 1), trong đó subuid là cơ sở của dải ID được cấp cho bạn trong /etc/subuid và /etc/subgid. Docker yêu cầu ít nhất 65,536 ID phụ ở đó.
Hãy đọc lại phần ánh xạ đó, vì nó đảo ngược lời khuyên thông thường. Trong rootless Docker, một container ghi file dưới quyền root sẽ tạo ra các file thuộc sở hữu của bạn. Một container ghi file dưới UID 1000 sẽ tạo ra các file thuộc sở hữu của một ID phụ nằm đâu đó quanh 100999, mà shell của bạn không thể truy cập được. Vì vậy, giá trị PUID đúng trên một daemon chạy rootful lại là giá trị sai ở đây. Hai cơ chế này giải quyết cùng một vấn đề ở các tầng khác nhau, và việc chồng chéo chúng mà không kiểm tra là lý do khiến người dùng kết thúc với một thư mục cần sudo mới xóa được. Nếu bạn dùng rootless, hãy kiểm tra quyền sở hữu của một file được ghi trên server của chính bạn trước khi di chuyển thư viện vào đó.
Đối với hầu hết các stack tự host trên một VPS đơn lẻ, PUID và PGID trên một daemon rootful là lựa chọn thực dụng, vì đó là những gì các image được xây dựng và viết tài liệu hỗ trợ. Hãy dùng user: khi README của image nói rằng image đó đã được kiểm thử cho cách này, hoặc khi bạn đang chạy một image upstream chính thức không hỗ trợ PUID.
Trường hợp media stack: một nhóm dùng chung giữa các container
Một media stack với Sonarr, Radarr và client tải xuống là nơi lý thuyết này được áp dụng thực tế. Client tải xuống ghi file đã hoàn tất vào /data/downloads. Sau đó, Sonarr tạo hardlink hoặc di chuyển file đó vào /data/media. Để hardlink hoạt động, cả hai container cần quyền ghi vào cùng một cây thư mục. Nếu client tải xuống chạy với UID 1000 còn Sonarr chạy với 1001, một trong hai sẽ sở hữu file mà bên kia chỉ có quyền đọc.
Cách giải quyết là tạo một nhóm dùng chung mà mọi container trong stack đều sử dụng làm PGID:
sudo groupadd -g 13000 media
sudo usermod -aG media deploy
sudo chown -R deploy:media /srv/media
sudo find /srv/media -type d -exec chmod 2775 {} +
sudo find /srv/media -type f -exec chmod 0664 {} +Ký tự 2 đứng đầu trong 2775 là bit setgid. Trên một thư mục, nó có nghĩa là mọi file và thư mục con mới được tạo bên trong sẽ kế thừa nhóm media thay vì nhóm chính của người tạo. Nhờ đó, cấu hình này vẫn duy trì hiệu lực với các file tải xuống mới mà bạn không cần chạy lại chown. Hãy đăng xuất và đăng nhập lại, hoặc chạy newgrp media trước khi kiểm tra quyền truy cập của chính bạn: một nhóm được thêm bằng usermod -aG sẽ không xuất hiện trong phiên shell đang mở.
Bên trong container, groupmod -o -g 13000 abc đánh số lại nhóm abc thành 13000, để abc ghi file với cùng GID như nhóm media trên host của bạn. Mỗi container trong stack giữ PUID riêng nhưng dùng chung một PGID.
Sau đó, thiết lập UMASK=002 trên mọi container linuxserver trong stack. Đây là bước mọi người thường bỏ qua. Mặc định trong các image này là UMASK=022, nó loại bỏ bit ghi của nhóm khỏi mọi file mới. Kết quả là file có quyền 0644 và việc chia sẻ bạn vừa cấu hình sẽ không có tác dụng. 002 tạo ra file có quyền 0664 và thư mục có quyền 0775, cho phép nhóm có quyền ghi:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=Etc/UTC
volumes:
- /srv/appdata/sonarr:/config
- /srv/media:/data
restart: unless-stoppedHai giá trị đó cần nằm trong file .env đặt cạnh file Compose, để toàn bộ stack đọc cùng một định nghĩa:
PUID=1000
PGID=13000Compose tự động đọc file đó để thay thế biến theo kiểu ${PUID}, đây cũng là cơ chế bạn dùng cho thông tin xác thực. Thói quen giữ các giá trị bên ngoài docker-compose.yml và đưa vào file .env cũng áp dụng ở đây, với điểm khác biệt là hai con số này không phải là bí mật.
Hãy xác minh từ đầu đến cuối thay vì chỉ tin vào cấu hình. Ghi một file từ bên trong một container và đọc nó từ host:
docker exec sonarr touch /data/downloads/permtest
ls -ln /srv/media/downloads/permtestKết quả đúng sẽ hiển thị PUID của bạn là chủ sở hữu, 13000 là nhóm và -rw-rw-r-- là mode. Nếu nhóm hiển thị 1000, nghĩa là bit setgid đang thiếu trên thư mục đó. Nếu mode hiển thị -rw-r--r--, nghĩa là biến UMASK chưa có hiệu lực, hãy kiểm tra xem bạn đã recreate container thay vì chỉ restart nó chưa. Xóa file kiểm tra bằng rm /srv/media/downloads/permtest khi bạn hoàn tất.
Image nào sử dụng biến nào
Các image từ linuxserver.io sử dụng PUID, PGID và UMASK. Paperless-ngx sử dụng các tên khác cho cùng một mục đích: USERMAP_UID và USERMAP_GID, cả hai đều mặc định là 1000, và tài liệu của nó hướng dẫn bạn đọc các giá trị này từ id -u và id -g. Nhiều image chính thức từ upstream, bao gồm các image cơ sở dữ liệu và web server phổ biến, được thiết lập sẵn một user cố định và yêu cầu bạn sử dụng user: hoặc giữ nguyên cấu hình mặc định.
Vì vậy, hãy kiểm tra README của từng image trước khi bạn sao chép một khối biến môi trường giữa các dự án. Docker truyền bất kỳ biến môi trường nào bạn thiết lập vào trong container, bất kể bên trong có ứng dụng nào đọc nó hay không, và một PUID không được sử dụng sẽ không tạo ra lỗi, không cảnh báo và không có tác dụng gì. Container sẽ chạy dưới quyền của user được chỉ định cuối cùng trong Dockerfile của chính nó, và bạn chỉ có thể biết được điều này thông qua quyền sở hữu của các file mà nó tạo ra.
FAQ
Tại sao các file Docker của tôi lại thuộc sở hữu của 911:911?
911 là UID và GID của người dùng abc được tích hợp sẵn trong các image của linuxserver.io. Việc thấy con số này nghĩa là container đã khởi động mà không có PUID và PGID được thiết lập, nên script khởi tạo của nó giữ nguyên các giá trị mặc định. ls -l hiển thị các con số thô vì không có tài khoản nào trên host của bạn có ID 911, nên không có tên để hiển thị. Hãy đặt PUID và PGID theo kết quả của lệnh id, tạo lại container bằng docker compose up -d, sau đó sửa quyền sở hữu các file hiện có bằng sudo chown -R 1000:1000 trên thư mục bị ảnh hưởng.
PUID và PGID có hoạt động trên mọi Docker image không?
Không. Chúng không phải là tính năng của Docker và Docker không bao giờ đọc các biến này. Chúng chỉ hoạt động trên các image mà entrypoint tự đọc các biến này rồi gọi usermod và groupmod trước khi chạy ứng dụng, ví dụ như các image của linuxserver.io và một vài dự án sao chép mô hình này. Các dự án khác sử dụng tên gọi khác, ví dụ như USERMAP_UID và USERMAP_GID trong paperless-ngx. Trên một image không đọc các biến này, chúng sẽ được chấp nhận nhưng bị bỏ qua mà không có cảnh báo nào.
Tôi nên dùng PUID và PGID hay khóa user: trong Docker Compose?
Hãy dùng PUID và PGID khi image hỗ trợ chúng, vì entrypoint vẫn chạy dưới quyền root đủ lâu để sửa /config và khởi động các dịch vụ của nó một cách chính xác. Hãy dùng user: khi image không hỗ trợ PUID, hoặc khi README của image ghi rõ là đã được kiểm thử để chạy mà không cần quyền root. Trên một image của linuxserver, việc thiết lập user: sẽ làm cho PUID và PGID trở nên vô tác dụng, ngăn Docker Mods và các dịch vụ tùy chỉnh chạy, đồng thời khiến việc quản lý quyền truy cập của mọi volume được mount trở thành trách nhiệm của bạn.
Sonarr có PUID đúng nhưng vẫn không thể di chuyển file. Lỗi do đâu?
Hãy kiểm tra ba thứ theo thứ tự. Thứ nhất, bản thân mount chứa media: script khởi tạo chỉ thực hiện chown cho /app, /config và /defaults, nên /data hoặc /downloads vẫn giữ nguyên quyền sở hữu như trên host. Thứ hai, group dùng chung: nếu client tải file và Sonarr chạy dưới các GID khác nhau, không bên nào có thể sửa file của bên kia, vì vậy hãy đặt cùng một PGID cho mọi container trong stack. Thứ ba, umask: giá trị mặc định UMASK=022 của image ghi file với quyền 0644 mà không có bit ghi cho group, điều này làm vô hiệu hóa hoàn toàn việc chia sẻ theo group. Hãy thiết lập UMASK=002 và đặt bit setgid trên các thư mục bằng chmod 2775 để các file mới tự động kế thừa group.