Cài Docker trên Rocky Linux và AlmaLinux
Cài Docker Engine bằng dnf trên Rocky Linux hoặc AlmaLinux, bật service và xử lý lỗi podman chiếm lệnh docker cùng SELinux chặn bind mount.
Cài đặt Docker trên Rocky Linux và AlmaLinux
Để cài Docker trên Rocky Linux hoặc AlmaLinux, bạn thêm repository dnf của Docker, cài engine cùng compose plugin, rồi bật service. Phần này chỉ cần 4 lệnh và giống hệt trên cả hai bản phân phối vì cả hai đều là bản dựng lại của Red Hat Enterprise Linux (RHEL) và dùng chung bố cục package. CentOS Stream cũng hoạt động theo cách tương tự. Mọi nội dung bên dưới áp dụng cho cả hai bản phân phối. Nếu bạn vẫn đang chọn giữa chúng, yếu tố quyết định là cam kết tương thích của từng dự án và việc CPU cũ của bạn còn được hỗ trợ hay không.
Quy trình cài đặt ngắn, nên phần lớn hướng dẫn này tập trung vào những điểm Enterprise Linux (EL) khác Ubuntu. Podman có thể đã chiếm lệnh docker trên image của bạn. SELinux sẽ chặn các file bind mount cho đến khi chúng có label phù hợp. Firewalld không lọc các port mà Docker publish, nên port của container có thể mở ra Internet trong khi firewall-cmd báo rằng không có port nào đang mở.
Không dùng convenience script của Docker từ get.docker.com. Tài liệu chính thức của Docker nói rằng script này không được khuyến nghị cho môi trường production. Script tự ghi đè cấu hình repository mà không hỏi và không thể chạy lại an toàn để upgrade. Tự thêm repository có nghĩa là dnf upgrade xử lý Docker như mọi package khác trên máy. Cách này cũng đưa engine vào phạm vi của dnf-automatic, nếu bạn cấu hình công cụ này tự áp dụng security update theo lịch, nên hãy quyết định sớm xem bạn muốn Docker được patch tự động hay giữ nguyên cho đến maintenance window. Dù chọn cách nào, một lần upgrade sẽ thay thế binary trong package còn dockerd cũ vẫn tiếp tục chạy, và needs-restarting là lệnh cho biết service nào vẫn đang chạy code mà bạn vừa thay thế.
Podman đã xử lý lệnh docker chưa?
Rocky Linux và AlmaLinux cung cấp podman trong các repository mặc định, và nhiều image VPS cài sẵn podman cho bạn. Một số image còn cài thêm podman-docker, tạo một shell script tại /usr/bin/docker để gọi podman. Khi đó, mọi lệnh docker bạn nhập đều chạy podman thay vì Docker, nên các hướng dẫn viết cho Docker có thể cho ra kết quả bạn không mong đợi.
Dấu hiệu đầu tiên là một banner. Script /usr/bin/docker kiểm tra file /etc/containers/nodocker. Nếu file này không tồn tại, script sẽ in một dòng trước khi chạy bất kỳ việc gì:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.Có thể ai đó đã tạo file này để tắt banner, nên không được chỉ dựa vào dấu hiệu đó. Hãy hỏi package database xem package nào sở hữu binary này:
command -v docker
rpm -qf "$(command -v docker)"Nếu câu trả lời bắt đầu bằng podman-docker, podman đang xử lý lệnh. Nếu câu trả lời bắt đầu bằng docker-ce-cli, đó là Docker thật. Nếu rpm -qf báo không có package nào sở hữu file, có người đã cài file này thủ công. Hãy đọc script trước khi tin tưởng nó.
Podman chạy cùng các OCI image và là một lựa chọn hợp lý. Nếu muốn dùng podman, bạn có thể dừng tại đây. Cả hai đều là container engine cho Linux. Nếu vẫn chưa quyết định nền tảng, bạn nên biết rằng FreeBSD jail cô lập toàn bộ userland thay vì chạy các image dạng layer được kéo từ registry. Nếu muốn dùng Docker Engine, trước tiên hãy gỡ các package xung đột. Đây là danh sách Docker cung cấp cho RHEL:
sudo dnf remove docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runcĐọc kỹ những gì dnf dự định gỡ kèm trước khi xác nhận. Trên image VPS mới, danh sách này thường ngắn. Trên máy đã được sử dụng trước đó, việc gỡ podman có thể kéo theo cockpit-podman hoặc một công cụ khác đang phụ thuộc vào package này.
Về nguyên tắc, có thể giữ podman cùng với Docker: chỉ gỡ podman-docker để tên docker được giải phóng, cùng với runc, là package được containerd.io thay thế. Tài liệu của Docker xem podman là package xung đột, nên Docker không hỗ trợ cách bố trí này. Nếu quá trình cài đặt vẫn báo xung đột, hãy dùng toàn bộ danh sách gỡ ở trên.
Thêm repository của Docker bằng dnf config-manager
Docker phát hành các gói RPM cho Enterprise Linux tại download.docker.com. File repository trỏ đến cây CentOS, là cây mà Rocky Linux và AlmaLinux dùng để phân giải. Việc trỏ một máy Rocky đến repository CentOS có vẻ là lỗi cho đến khi bạn biết cả hai bản phân phối đều phát triển từ dòng CentOS sau khi Red Hat chuyển CentOS sang Stream vào năm 2020. Kiểm tra vào tháng 8 năm 2026, tài liệu Docker ghi repository này dành cho CentOS Stream 9 và CentOS Stream 10.
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repodnf phiên bản 5 đã bỏ tham số --add-repo, vì vậy lệnh thứ hai sẽ fail trên các bản phát hành mới hơn. Kiểm tra phiên bản đang dùng, rồi chọn cú pháp tương ứng:
dnf --versionNếu lệnh in ra phiên bản 5.x, hãy dùng dạng subcommand:
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repoCả hai cách đều ghi cùng một file vào /etc/yum.repos.d/docker-ce.repo. Dùng sai cú pháp sẽ fail với lỗi unknown-argument thay vì âm thầm thực hiện sai, nên bạn sẽ nhận ra ngay.
File repository này đặt baseurl thành một đường dẫn có chứa $releasever, còn dnf sẽ mở rộng biến đó từ release package của bạn. Rocky Linux và AlmaLinux đặt biến này thành số phiên bản chính, nên 9 trên EL 9 và 10 trên EL 10. Vì vậy repository CentOS vẫn được phân giải đúng trên máy Rocky. Xác nhận giá trị sau khi mở rộng trước khi cài đặt:
sudo dnf repoinfo docker-ce-stableĐọc dòng Repo-baseurl. Dòng này phải kết thúc bằng /9/x86_64/stable hoặc /10/x86_64/stable. Nếu bản phát hành của bạn đặt $releasever thành một point version như 9.6, dnf sẽ báo Status code: 404 cho URL đó khi tải metadata. Sửa bằng cách chỉnh sửa /etc/yum.repos.d/docker-ce.repo và thay $releasever bằng số phiên bản chính, không có phần bổ sung.
Cài engine và compose plugin
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginCó 5 package, mỗi package đảm nhiệm một việc. docker-ce là daemon, dockerd. docker-ce-cli là lệnh docker bạn nhập. containerd.io là container runtime mà daemon điều khiển. docker-buildx-plugin dùng để build image. docker-compose-plugin cung cấp docker compose dưới dạng subcommand.
Các package này không cài binary docker-compose có dấu gạch nối. Đó là Compose v1, đã hết vòng đời vào tháng 7 năm 2023. Mọi thứ gọi docker-compose có dấu gạch nối đều phải được cập nhật thành docker compose với khoảng trắng.
Lần cài đặt đầu tiên sẽ dừng lại để import signing key của Docker và hiển thị fingerprint của key. Key này lấy từ gpgkey=https://download.docker.com/linux/centos/gpg trong file repo bạn vừa thêm, vì vậy hãy so sánh fingerprint do dnf in ra với URL đó trước khi chấp nhận.
Có một lỗi xuất hiện đủ thường xuyên để cần nêu riêng. Nếu dnf báo containerd.io yêu cầu container-selinux nhưng không có package nào cung cấp package đó, repository AppStream của bạn đang bị tắt. Chạy dnf repolist và xác nhận appstream được liệt kê, vì đó là nơi container-selinux được phát hành trên EL 9 và EL 10.
Khởi động Docker và xác nhận Docker đang chạy
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
sudo docker run --rm hello-worldCác gói RPM của Docker để daemon ở trạng thái đã dừng và bị tắt sau khi cài đặt. Vì vậy, bước này có trên trang Docker dành cho CentOS nhưng không có trên trang Ubuntu, nơi gói deb tự khởi động service. Nếu bỏ qua enable, Docker chỉ chạy đến lần reboot tiếp theo, sau đó sẽ dừng và khiến mọi container cũng dừng theo.
systemctl status phải hiển thị Active: active (running). Container hello-world phải in This message shows that your installation appears to be working correctly. rồi thoát. Nếu thay vào đó lệnh in lỗi permission trên /var/run/docker.sock, bạn đã bỏ sudo. Phần về nhóm docker bên dưới sẽ khắc phục lỗi này.
Hãy kiểm tra riêng compose plugin, vì đây là một gói khác và có thể bị thiếu dù engine vẫn hoạt động:
docker compose versionKết quả bình thường có dạng Docker Compose version v2.x.x. Việc khôi phục các service sau reboot là vấn đề khác với việc enable daemon, và restart policy quyết định các service Compose có tự khởi động lại khi boot hay không.
Vì sao bind mount bị từ chối quyền?
Rocky Linux và AlmaLinux chạy SELinux (Security-Enhanced Linux) ở chế độ enforcing theo mặc định. Xác nhận bằng getenforce; lệnh này in ra Enforcing.
Container Docker chạy dưới SELinux type container_t. Type này chỉ được đọc và ghi các file có label container_file_t. Directory bạn tạo trên host nhận label từ đường dẫn cha, và label đó không phải container_file_t. Container vẫn bị từ chối dù owner, group và mode nhìn có vẻ đúng từ phía host. Tái hiện bằng 3 lệnh:
sudo mkdir -p /srv/site
echo hello | sudo tee /srv/site/index.html
sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro nginx:alpine cat /usr/share/nginx/html/index.htmlContainer in ra:
cat: can't open '/usr/share/nginx/html/index.html': Permission deniedHai lệnh sẽ cho thấy nguyên nhân. ls -ldZ /srv/site in label; với một path bên dưới /srv, label đó là system_u:object_r:var_t:s0 chứ không phải container_file_t. Sau đó, sudo ausearch -m avc -ts recent in audit record của kernel. Record này chứa avc: denied { read }, một trường scontext= chỉ ra container_t, và trường tcontext= chỉ ra label mà bạn vừa thấy trên directory. Sự không khớp giữa hai trường đó chính là toàn bộ nguyên nhân.
Cách sửa là thêm một suffix vào volume argument. Docker sẽ relabel path cho bạn:
sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro,z nginx:alpine cat /usr/share/nginx/html/index.html:z viết thường sẽ relabel content ở chế độ shared, để nhiều container có thể dùng cùng một directory. :Z viết hoa sẽ relabel content ở chế độ private và unshared, gắn với một container; khi đó container thứ hai đọc cùng path sẽ bị từ chối. Dùng :z cho mọi directory mà sidecar hoặc backup container cũng truy cập. Dùng :Z cho directory của database do một container duy nhất quản lý.
Tài liệu Docker có một cảnh báo cần nhắc lại vì thao tác relabel được áp dụng đệ quy. Bind-mount một system directory như /home hoặc /usr với :Z sẽ "khiến host machine không thể hoạt động và bạn có thể phải relabel thủ công các file trên host machine". Chỉ trỏ các suffix này đến directory bạn tạo cho container, không bao giờ trỏ đến system path.
Trong Compose, suffix được đặt trên cùng chuỗi đó:
services:
web:
image: nginx:alpine
volumes:
- /srv/site:/usr/share/nginx/html:ro,zCó hai giới hạn dễ gặp. Flag --mount hoàn toàn không thể đặt SELinux label, nên hãy dùng -v khi cần đặt label. Named volume không cần suffix vì Docker tự label các directory mà nó tạo bên dưới /var/lib/docker/volumes.
Không tắt SELinux. Chỉ dùng sudo setenforce 0 để kiểm tra trong một phút: nếu container hoạt động sau đó, vấn đề là label và :z là cách xử lý. Lập tức bật lại bằng sudo setenforce 1. Trên Enterprise Linux, permission denied trên bind mount có hai nguyên nhân riêng biệt nhưng nhìn giống hệt nhau từ bên trong container. Một nguyên nhân là SELinux label. Nguyên nhân còn lại là owner và group dạng số theo cơ chế thông thường; đây là vấn đề mà các biến PUID và PGID được dùng để xử lý. ls -lnZ hiển thị mode, owner dạng số và label trên cùng một dòng, giúp bạn xác định mình đang xử lý nguyên nhân nào.
Vì sao port đã publish vẫn truy cập được khi firewalld có vẻ đã đóng?
Firewalld là firewall mặc định trên Rocky Linux và AlmaLinux. Kiểm tra firewalld đang chạy bằng sudo systemctl is-active firewalld. Nếu bạn chưa cấu hình firewalld trên máy này, hãy xem mở SSH và một web port bằng firewalld trước. Phần bất ngờ bên dưới chỉ có ý nghĩa khi bạn đã có một ruleset của zone đang hoạt động để đối chiếu. Bây giờ hãy publish một port và xem firewalld cho rằng port nào đang mở:
sudo docker run -d --name web -p 8080:80 nginx:alpine
sudo firewall-cmd --list-portsfirewall-cmd in ra một dòng trống. Từ máy khác, curl -I http://YOUR_SERVER_IP:8080/ trả về HTTP/1.1 200 OK. Port này mở ra Internet nhưng firewall không báo gì.
Nguyên nhân là đường đi của packet. Các rule zone của firewalld lọc traffic được gửi trực tiếp đến chính host. Port đã publish không được gửi đến host: Docker cài một rule destination NAT (network address translation) để đổi đích sang địa chỉ của container trước khi packet đi vào input path của host. Vì vậy kernel forward packet thay vì giao packet cho host ở local path. Sau đó Docker đặt các bridge interface của mình vào một firewalld zone có tên docker với target là ACCEPT, đồng thời thêm một forwarding policy có tên docker-forwarding, cho phép forward từ mọi zone vào zone docker. Các rule của zone bạn không nhìn thấy packet này.
Cách xử lý gọn nhất không cần thêm firewall rule. Hãy bind phía host của publish vào loopback và đặt một reverse proxy phía trước:
sudo docker rm -f web
sudo docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
curl -I http://127.0.0.1:8080/Lệnh local curl trả về HTTP/1.1 200 OK, còn cùng request từ máy khác sẽ không kết nối được nữa. Nếu đối số -p không có địa chỉ host, port sẽ được publish trên mọi interface. Vì vậy, hãy xem -p 8080:80 không có địa chỉ host là quyết định public service đó ra ngoài.
Khi cần cho phép một service truy cập từ một số địa chỉ nhưng chặn các địa chỉ khác, Docker có sẵn một chain dành cho việc này. DOCKER-USER được xử lý trước các accept rule riêng của Docker. Vì vậy, rule bạn đặt tại đây vẫn còn sau khi Docker restart và ghi lại các chain của nó:
sudo iptables -I DOCKER-USER -i enp1s0 ! -s 203.0.113.10 -j DROP
sudo iptables -S DOCKER-USERHãy lấy tên interface từ ip route show default thay vì mặc định dùng eth0, vì các image EL hiện tại sử dụng những tên như enp1s0 hoặc ens3. Trên Rocky và AlmaLinux, lệnh iptables là lớp tương thích trên nftables, và các chain của Docker có thể xem qua lệnh này. Các rule thêm theo cách này sẽ mất sau khi reboot nếu bạn không lưu chúng. Khi đã kiểm tra và thấy đúng, hãy ghi chúng vào một systemd unit.
Docker Engine 28.0, phát hành vào năm 2025, đã xử lý một lỗ hổng liên quan: truy cập trực tiếp theo routed path đến các port của container chưa từng được publish giờ bị chặn trong chain DOCKER. Thay đổi này không ảnh hưởng đến các port đã publish, nên toàn bộ nội dung trên vẫn áp dụng cho các phiên bản hiện tại. Bạn nên hình thành một thói quen vận hành: sau mỗi sudo firewall-cmd --reload, hãy kiểm tra lại port đã publish. Nếu port không còn phản hồi, sudo systemctl restart docker sẽ cài lại các rule của Docker.
Quản trị viên Ubuntu gặp vấn đề tương tự qua một công cụ khác. Xem vì sao các port Docker đã publish bỏ qua rule của ufw. Trong cả hai trường hợp, nguyên nhân là NAT path. Chỉ firewall đứng phía trước NAT path là khác nhau.
Thêm user không phải root vào group docker
Gõ sudo trước mỗi lệnh docker sẽ nhanh chóng trở nên bất tiện, và group docker giúp loại bỏ việc đó:
sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-worldusermod -aG chỉnh sửa /etc/group, nhưng shell hiện tại của bạn đã có danh sách group riêng, nên thay đổi này chưa có hiệu lực cho đến khi bạn tạo shell mới. newgrp docker khởi động một shell có group này để bạn kiểm tra ngay. Các phiên SSH mới sẽ tự nhận thay đổi này.
Hãy hiểu rõ quyền mà group này cấp. Thành viên của group có quyền ghi vào /var/run/docker.sock, và mọi tiến trình có thể giao tiếp với socket đó đều có thể yêu cầu daemon khởi động một container mount filesystem của host. Một lệnh sẽ cho thấy điều này có ý nghĩa gì:
docker run --rm -v /:/host alpine wc -l /host/etc/shadowLệnh này đọc một file chỉ root mới đọc được, từ một account không có quyền sudo. Tài liệu post-install của Docker cũng nói rõ điều tương tự: group docker cấp quyền tương đương root. Chỉ thêm một account vào group này nếu bạn cũng sẵn sàng cấp cho account đó sudo. Nếu đang thiết lập account trên server mới, hãy quyết định việc này cùng với phần thiết lập user theo nguyên tắc đặc quyền tối thiểu trên VPS thay vì để đến sau.
Docker cũng có rootless mode, chạy daemon dưới một user không có đặc quyền. Đây là một quy trình cài đặt riêng và làm thay đổi cách storage driver cũng như các cổng dưới 1024 hoạt động. Vì vậy, hãy lập kế hoạch cho nó như một dự án riêng, thay vì xem đó là một flag có thể thêm sau.
Bước tiếp theo
Bạn đã có Docker Engine, Compose plugin, một service tự khởi động lại sau reboot và ba hành vi riêng của EL được ghi ở trên. Bước tiếp theo là compose.yaml cho từng service. Cấu trúc của một file Compose trình bày định dạng file và các command dùng để vận hành file đó. Nếu đây là container host đầu tiên của bạn, Chạy Docker trên một VPS trình bày các vấn đề về sizing, storage và image hygiene mà hướng dẫn này chưa đề cập.
FAQ
Repository CentOS của Docker có hoạt động trên Rocky Linux và AlmaLinux không?
Có. Thêm https://download.docker.com/linux/centos/docker-ce.repo bằng dnf config-manager. baseurl trong file đó chứa $releasever. Rocky Linux và AlmaLinux thay biến này bằng số phiên bản chính. Vì vậy, máy EL 9 sẽ dùng cây CentOS 9, còn máy EL 10 sẽ dùng cây CentOS 10. Xác nhận việc thay biến bằng sudo dnf repoinfo docker-ce-stable và đọc dòng Repo-baseurl. Nếu Status code: 404 xuất hiện khi dnf tải metadata, biến đã được thay bằng phiên bản phụ. Sửa /etc/yum.repos.d/docker-ce.repo để dùng riêng số phiên bản chính sẽ khắc phục lỗi này.
Có thể cài Docker và podman trên cùng một server không?
Tài liệu Docker liệt kê podman và runc là các package xung đột, đồng thời yêu cầu gỡ cả hai trước khi cài Docker Engine. Xung đột cụ thể nằm ở package podman-docker. Package này sở hữu /usr/bin/docker và biến mọi lệnh docker thành lệnh podman. Chạy rpm -qf "$(command -v docker)" để xem package nào sở hữu đường dẫn đó. Nếu output bắt đầu bằng podman-docker, podman đang xử lý lệnh. Docker không hỗ trợ bố cục giữ cả hai engine. Vì vậy, trên server quan trọng, hãy chọn một engine.
Vì sao container của tôi nhận lỗi permission denied trên bind mount?
SELinux được bật enforcement mặc định trên Rocky Linux và AlmaLinux. Container chạy với type container_t và chỉ có thể truy cập các file được gắn nhãn container_file_t. Vì vậy, thư mục bạn tạo có thể mang nhãn sai và bị từ chối truy cập, bất kể owner và mode của thư mục. Xác nhận bằng ls -ldZ trên đường dẫn ở host và sudo ausearch -m avc -ts recent. Lệnh này in avc: denied cùng với hai context không khớp. Thêm :z vào đối số volume cho nội dung được chia sẻ giữa các container, hoặc dùng :Z cho nội dung riêng của một container. Không bao giờ trỏ :Z vào /home hoặc /usr, vì thao tác gắn nhãn lại có tính đệ quy và sẽ làm hỏng host.
Có cần mở port trong firewalld để publish port của container không?
Không, và đó chính là vấn đề. Rule NAT của Docker thay đổi địa chỉ đích trước khi packet đến đường xử lý input của host. Vì vậy, các rule zone của firewalld không kiểm tra được packet này. Docker cũng đưa các bridge của mình vào firewalld zone có tên docker với target ACCEPT. Container khởi động bằng -p 8080:80 vẫn có thể truy cập từ Internet, trong khi sudo firewall-cmd --list-ports không in ra gì. Dùng -p 127.0.0.1:8080:80 để publish đến một địa chỉ cụ thể khi chỉ host cần truy cập service, hoặc chèn các rule lọc vào chain DOCKER-USER. Docker xử lý chain này trước các rule accept của chính nó.
Thêm user vào group docker có an toàn không?
Thao tác này cấp quyền root. Thành viên của group docker có thể ghi vào /var/run/docker.sock. Sau đó, docker run --rm -v /:/host alpine wc -l /host/etc/shadow có thể đọc một file chỉ root được phép đọc từ một account không có quyền sudo. Tài liệu post-install của Docker cũng nêu rõ quyền tương đương này. Chỉ thêm những account mà bạn vốn đã tin cậy như các account có quyền sudo. Với account dùng chung hoặc account chạy service, tiếp tục dùng sudo docker. Rootless mode là lựa chọn thay thế khi bạn cần chạy container dưới một user không đặc quyền. Đây là một quy trình cài đặt riêng, không phải một setting.