Cài Docker trên Rocky Linux, AlmaLinux bằng dnf
Cài Docker Engine trên Rocky Linux hoặc AlmaLinux bằng dnf, xử lý lỗi lệnh docker do Podman chiếm và thêm label SELinux cho bind mount.
Cài 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 enable service. Phần này gồm 4 lệnh và giống hệt trên cả hai distribution 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ự.
Quá trình cài đặt khá 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ể đã sở hữu lệnh docker trên image của bạn. SELinux 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 được 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 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.
Podman đã trả lời lệnh docker chưa?
Rocky Linux và AlmaLinux cung cấp podman trong các repository mặc định, nhiều image VPS còn cài sẵn công cụ này. 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. Vì vậy, mọi lệnh docker bạn nhập thực chất sẽ chạy podman, khiến các hướng dẫn dành cho Docker tạo ra output không như dự kiến.
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ỳ lệnh nào:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.Có thể ai đó đã tạo file này để ẩn banner, vì vậy không nên 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 kết quả bắt đầu bằng podman-docker, podman đang trả lời lệnh. Nếu kết quả 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, file này được cài thủ công. Khi đó, hãy đọc script trước khi tin tưởng nó.
Podman chạy được các OCI image tương tự 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. 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 ghi nhận cho RHEL:
sudo dnf remove docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runcTrước khi xác nhận, hãy đọc danh sách các package mà dnf dự định gỡ cùng. 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 để giải phóng tên docker, và gỡ runc, package bị containerd.io thay thế. Tài liệu Docker xem podman là package xung đột, nên Docker không hỗ trợ layout 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ỡ package ở 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. Kiểm tra vào tháng 8 năm 2026, tài liệu của Docker ghi rõ 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, nên 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 dạng lệnh 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 dạng đều ghi cùng một file vào /etc/yum.repos.d/docker-ce.repo. Dạng không đúng 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 biết được ngay.
File repository đó đặt baseurl thành một path có chứa $releasever, rồi dnf mở rộng biến này dựa trên release package của hệ thống. Rocky Linux và AlmaLinux đặt biến này bằng 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 kết quả 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à plugin compose
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 lệnh gọi docker-compose có dấu gạch nối cần được cập nhật thành docker compose có khoảng trắng.
Lần cài đặt đầu tiên sẽ dừng để 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 đối chiếu fingerprint mà dnf in ra với URL đó trước khi chấp nhận key.
Có một lỗi thường gặp đến mức 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 nó, 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ì đây 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 dành cho 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 đó vẫn dừng và kéo theo mọi container cũng dừng.
systemctl status phải hiển thị Active: active (running). Container hello-world phải in ra 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ề docker group bên dưới sẽ xử lý lỗi này.
Kiểm tra riêng compose plugin, vì đây là một package 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. restart policy quyết định các service của Compose có khởi động lại khi boot hay không.
Tại 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. Thư mục 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 bị từ chối dù owner, group và mode nhìn đúng từ phía host. Bạn có thể 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 đường dẫn bên dưới /srv, label này là system_u:object_r:var_t:s0 chứ không phải container_file_t. Sau đó, sudo ausearch -m avc -ts recent in bản ghi audit của kernel. Bản ghi chứa avc: denied { read }, một trường scontext= chỉ ra container_t, và một trường tcontext= chỉ ra label bạn vừa thấy trên thư mục. Toàn bộ nguyên nhân là sự không khớp giữa hai trường đó.
Cách sửa là thêm suffix vào volume argument. Docker sẽ relabel đường dẫn 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 relabel nội dung thành shared, để nhiều container có thể dùng cùng thư mục. :Z viết hoa relabel nội dung thành private và unshared, gắn với một container; khi đó container thứ hai đọc cùng đường dẫn sẽ bị từ chối. Dùng :z cho mọi thư mục mà sidecar hoặc backup container cũng truy cập. Dùng :Z cho thư mục 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 thực hiện đệ quy. Bind-mount một system directory như /home hoặc /usr với :Z sẽ "khiến host không thể hoạt động và bạn có thể phải relabel thủ công các file trên host". Chỉ trỏ các suffix này đến những thư mục bạn tạo cho container, không bao giờ trỏ đến system path.
Trong Compose, suffix được thêm vào cùng chuỗi đó:
services:
web:
image: nginx:alpine
volumes:
- /srv/site:/usr/share/nginx/html:ro,zCó 2 giới hạn dễ gặp. Flag --mount hoàn toàn không thể đặt SELinux label, vì vậy hãy dùng -v khi cần đặt label. Named volume không cần suffix vì Docker tự label các thư mục 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 1 phút: nếu container hoạt động sau đó, vấn đề nằm ở label và :z là cách sửa. Lập tức bật lại bằng sudo setenforce 1. Trên Enterprise Linux, permission denied trên bind mount có 2 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ố thông thường; đây là vấn đề mà các biến PUID và PGID 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. Bây giờ publish một port rồi 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ột máy khác, curl -I http://YOUR_SERVER_IP:8080/ trả về HTTP/1.1 200 OK. Port đó đang mở ra Internet, nhưng firewall không báo gì.
Nguyên nhân nằm ở đường đi của packet. Các rule zone của firewalld lọc traffic được gửi đế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, nên kernel forward packet thay vì chuyển packet cho tiến trình local. Sau đó Docker đặt các bridge interface của mình vào một zone firewalld có tên docker, với target là ACCEPT, đồng thời thêm forwarding policy 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 sửa gọn nhất không cần thêm firewall rule. 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 curl trên máy local trả về HTTP/1.1 200 OK, còn cùng request từ mộ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 coi một -p 8080:80 không kèm địa chỉ là quyết định expose service đó công khai.
Khi cần cho phép service chỉ truy cập được từ một số địa chỉ nhất định, Docker cung cấp sẵn một chain dành cho việc này. DOCKER-USER được xử lý trước các rule accept riêng của Docker, nên rule bạn đặt ở đó 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-USERLấy tên interface từ ip route show default thay vì giả định là eth0, vì các image EL hiện tại 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 phía trên nftables, và các chain của Docker có thể xem được qua lệnh này. Các rule thêm theo cách này sẽ mất sau reboot nếu bạn không lưu chúng, vì vậy hãy ghi chúng vào một systemd unit sau khi đã xác nhận các rule hoạt động đúng.
Docker Engine 28.0, được phát hành vào năm 2025, đã khắc phục một lỗ hổng liên quan: truy cập trực tiếp theo kiểu routed đế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. Hãy hình thành một thói quen vận hành: sau mỗi sudo firewall-cmd --reload, 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, đó là 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ỉ có firewall đứng phía trước NAT path là khác nhau.
Thêm user không phải root vào nhóm docker
Gõ sudo trước mỗi lệnh docker sẽ nhanh chóng trở nên bất tiện. Nhóm docker loại bỏ việc này:
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ó danh sách nhóm riêng, nên thay đổi 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ó nhóm này để bạn kiểm tra ngay. Các phiên SSH mới sẽ tự nhận thay đổi.
Cần hiểu rõ quyền mà nhóm này cấp. Thành viên nhóm có quyền ghi vào /var/run/docker.sock. Bất kỳ tiến trình nào 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 cho thấy điều này có nghĩa là gì:
docker run --rm -v /:/host alpine wc -l /host/etc/shadowLệnh này đọc một file mà chỉ root có thể đọc, từ một account không có quyền sudo. Tài liệu post-install chính thức của Docker cũng nêu rõ điều tương tự: nhóm docker cấp quyền tương đương root. Chỉ thêm account vào nhóm 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 thiết lập user theo nguyên tắc đặc quyền tối thiểu trên VPS thay vì xử lý 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 thấp hơn 1024 hoạt động. Vì vậy, hãy lập kế hoạch cho nó như một dự án riêng, không phải một flag có thể thêm sau.
Bước tiếp theo
Bạn đã có Docker engine, Compose plugin, một service hoạt động sau khi reboot và ba hành vi riêng của EL được ghi lại ở 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 lệnh 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, và Rocky Linux cùng AlmaLinux sẽ mở rộng biến này thành số phiên bản major. Vì vậy, máy EL 9 sẽ ánh xạ đến cây CentOS 9, còn máy EL 10 sẽ ánh xạ đến cây CentOS 10. Xác nhận việc mở rộng bằng sudo dnf repoinfo docker-ce-stable rồi đọc dòng Repo-baseurl. Nếu xuất hiện Status code: 404 khi dnf tải metadata, biến đã được mở rộng thành point release. Sửa /etc/yum.repos.d/docker-ce.repo để dùng trực tiếp số major sẽ khắc phục lỗi này.
Có thể cài Docker và podman trên cùng một máy chủ 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 path đó. Nếu output bắt đầu bằng podman-docker thì podman đang xử lý lệnh. Docker không hỗ trợ bố cục cài đặt giữ cả hai engine, nên trên máy chủ quan trọng, hãy chọn một.
Vì sao container của tôi bị từ chối quyền 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 truy cập các file có label container_file_t. Vì vậy, directory bạn tạo có label không đúng và quyền truy cập bị từ chối bất kể owner và mode của directory. Xác nhận bằng ls -ldZ trên path của host và sudo ausearch -m avc -ts recent. Lệnh này in avc: denied cùng hai context không khớp. Thêm :z vào volume argument cho nội dung được chia sẻ giữa các container, hoặc :Z cho nội dung chỉ dùng bởi một container. Không bao giờ trỏ :Z vào /home hoặc /usr, vì thao tác đổi label là đệ quy và sẽ làm hỏng host.
Có cần mở một port trong firewalld để publish port của container không?
Không, và đó chính là vấn đề. NAT rule của Docker rewrite địa chỉ đích trước khi packet đến input path của host, nên zone rule của firewalld không kiểm tra packet đó. Docker cũng đặt các bridge của nó vào firewalld zone có tên docker với target ACCEPT. Container được khởi động bằng -p 8080:80 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 vào một địa chỉ cụ thể khi chỉ host cần truy cập service, hoặc thêm filtering rule vào chain DOCKER-USER. Docker xử lý chain này trước các accept rule của chính nó.
Thêm user của tôi 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 được file chỉ root mới có quyền đọ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õ sự tương đương này. Chỉ thêm những account mà bạn vốn đã tin cậy với sudo, và tiếp tục dùng sudo docker cho shared account hoặc service account. 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 đường dẫn cài đặt riêng, không phải một setting.