SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

Install Docker for Rocky Linux and AlmaLinux

Install Docker Engine on Rocky Linux or AlmaLinux with dnf, then fix Podman owning the docker command and SELinux blocking bind mounts.

Install Docker for Rocky Linux and AlmaLinux

To install Docker for Rocky Linux or AlmaLinux, add Docker own dnf repository, install the engine with the compose plugin, then enable the service. That part na four commands, and e dey work the same way 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 too.

The installation short, so most of this guide dey explain wetin Enterprise Linux (EL) do differently from Ubuntu. Podman fit don already own the docker command for your image. SELinux blocks bind-mounted files until dem get the correct label. Firewalld no dey filter Docker published ports, so container port fit open to internet while firewall-cmd dey report say nothing 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. If you add the repository by hand, dnf upgrade go treat Docker like every other package for the system.

Podman don already dey answer the docker command?

Rocky Linux and AlmaLinux get podman for their default repositories, and plenty VPS images dey install am for you. Some images go even install podman-docker, wey dey put a shell script for /usr/bin/docker wey 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 banner. The /usr/bin/docker script dey check for the file /etc/containers/nodocker, and when that file no dey, e go 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 hide 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 na wetin you want, you fit stop here. If you want Docker Engine, remove the packages wey dey conflict 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 runc

Read wetin dnf plan to remove together with am before you confirm. For fresh VPS image, the list short. For server wey person don use before, removing podman fit remove cockpit-podman or another tool wey depend on am.

You fit keep podman together with Docker in principle: remove only podman-docker, so the docker name go dey free, and runc, wey the containerd.io package replaces. Docker documentation treat podman as conflicting package, so Docker no support this arrangement. If the installation still report conflict, use the full 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. For August 2026, Docker documentation list 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.repo

Version 5 of dnf remove the --add-repo argument, so that second command go fail for newer releases. Check the version wey you get, then choose the matching form:

dnf --version

If 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.repo

Both commands dey 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 the problem.

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 dey 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-stable

Read the Repo-baseurl line. E suppose end with /9/x86_64/stable or /10/x86_64/stable. If your release set $releasever to a 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-plugin

Packages five dey, and each one get im own work. docker-ce na the daemon, dockerd. docker-ce-cli na the docker command wey you dey type. containerd.io na the container runtime wey the daemon dey drive. docker-buildx-plugin dey build images. docker-compose-plugin dey provide docker compose as 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 im 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 require container-selinux and nothing provide am, your AppStream repository dey disabled. Run dnf repolist and confirm say appstream dey listed, because na there container-selinux dey ship for EL 9 and EL 10.

Start Docker and confirm say e dey run

sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
sudo docker run --rm hello-world

Docker RPM packages dey leave daemon stopped and disabled after installation. Na why this step dey Docker CentOS page, but e no dey Ubuntu page, where deb go start the service for you. If you skip enable, Docker go run until next reboot. After that, e go stay down and carry every container go down 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. and exit. If e print permission error for /var/run/docker.sock instead, you no add sudo. The docker group section below go fix am.

Check compose plugin separately, because na different package and e fit miss even when engine dey okay:

docker compose version

Healthy answer suppose look like Docker Compose version v2.x.x. Bringing your services back after reboot na separate matter from enabling 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 the parent path give am, and e no be container_file_t. Container go get deny 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.html

Container go print:

cat: can't open '/usr/share/nginx/html/index.html': Permission denied

Two commands go show you the cause. ls -ldZ /srv/site go print the label, wey for path under /srv be system_u:object_r:var_t:s0, no be container_file_t. Then sudo ausearch -m avc -ts recent go print the kernel audit record. E contain avc: denied { read }, an scontext= field wey name container_t, and 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 many 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 read the same path go get deny. 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. Bind-mounting system directory like /home or /usr with :Z "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 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,z

Two limits 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 for bind mount get two separate causes wey look the same from inside the container. One na SELinux label. The other na normal numeric user and group ownership, wey be wetin the PUID and PGID variables dey solve. ls -lnZ go show you the mode, numeric owner, and label for one line, so you fit know which one 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. Now publish one port and check wetin firewalld think say dey open:

