Install Docker on Rocky Linux or AlmaLinux with dnf
Install Docker Engine with dnf on Rocky Linux or AlmaLinux, then fix the docker command when Podman owns am and label SELinux bind mounts correctly.
Install Docker for Rocky Linux and AlmaLinux
To install Docker for Rocky Linux or AlmaLinux, add Docker own dnf repository, install the engine together with the compose plugin, then enable the service. That part na four commands, and e dey the same for both distributions because both na rebuilds of Red Hat Enterprise Linux (RHEL) and dem share the same package layout. CentOS Stream dey work the same way. Everything below apply to both, so if you still dey choose between dem, the main things wey go decide na the compatibility promise each project make and whether your older CPU still get support.
The install short, so most of this guide dey cover wetin Enterprise Linux (EL) do differently from Ubuntu. Podman fit already dey use the docker command for your image. SELinux dey block bind-mounted files until dem get the correct label. Firewalld no dey filter Docker published ports, so container port fit dey open to internet while firewall-cmd dey report say nothing dey open.
No use Docker convenience script from get.docker.com. Docker own documentation talk say dem no recommend am for production. E dey rewrite your repository configuration without asking, and you no fit safely run am again to upgrade. When you add the repository by hand, dnf upgrade go treat Docker like every other package for the server. This one also put the engine inside the scope of dnf-automatic, if you get am applying security updates on a timer, so decide early whether you want Docker patched unattended or held back for a maintenance window. Any way wey you choose, upgrade go replace the packaged binary while the old dockerd still dey run, and needs-restarting na the command wey go tell you which services still dey run the code wey you just replace.
Podman don answer the docker command already?
Rocky Linux and AlmaLinux dey ship podman for their default repositories, and plenty VPS images dey install am for you. Some images go further and install podman-docker, wey dey put one shell script for /usr/bin/docker that dey call podman. Every docker command wey you type go run podman instead, so guide wey Docker write go start to produce output wey you no expect.
The first sign na one banner. The /usr/bin/docker script dey check for the file /etc/containers/nodocker, and when that file no dey, e dey print one line before e run anything:
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.Person fit don create that file to silence the banner, so no depend on am alone. Ask the package database which package own the binary:
command -v docker
rpm -qf "$(command -v docker)"Answer wey start with podman-docker mean say podman dey answer. Answer wey start with docker-ce-cli mean say na real Docker. If rpm -qf report say no package own the file, person install am by hand and you suppose read the script before you trust am.
Podman dey run the same OCI images and e reasonable to use. If you want am, stop here. Both na Linux container engines, so if platform decision never settle, e good to know say FreeBSD jails dey isolate complete userland instead of running layered images wey dem pull from registry. If you want Docker Engine, remove the conflicting packages first. Na this list Docker document for RHEL:
sudo dnf remove docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runcRead wetin dnf plan to remove together with am before you confirm. For fresh VPS image, the list short. For system wey person don use before, removing podman fit remove cockpit-podman or another tool wey depend on am.
Keeping podman together with Docker dey possible in principle: remove only podman-docker, so the docker name go free, and runc, wey the containerd.io package dey replace. Docker documentation treats podman as conflicting package, so Docker no support this arrangement. If the installation still report conflict, use the complete removal list above.
Add Docker repository with dnf config-manager
Docker dey publish RPMs for Enterprise Linux for download.docker.com. The repository file dey point to the CentOS tree, wey Rocky Linux and AlmaLinux dey resolve against. Pointing Rocky box to CentOS repository fit look like mistake until you know how both distributions come from the CentOS lineage after Red Hat turn CentOS to Stream for 2020. As checked for August 2026, Docker document this repository for CentOS Stream 9 and CentOS Stream 10.
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repoVersion 5 of dnf remove the --add-repo argument, so that second command go fail for newer releases. Check the one wey you get, then choose the matching form:
dnf --versionIf e print 5.x version, use the subcommand form instead:
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repoBoth commands go write the same file to /etc/yum.repos.d/docker-ce.repo. The wrong form go fail with unknown-argument error instead of doing something silently wrong, so you no go miss am.
That repo file set baseurl to a path wey contain $releasever, and dnf dey expand that variable from your release package. Rocky Linux and AlmaLinux set am to the major version number, so 9 for EL 9 and 10 for EL 10. Na why CentOS repository dey resolve correctly for Rocky box. Confirm the expansion before you install:
sudo dnf repoinfo docker-ce-stableRead the Repo-baseurl line. E suppose end with /9/x86_64/stable or /10/x86_64/stable. If your release set $releasever to point version like 9.6, dnf go report Status code: 404 for that URL when e fetch metadata. Fix am by editing /etc/yum.repos.d/docker-ce.repo and replacing $releasever with the bare major number.
Install the engine and the compose plugin
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginPackages five dey here, and each one get im own work. docker-ce na daemon, dockerd. docker-ce-cli na the docker command wey you dey type. containerd.io na the container runtime wey daemon dey control. docker-buildx-plugin dey build images. docker-compose-plugin dey provide docker compose as a subcommand.
These packages no install hyphenated docker-compose binary. Na Compose v1 be that, and e reach end of life for July 2023. Anything wey dey call docker-compose with hyphen need update to docker compose with space.
The first install go stop to import Docker signing key and show you the fingerprint. The key come from gpgkey=https://download.docker.com/linux/centos/gpg for the repo file wey you just add, so compare the fingerprint wey dnf print with that URL before you accept am.
One failure dey happen often enough make we name am. If dnf report say containerd.io need container-selinux and nothing provide am, your AppStream repository don disable. Run dnf repolist and confirm say appstream dey listed, because na there container-selinux dey ship for EL 9 and EL 10.
Docker start am confirm say e dey run
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
sudo docker run --rm hello-worldDocker RPM packages dey leave the daemon stopped and disabled after installation. Na why this step dey Docker CentOS page but e no dey Ubuntu page, where deb package start the service for you. If you skip enable, Docker go run until the next reboot. After that, e go remain down and carry every container go with am.
systemctl status suppose show Active: active (running). hello-world container suppose print This message shows that your installation appears to be working correctly., then exit. If e print permission error for /var/run/docker.sock instead, you no add sudo. The docker group section below go fix this.
Check the compose plugin separately, because na different package and e fit dey missing even when the engine dey work:
docker compose versionHealthy answer suppose look like Docker Compose version v2.x.x. Bringing your services back after reboot na separate matter from enabling the daemon, and restart policies decide whether Compose services go come back when system boot.
Why bind mount dey give permission denied?
Rocky Linux and AlmaLinux dey run SELinux (Security-Enhanced Linux) for enforcing mode by default. Confirm am with getenforce, wey go print Enforcing.
Docker containers dey run under SELinux type container_t, and that type fit only read and write files wey get label container_file_t. Directory wey you create for host go carry any label wey e inherit from the parent path, and that one no be container_file_t. Container go get denial even when owner, group, and mode look correct from host side. Reproduce am with three commands:
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 go print:
cat: can't open '/usr/share/nginx/html/index.html': Permission deniedTwo commands go show you the cause. ls -ldZ /srv/site go print the label, and for path under /srv, the label na system_u:object_r:var_t:s0, no be container_file_t. Then sudo ausearch -m avc -ts recent go print kernel audit record. The record contain avc: denied { read }, an scontext= field wey name container_t, and a tcontext= field wey name the label wey you just see for the directory. The mismatch between those two fields na the whole problem.
The fix na suffix for the volume argument. Docker go relabel the path for you:
sudo docker run --rm -v /srv/site:/usr/share/nginx/html:ro,z nginx:alpine cat /usr/share/nginx/html/index.html:z for lower case go relabel the content as shared, so multiple containers fit use the same directory. :Z for upper case go relabel am as private and unshared. E tie am to one container, and second container wey dey read the same path go get denial. Use :z for anything wey sidecar or backup container dey also touch. Use :Z for database directory wey one container own.
Docker documentation get warning wey worth repeating, because the relabel dey recursive. If you bind-mount system directory like /home or /usr with :Z, e "renders your host machine inoperable and you may need to relabel the host machine files by hand". Point these suffixes to directories wey you create for the container. Never point dem to system path.
For Compose, put the suffix for the same string:
services:
web:
image: nginx:alpine
volumes:
- /srv/site:/usr/share/nginx/html:ro,zTwo limits dey easy to miss. --mount flag no fit set SELinux label at all, so use -v when you need one. Named volumes no need suffix, because Docker dey label the directories wey e create under /var/lib/docker/volumes by itself.
No turn off SELinux. Use sudo setenforce 0 only as one-minute test. If container work after that, the problem na label and :z na the answer. Put am back with sudo setenforce 1 immediately. For Enterprise Linux, permission denied on bind mount get two separate causes wey look the same from inside container. One na SELinux label. The other na ordinary numeric user and group ownership. Na wetin PUID and PGID variables dey solve this one. ls -lnZ go show mode, numeric owner, and label for one line, so you fit know which problem you dey fight.
Why published port dey reachable when firewalld look closed?
Firewalld na the default firewall for Rocky Linux and AlmaLinux. Check whether e dey run with sudo systemctl is-active firewalld. If you never configure am for this box yet, open SSH and web port with firewalld come first, because the surprise below only make sense once you get working zone ruleset to compare with. Now publish one port and check wetin firewalld think say e open:
sudo docker run -d --name web -p 8080:80 nginx:alpine
sudo firewall-cmd --list-portsfirewall-cmd print empty line. From another machine, curl -I http://YOUR_SERVER_IP:8080/ return HTTP/1.1 200 OK. The port open to internet, but your firewall report nothing.
The cause na the path wey the packet take. Firewalld zone rules filter traffic wey address the host itself. Published port no address the host: Docker install destination NAT (network address translation) rule wey rewrite the destination to the container address before the packet reach the host input path, so the kernel forward the packet instead of delivering am locally. Docker then put im bridge interfaces for firewalld zone wey dem call docker, and add forwarding policy wey dem call docker-forwarding. The policy target na ACCEPT and e allow forwarding from any zone enter docker zone. Your zone rules no ever see the packet.
The cleanest fix no need firewall rule. Bind the host side of the publish to loopback and put reverse proxy in front of am:
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/The local curl return HTTP/1.1 200 OK, and the same request from another machine no connect again. Anything wey no get host address for the -p argument publish for every interface, so treat bare -p 8080:80 as decision to expose that service publicly.
When you need service wey some addresses fit reach but others no fit, Docker reserve one chain for you. DOCKER-USER dey process before Docker own accept rules, so rule wey you put there survive Docker restarting and rewriting im chains:
sudo iptables -I DOCKER-USER -i enp1s0 ! -s 203.0.113.10 -j DROP
sudo iptables -S DOCKER-USERTake interface name from ip route show default instead of assuming eth0, because current EL images use names like enp1s0 or ens3. For Rocky and AlmaLinux, iptables command na compatibility layer over nftables, and you fit see Docker chains through am. Rules wey you add this way go disappear after reboot unless you save dem, so write dem inside systemd unit once you confirm say dem work well.
Docker Engine 28.0, released for 2025, close one related hole: direct routed access to container ports wey never publish now block for DOCKER chain. That change no affect published ports, so everything above still apply for current versions. Form this operational habit: after any sudo firewall-cmd --reload, test published port again. If e stop answer, sudo systemctl restart docker reinstall Docker rules.
Ubuntu administrators face the same problem through different tool, wey be why published Docker ports ignore ufw rules. NAT path na the cause for both cases. Na only the firewall in front of am change.
Add non-root user go the docker group
Typing sudo before every docker command dey tire, and docker group remove that need:
sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-worldusermod -aG dey edit /etc/group, but your current shell don already get its group list, so the change no go apply until you get new shell. newgrp docker dey start shell with the group attached, so you fit test am immediately. New SSH sessions go pick am up by themselves.
Make you understand wetin that group dey grant. Membership gives write access to /var/run/docker.sock, and anything wey fit talk to that socket fit ask the daemon to start container wey mount the host filesystem. One command go show wetin that mean:
docker run --rm -v /:/host alpine wc -l /host/etc/shadowThat command dey read file wey only root fit read, from account wey no get sudo rights. Docker own post-install documentation talk the same thing: docker group grants privileges equivalent to root. Add account to am only if you go also give that account sudo. If you dey set up accounts for new server, decide this together with the rest of your least-privilege user setup for VPS, instead of waiting until later.
Docker also ships rootless mode wey dey run daemon as unprivileged user. Na separate install path, and e changes how storage drivers and ports below 1024 dey behave, so plan am as separate project instead of flag wey you add later.
Wey you fit go next
Now you get the engine, the compose plugin, one service wey fit survive reboot, and the three EL-specific behaviours wey we document above. The next step na one compose.yaml for each service, and how Compose file dey structured cover the file format and the commands wey dey run am. If na your first container host, how to run Docker for VPS cover the sizing, storage, and image-hygiene questions wey this guide no cover.
FAQ
Docker's CentOS repository dey work for Rocky Linux and AlmaLinux?
Yes. Add https://download.docker.com/linux/centos/docker-ce.repo with dnf config-manager. The baseurl for that file get $releasever, and Rocky Linux plus AlmaLinux expand am to the major version number, so EL 9 box go resolve to CentOS 9 tree and EL 10 box go resolve to CentOS 10 tree. Confirm the expansion with sudo dnf repoinfo docker-ce-stable and read the Repo-baseurl line. If you see Status code: 404 when dnf dey fetch metadata, e mean say the variable expand to a point release. Edit /etc/yum.repos.d/docker-ce.repo make e use the bare major number to fix am.
Docker and podman fit dey installed for the same server?
Docker documentation list podman and runc as packages wey dey conflict, and e tell you make you remove both before you install Docker Engine. The actual clash na podman-docker package. E own /usr/bin/docker and turn every docker command to podman command. Run rpm -qf "$(command -v docker)" to see which package own that path. If the output start with podman-docker, podman dey answer. Docker no support keeping both engines for the same layout, so for server wey matter, choose one.
Why my container dey get permission denied for bind mount?
SELinux dey enforce by default for Rocky Linux and AlmaLinux. Containers dey run as type container_t and fit only touch files wey get container_file_t label. So directory wey you create fit get wrong label, and access go deny am no matter who own am or the mode wey e get. Confirm am with ls -ldZ for the host path and sudo ausearch -m avc -ts recent, wey print avc: denied with the two contexts wey no match. Add :z to the volume argument for content wey containers share, or :Z for content wey one container alone dey use. Never point :Z to /home or /usr, because the relabel dey recursive and e go break the host.
I need open port for firewalld before I publish container port?
No, and na that be the problem. Docker's NAT rule dey rewrite the destination address before the packet reach the host input path, so firewalld zone rules never inspect am. Docker also put its bridges for firewalld zone wey dem call docker, with target ACCEPT. Container wey start with -p 8080:80 fit reach internet while sudo firewall-cmd --list-ports no print anything. Publish to specific address with -p 127.0.0.1:8080:80 when na only the host suppose reach the service, or put filtering rules inside DOCKER-USER chain, wey Docker process before its own accept rules.
Adding my user to the docker group safe?
E grant root access. Member of docker group fit write to /var/run/docker.sock, and docker run --rm -v /:/host alpine wc -l /host/etc/shadow fit then read root-only file from account wey no get sudo rights. Docker post-install documentation state the same equivalence. Add only accounts wey you already trust with sudo, and continue using sudo docker for shared or service accounts. Rootless mode na the alternative when you need containers under unprivileged user. E get separate install path; e no be setting.