Rocky Linux 和 AlmaLinux 安装 Docker 指南
在 Rocky Linux 和 AlmaLinux 上用 dnf 安装 Docker Engine 和 Compose 插件,并解决 podman 接管 docker 命令、SELinux 阻止绑定挂载访问等问题。
在 Rocky Linux 和 AlmaLinux 上安装 Docker
在 Rocky Linux 或 AlmaLinux 上安装 Docker 时,需要添加 Docker 自有的 dnf 软件源,安装包含 compose 插件的引擎,然后启用服务。整个过程只需 4 条命令。由于这两个发行版都是 Red Hat Enterprise Linux (RHEL) 的重构版本,并且共用其软件包布局,因此安装步骤完全相同。CentOS Stream 也采用相同方式。
安装过程很短,因此本指南的大部分内容都介绍 Enterprise Linux (EL) 与 Ubuntu 的不同之处。在您的镜像中,Podman 可能已经占用了 docker 命令。绑定挂载的文件必须带有正确的标签,否则 SELinux 会阻止访问。Firewalld 不会过滤 Docker 发布的端口,因此容器端口可能已对互联网开放,但 firewall-cmd 却显示没有开放端口。
不要使用来自 get.docker.com 的 Docker 便捷安装脚本。Docker 官方文档明确表示不建议在生产环境中使用该脚本。脚本会在未询问的情况下重写软件源配置,而且不能安全地重复运行以执行升级。手动添加软件源后,dnf upgrade 会像管理系统上的其他软件包一样管理 Docker。
podman 是否已经接管了 docker 命令?
Rocky Linux 和 AlmaLinux 在默认仓库中提供 podman,许多 VPS 镜像还会预装它。有些镜像还会安装 podman-docker,该软件会在 /usr/bin/docker 放置一个调用 podman 的 shell 脚本。此后,您输入的每个 docker 命令都会改为运行 podman,因此针对 Docker 编写的指南可能会产生意外输出。
最先出现的提示通常是横幅信息。/usr/bin/docker 脚本会检查文件 /etc/containers/nodocker 是否存在。如果该文件不存在,脚本会在运行任何命令前输出一行:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.有人可能创建过该文件以隐藏横幅信息,因此不要只依赖这一点。请通过软件包数据库查询哪个软件包拥有该二进制文件:
command -v docker
rpm -qf "$(command -v docker)"如果输出以 podman-docker 开头,说明 podman 正在响应。如果输出以 docker-ce-cli 开头,说明使用的是真正的 Docker。如果 rpm -qf 报告没有软件包拥有该文件,说明有人手动安装了它。此时应先阅读脚本,再决定是否信任它。
Podman 运行相同的 OCI 镜像,本身是合理的选择。如果您想使用 podman,可以到此为止。如果您想安装 Docker Engine,应先删除冲突的软件包。Docker 针对 RHEL 文档列出的软件包如下:
sudo dnf remove docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runc确认前,请先查看 dnf 计划同时删除哪些内容。在全新的 VPS 镜像上,列表通常很短。在已经被使用过的服务器上,删除 podman 可能会同时删除 cockpit-podman 或其他依赖它的工具。
原则上,可以让 podman 与 Docker 共存:只删除 podman-docker,释放 docker 名称,并删除会被 containerd.io 软件包替换的 runc。Docker 文档将 podman 视为冲突软件包,因此 Docker 不支持这种布局。如果安装过程仍然报告冲突,请使用上面的完整删除列表。
使用 dnf config-manager 添加 Docker 软件源
Docker 在 download.docker.com 发布 Enterprise Linux 的 RPM 包。该软件源文件指向 CentOS 软件源树,Rocky Linux 和 AlmaLinux 会解析到这棵软件源树。经 2026 年 8 月核对,Docker 为 CentOS Stream 9 和 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 版本 5 移除了 --add-repo 参数,因此第二条命令在较新的版本中会失败。先检查已安装的版本,再选择匹配的形式:
dnf --version如果输出的是 5.x 版本,请改用子命令形式:
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repo两种形式都会将同一个文件写入 /etc/yum.repos.d/docker-ce.repo。使用错误形式时会返回未知参数错误,不会静默执行错误操作,因此可以立即发现问题。
该软件源文件会将 baseurl 设置为包含 $releasever 的路径,dnf 会根据 release 软件包展开此变量。Rocky Linux 和 AlmaLinux 会将其设置为主版本号,因此在 EL 9 上是 9,在 EL 10 上是 10。这就是 CentOS 软件源可以在 Rocky 主机上正确解析的原因。安装前确认展开结果:
sudo dnf repoinfo docker-ce-stable读取 Repo-baseurl 行。该行应以 /9/x86_64/stable 或 /10/x86_64/stable 结尾。如果您的发行版将 $releasever 设置为类似 9.6 的次版本号,dnf 获取元数据时会针对该 URL 报告 Status code: 404。编辑 /etc/yum.repos.d/docker-ce.repo,将 $releasever 替换为不带小数的主版本号即可修复。
安装引擎和 Compose 插件
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin这 5 个软件包各自负责一项功能。docker-ce 是守护进程,dockerd。docker-ce-cli 是您输入的 docker 命令。containerd.io 是守护进程驱动的容器运行时。docker-buildx-plugin 用于构建镜像。docker-compose-plugin 将 docker compose 作为子命令提供。
这些软件包不会安装带连字符的 docker-compose 二进制文件。那是 Compose v1,该版本已于 2023 年 7 月终止支持。凡是使用带连字符的 docker-compose 的内容,都需要更新为使用带空格的 docker compose。
首次安装会暂停,以导入 Docker 的签名密钥,并显示其指纹。该密钥来自您刚添加的仓库文件中的 gpgkey=https://download.docker.com/linux/centos/gpg,因此在接受密钥前,请将 dnf 输出的指纹与该 URL 进行比较。
有一种故障出现得非常频繁,值得单独说明。如果 dnf 报告 containerd.io 需要 container-selinux,但没有任何软件包提供该依赖项,则表示您的 AppStream 仓库已禁用。运行 dnf repolist,并确认列表中包含 appstream,因为在 EL 9 和 EL 10 上,container-selinux 就由该仓库提供。
启动 Docker 并确认其正常运行
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
sudo docker run --rm hello-worldDocker 的 RPM 软件包安装后不会启动或启用 daemon。因此,Docker 的 CentOS 页面包含此步骤,而 Ubuntu 页面没有,因为 deb 软件包会自动为您启动服务。跳过 enable 后,Docker 会持续运行到下一次重启;重启后它将保持停止状态,并导致所有容器一并停止。
systemctl status 应显示 Active: active (running)。hello-world 容器应输出 This message shows that your installation appears to be working correctly.,然后退出。如果它改为在 /var/run/docker.sock 上输出权限错误,说明您遗漏了 sudo。下面的 docker 组部分会解决此问题。
请单独检查 compose 插件,因为它属于不同的软件包。即使 engine 正常运行,该插件也可能缺失:
docker compose version正常结果应类似 Docker Compose version v2.x.x。重启后恢复服务与启用 daemon 是两个不同的问题;重启策略决定 Compose 服务是否会在启动时恢复。
为什么 bind mount 会提示 permission denied?
Rocky Linux 和 AlmaLinux 默认以 enforcing 模式运行 SELinux(Security-Enhanced Linux)。使用 getenforce 确认;该命令会输出 Enforcing。
Docker 容器以 SELinux 类型 container_t 运行。该类型只能读写标记为 container_file_t 的文件。您在主机上创建的目录会继承其父路径提供的标签,而该标签不是 container_file_t。即使从主机侧看所有者、组和权限模式都正确,容器仍会被拒绝访问。使用以下 3 条命令复现:
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.html容器会输出:
cat: can't open '/usr/share/nginx/html/index.html': Permission denied使用以下 2 条命令即可找出原因。ls -ldZ /srv/site 会输出标签。对于 /srv 下的路径,标签是 system_u:object_r:var_t:s0,而不是 container_file_t。然后,sudo ausearch -m avc -ts recent 会输出内核的审计记录,其中包含 avc: denied { read }、用于标识 container_t 的 scontext= 字段,以及用于标识您刚才在目录上看到的标签的 tcontext= 字段。这两个字段不匹配,就是全部原因。
修复方法是在卷参数末尾添加后缀。Docker 会自动为路径重新标记:
sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro,z nginx:alpine cat /usr/share/nginx/html/index.html小写的 :z 会将内容重新标记为 shared,因此多个容器可以使用同一目录。大写的 :Z 会将内容重新标记为 private 和 unshared,并与一个容器绑定;此时,第二个容器读取同一路径会被拒绝。凡是 sidecar 或备份容器也需要访问的目录,都使用 :z。数据库目录由单个容器独占时,使用 :Z。
Docker 文档有一条值得重复的警告,因为重新标记操作是递归的。使用 :Z 对系统目录(例如 /home 或 /usr)执行 bind mount,会“使主机无法运行,您可能需要手动重新标记主机文件”。这些后缀只能用于为容器创建的目录,不能用于系统路径。
在 Compose 中,将后缀添加到同一个字符串末尾:
services:
web:
image: nginx:alpine
volumes:
- /srv/site:/usr/share/nginx/html:ro,z有两个容易误用的限制。--mount 标志根本不能设置 SELinux 标签,因此需要设置标签时应使用 -v。命名卷不需要后缀,因为 Docker 会自行标记其在 /var/lib/docker/volumes 下创建的目录。
不要关闭 SELinux。仅将 sudo setenforce 0 作为 1 分钟测试使用:如果容器随后可以正常运行,说明问题出在标签上,解决方法是 :z。测试后立即使用 sudo setenforce 1 恢复。在 Enterprise Linux 中,bind mount 的 permission denied 有两个彼此独立、但在容器内表现相同的原因。一个是 SELinux 标签。另一个是普通的数字用户和组所有权,这正是 PUID 和 PGID 变量要解决的问题。ls -lnZ 会在一行中显示权限模式、数字所有者和标签,因此您可以判断实际遇到的是哪一种问题。
为什么发布的端口在 firewalld 看似关闭时仍可访问?
Firewalld 是 Rocky Linux 和 AlmaLinux 的默认防火墙。使用 sudo systemctl is-active firewalld 确认它正在运行。现在发布一个端口,然后查看 firewalld 认为哪些端口已开放:
sudo docker run -d --name web -p 8080:80 nginx:alpine
sudo firewall-cmd --list-portsfirewall-cmd 输出空行。在另一台机器上,curl -I http://YOUR_SERVER_IP:8080/ 返回 HTTP/1.1 200 OK。该端口已对互联网开放,但防火墙没有报告任何信息。
原因在于数据包经过的路径。Firewalld 的区域规则会过滤发往主机自身的流量。发布的端口并不是发往主机的端口:Docker 会安装目标 NAT(网络地址转换)规则,在数据包到达主机的 input 路径前,将目标地址改写为容器地址。因此,内核会转发该数据包,而不是在本地交付它。随后,Docker 会将其网桥接口放入名为 docker 的 firewalld 区域,该区域的 target 为 ACCEPT,并添加名为 docker-forwarding 的转发策略,允许从任意区域向 docker 区域转发。你的区域规则根本看不到这个数据包。
最简洁的修复方法是不添加防火墙规则。将发布端的主机地址绑定到 loopback,再在其前面放置反向代理:
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/本机执行 curl 会返回 HTTP/1.1 200 OK,从另一台机器发送相同请求时则无法连接。-p 参数中不包含主机地址时,服务会发布到所有接口,因此应将不带主机地址的 -p 8080:80 视为公开暴露该服务的决定。
如果确实需要让某些地址可以访问服务,而其他地址不能访问,Docker 会为此保留一个链。DOCKER-USER 会在 Docker 自身的 accept 规则之前处理,因此放置在其中的规则在 Docker 重启并重写其链后仍会保留:
sudo iptables -I DOCKER-USER -i enp1s0 ! -s 203.0.113.10 -j DROP
sudo iptables -S DOCKER-USER通过 ip route show default 获取接口名称,不要假定它是 eth0,因为当前的 EL 镜像会使用 enp1s0 或 ens3 这类名称。在 Rocky 和 AlmaLinux 上,iptables 命令是 nftables 的兼容层,Docker 的链可以通过它查看。通过这种方式添加的规则在重启后会丢失,除非保存它们。因此,确认规则无误后,应将其写入 systemd 单元。
2025 年发布的 Docker Engine 28.0 修复了另一个相关漏洞:对于从未发布的容器端口,当前已在 DOCKER 链中阻止直接路由访问。此更改不影响已发布的端口,因此上述内容仍适用于当前版本。建议养成一个运维习惯:每次执行 sudo firewall-cmd --reload 后,都重新测试已发布的端口。如果端口停止响应,sudo systemctl restart docker 会重新安装 Docker 的规则。
Ubuntu 管理员会通过另一个工具遇到同样的问题,具体原因见为什么已发布的 Docker 端口会忽略 ufw 规则。两种情况下,原因都是 NAT 路径。变化的只有其前置防火墙。
将非 root 用户添加到 docker 组
每次在 docker 命令前输入 sudo 都很麻烦,docker 组可以避免这一点:
sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-worldusermod -aG 会编辑 /etc/group,但当前 shell 已经加载了自己的组列表,因此更改不会立即生效,必须重新获取组列表。newgrp docker 会启动一个包含该组的新 shell,便于立即测试。之后建立的新 SSH 会话会自动获取该组。
请明确该组授予的权限。加入该组后,用户可以写入 /var/run/docker.sock。任何能够访问该套接字的进程,都可以请求 daemon 启动一个挂载主机文件系统的容器。下面的命令可以说明这意味着什么:
docker run --rm -v /:/host alpine wc -l /host/etc/shadow该命令从一个没有 sudo 权限的账户读取只有 root 才能读取的文件。Docker 的安装后文档也明确说明:docker 组授予的权限等同于 root。只有在你也愿意向该账户授予 sudo 时,才应将其添加到该组。如果你正在新服务器上配置账户,应将此决定与 VPS 上的最小权限用户配置 一并考虑,而不是事后再处理。
Docker 还提供 rootless 模式,可以让 daemon 以非特权用户身份运行。这是单独的安装路径,并会改变存储驱动程序以及 1024 以下端口的行为。因此,应将其作为独立项目规划,而不是事后再添加的一个标志。
下一步
现在,您已经完成引擎和 Compose 插件的安装,配置了可在重启后继续运行的服务,并记录了上述 3 个 EL 特有行为。下一步是为每项服务创建一个 compose.yaml;Compose 文件的结构介绍文件格式及用于操作它的命令。如果这是您的第一台容器主机,在 VPS 上运行 Docker将介绍本指南未涵盖的规格、存储和镜像维护问题。
FAQ
Docker 的 CentOS 软件源能否在 Rocky Linux 和 AlmaLinux 上使用?
可以。使用 dnf config-manager 添加 https://download.docker.com/linux/centos/docker-ce.repo。该文件中的 baseurl 包含 $releasever,Rocky Linux 和 AlmaLinux 会将其展开为主版本号。因此,EL 9 主机会解析到 CentOS 9 软件树,EL 10 主机会解析到 CentOS 10 软件树。使用 sudo dnf repoinfo docker-ce-stable 确认展开结果,并读取 Repo-baseurl 行。dnf 获取元数据时出现 Status code: 404,表示该变量展开成了次版本号。将 /etc/yum.repos.d/docker-ce.repo 修改为使用纯主版本号即可修复。
Docker 和 podman 能否安装在同一台服务器上?
Docker 文档将 podman 和 runc 列为冲突的软件包,并要求在安装 Docker Engine 前删除这两个包。实际冲突来自 podman-docker 软件包。该软件包拥有 /usr/bin/docker,并会将所有 docker 命令转换为 podman 命令。运行 rpm -qf "$(command -v docker)" 查看该路径归哪个软件包所有。如果输出以 podman-docker 开头,说明实际响应命令的是 podman。Docker 不支持同时保留这两个引擎,因此在重要服务器上应选择其中一个。
为什么容器在 bind mount 上出现 permission denied?
Rocky Linux 和 AlmaLinux 默认启用 SELinux 强制策略。容器以 container_t 类型运行,只能访问标记为 container_file_t 的文件。因此,您创建的目录会带有错误的标签。即使所有者和权限模式正确,访问仍会被拒绝。对主机路径运行 ls -ldZ,并运行 sudo ausearch -m avc -ts recent 进行确认。后者会输出 avc: denied 以及两个不匹配的安全上下文。对于容器之间共享的内容,在卷参数中添加 :z;对于仅供一个容器使用的内容,添加 :Z。不要将 :Z 指向 /home 或 /usr,因为重新标记会递归执行,并会破坏主机。
发布容器端口时,是否需要在 firewalld 中开放端口?
不需要,这正是问题所在。Docker 的 NAT 规则会在数据包到达主机输入路径前改写目标地址,因此 firewalld 的区域规则不会检查该数据包。Docker 还会将其网桥放入名为 docker、目标为 ACCEPT 的 firewalld 区域。使用 -p 8080:80 启动的容器可以从互联网访问,而 sudo firewall-cmd --list-ports 不会输出任何内容。仅允许主机访问服务时,使用 -p 127.0.0.1:8080:80 绑定到特定地址。也可以将过滤规则插入 DOCKER-USER 链,Docker 会在处理自身的允许规则前先处理该链。
将我的用户添加到 docker 组是否安全?
这相当于授予 root 权限。docker 组成员可以写入 /var/run/docker.sock,随后 docker run --rm -v /:/host alpine wc -l /host/etc/shadow 可以从没有 sudo 权限的账户读取仅 root 可访问的文件。Docker 的安装后文档也明确说明了这一等价关系。仅将您已经信任其使用 sudo 的账户添加到该组。共享账户或服务账户仍应使用 sudo docker。如果需要让非特权用户运行容器,可以使用 rootless 模式。它是独立的安装路径,而不是一个设置项。