sudo docker run -d --name web -p 8080:80 nginx:alpine
sudo firewall-cmd --list-ports

firewall-cmd go print empty line. From another machine, curl -I http://YOUR_SERVER_IP:8080/ go return HTTP/1.1 200 OK. The port dey open to internet, but your firewall no report anything.

The cause na the path wey the packet dey follow. Firewalld zone rules dey filter traffic wey target the host itself. Published port no target the host: Docker installs destination NAT (network address translation) rule wey rewrites the destination to the container address before the packet reach the host input path. So kernel go forward the packet instead of delivering am locally. Docker then puts its bridge interfaces inside firewalld zone called docker, whose target na ACCEPT, and e adds forwarding policy called docker-forwarding wey allows forwarding from any zone into docker zone. Your zone rules no dey see the packet.

The cleanest fix no need firewall rule. Bind the host side of the publish to loopback, then 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 go return HTTP/1.1 200 OK, and the same request from another machine no go connect again. Anything wey no get host address for -p argument go publish on 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 reserves one chain for you. DOCKER-USER dey process before Docker own accept rules, so any rule wey you put there go survive Docker restart and chain rewrite:

sudo iptables -I DOCKER-USER -i enp1s0 ! -s 203.0.113.10 -j DROP
sudo iptables -S DOCKER-USER

Take interface name from ip route show default instead of assuming say na eth0, because current EL images dey use names like enp1s0 or ens3. For Rocky and AlmaLinux, iptables command na compatibility layer over nftables, and Docker chains dey visible 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 dey work.

Docker Engine 28.0, released in 2025, close one related hole: direct routed access to container ports wey dem never publish now dey blocked inside DOCKER chain. This change no affect published ports, so everything above still apply for current versions. One operational habit worth forming: after any sudo firewall-cmd --reload, test the published port again. If e stop answering, sudo systemctl restart docker go reinstall Docker rules.

Ubuntu administrators dey meet the same problem through another tool, wey na why published Docker ports dey ignore ufw rules. NAT path na the cause for both cases. Na only the firewall in front of am dey change.

Add non-root user to docker group

Typing sudo before every docker command dey tire, and docker group fit remove that need:

sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-world

usermod -aG dey edit /etc/group, but your current shell 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 grants. 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 mounts the host filesystem. One command go show wetin that means:

docker run --rm -v /:/host alpine wc -l /host/etc/shadow

That 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 dey grant privileges equivalent to root. Add account to am only if you for 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 deciding am later.

Docker also ships rootless mode wey dey run daemon as unprivileged user. Na separate install path, and e change how storage drivers and ports below 1024 dey behave. So plan am as separate project, instead of flag wey you go add later.

Wey to go next

You don get the engine, the compose plugin, one service wey dey survive reboot, and the three EL-specific behaviours wey we document above. The next step na compose.yaml for each service, and the anatomy of a Compose file explain the file format and the commands wey dey run am. If na your first container host, running Docker on a VPS explain 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 contain $releasever, and Rocky Linux plus AlmaLinux expand am to the major version number. So, an EL 9 box go resolve to the CentOS 9 tree, while an EL 10 box go resolve to the 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 conflicting packages and tell you make you remove both before you install Docker Engine. The direct clash na the 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 one 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 carry wrong label, and access go deny am no matter the owner and mode. Confirm am with ls -ldZ for the host path and sudo ausearch -m avc -ts recent. E go 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 go spoil the host.

I need open port for firewalld before I publish container port?

No, and na that be the problem. Docker NAT rule dey rewrite the destination address before the packet reach the host input path, so firewalld zone rules never inspect am. Docker still put its bridges for firewalld zone wey dem call docker, with target ACCEPT. Container wey start with -p 8080:80 dey reachable from internet, even while sudo firewall-cmd --list-ports print nothing. Publish to specific address with -p 127.0.0.1:8080:80 when na only the host suppose reach the service. Or insert filtering rules for the DOCKER-USER chain, wey Docker process before e apply its own accept rules.

E safe to add my user to the docker group?

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 go already trust with sudo, and continue to use sudo docker for shared or service accounts. Rootless mode na the alternative when you need run containers under unprivileged user. E get separate install path; e no be setting.