AlmaLinux

Install Docker Engine on Rocky Linux 10 / AlmaLinux 10

On a freshly cloned Rocky Linux 10 cloud VM, Docker Engine installed without a single warning and then refused to start. The cause was a kernel mismatch: the install pulled kernel modules for the newest kernel installed on disk while the VM still ran an older one, so the netfilter module Docker needs for its NAT rules wasn’t there. A reboot fixed it, and so did installing the matching modules, but nothing in the install output tells you that you need either.

Original content from computingforgeeks.com - post 63719

This guide shows how to install Docker on Rocky Linux 10 and AlmaLinux 10 from Docker’s RHEL repository, get past that first start, turn on SELinux labels, and keep published ports from slipping past firewalld. We tested the steps on Rocky Linux 10.2 in October 2026 with Docker Engine 29.9.1 and Compose 5.6.0, and for AlmaLinux 10.2 we only checked package resolution in a container.

Prepare the server

Docker itself needs little, so size the server for the containers you will run on it. A fresh Docker 29 install keeps images in the containerd image store under /var/lib/containerd, which held 360 MB after this lab while /var/lib/docker held 172 KB, so that’s the path to give disk space to. You need:

  • Rocky Linux 10 or AlmaLinux 10, 64-bit
  • a user with sudo rights
  • outbound HTTPS to download.docker.com and Docker Hub
  • about 450 MB free for the packages, plus room for images
  • vim for the edit steps (the cloud image has only vi)

Our VM had 2 vCPU, 4 GB of RAM and a 40 GB disk, which is a floor for following along, not a production recommendation. If containers are new to you, getting the engine running is the quick part, and one of the Docker books we recommend covers images, layers and networking faster than error messages will. Running Docker on more than one distro family? The multi-distro Docker install guide puts Ubuntu, Debian and Fedora side by side with this one. Our Rocky cloud image shipped only vim-minimal, which provides vi but no vim command, so install the full editor before you start:

sudo dnf install -y vim-enhanced

That pulled four packages on our VM. If you prefer vi or nano, use it wherever this guide says vim.

Remove packages that conflict

Docker’s RHEL instructions tell you to remove Podman and runc before installing, since containerd.io bundles its own runc. Decide first whether you need Podman, because we found the removal is no longer forced on Rocky 10. With Podman 5.8.2 installed, a dry run of the Docker install resolved nine packages with no conflict, and Podman installed later next to Docker still ran containers, because it uses its own runtime and runc was never installed. On a dedicated Docker server we’d still follow Docker’s instruction, since one runtime is easier to support, and that’s the command below. Removing Podman took 18 packages with it on our VM.

sudo dnf remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine podman runc

On a workstation where you use Podman, drop podman runc from the end and remove only the old Docker names. With none of them installed, dnf answered “No packages marked for removal” and “Nothing to do”, so the command is safe on a fresh host.

Add the Docker repository

Rocky Linux and AlmaLinux 10 still ship dnf 4, so the classic config-manager --add-repo syntax works here, unlike Docker on Fedora, where dnf5 rejects it. The plugin that provides config-manager was already on our cloud image, but install it first on a minimal build where it may be missing.

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo

Use the rhel path, not centos. Docker’s install docs list the centos repo for CentOS Stream only, while the rhel repo’s $releasever resolves to 10 on both rebuilds. You should end up with this file in /etc/yum.repos.d/.

[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://download.docker.com/linux/rhel/$releasever/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://download.docker.com/linux/rhel/gpg

If gpgcheck is 0 in your copy, another script wrote it, so delete the file and add the repo again.

Install Docker Engine on Rocky Linux 10

Install the engine, CLI, containerd and both plugins in one transaction, so every piece comes from the same repo snapshot and the versions match.

sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

dnf imports Docker’s GPG key on the first run and prints its fingerprint, which must read 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35, the value on Docker’s RHEL install page. With -y the key is accepted without a prompt, so either drop -y on this first install and compare before you answer, or read the imported key back afterwards with rpm -qi gpg-pubkey-621e9f35-*, which named “Docker Release (CE rpm)” on our VM. Anything else means you are not talking to Docker’s repo. Look at the dependencies in this excerpt before you move on, because they explain the next section.

Installing:
 containerd.io             x86_64 2.4.2-2.el10           docker-ce-stable  35 M
 docker-buildx-plugin      x86_64 0.38.0-1.el10          docker-ce-stable  18 M
 docker-ce                 x86_64 3:29.9.1-1.el10        docker-ce-stable  26 M
 docker-ce-cli             x86_64 1:29.9.1-1.el10        docker-ce-stable 8.9 M
 docker-compose-plugin     x86_64 5.6.0-1.el10           docker-ce-stable 8.4 M
Installing dependencies:
 container-selinux         noarch 4:2.246.0-1.el10       appstream         57 k
 iptables-nft              x86_64 1.8.11-16.el10_2       appstream        182 k
 kernel-modules            x86_64 6.12.0-211.66.1.el10_2 baseos            42 M
 libnftnl                  x86_64 1.3.0-3.el10           baseos            87 k
 nftables                  x86_64 1:1.1.5-6.el10_2       baseos           447 k
Installing weak dependencies:
 docker-ce-rootless-extras x86_64 29.9.1-1.el10          docker-ce-stable 5.6 M
 kernel-modules-extra      x86_64 6.12.0-211.66.1.el10_2 baseos           3.1 M

Transaction Summary
================================================================================
Install  12 Packages

Total download size: 148 M
Installed size: 429 M

You get twelve packages, 148 MB, and on our VM the install took 64 seconds, including an initramfs rebuild for the new modules. The Rocky 10 cloud image leaves out kernel-modules and kernel-modules-extra, and Docker needs netfilter modules from them, so dnf adds both. On our VM it picked the build for the newest kernel on disk, which was not the kernel the VM was running. Note that kernel-modules-extra, the package that holds the module Docker needs, arrives only as a weak dependency, so a host with install_weak_deps=False in /etc/dnf/dnf.conf skips it.

Start Docker and fix the addrtype error

Docker’s RPMs install the service disabled and stopped. Before you start it, compare the running kernel with the modules dnf just installed, because that decides whether the first start works.

uname -r
rpm -q kernel-modules-extra

If rpm prints “package kernel-modules-extra is not installed”, weak dependencies are off on your host, so install it yourself and then compare again:

sudo dnf install -y kernel-modules-extra

Ours did not match. The VM ran 6.12.0-211.16.1.el10_2.0.1, an older kernel left over from the image, while the modules were built for 6.12.0-211.66.1.el10_2, a newer kernel already installed but not yet booted. If yours differ the same way, starting Docker fails.

sudo systemctl enable --now docker

systemd reports “Job for docker.service failed because the control process exited with error code”, and the journal names the module:

Docker fails to start with the addrtype missing kernel module error on Rocky Linux 10

The two version strings at the top are the whole story: the running kernel and the module package disagree, and the iptables warning at the bottom is what that disagreement looks like to Docker.

Error: “Extension addrtype revision 0 not supported, missing kernel module?”

Docker builds its bridge network by adding NAT rules that match on address type, which needs the xt_addrtype module. That module ships in kernel-modules-extra, and the copy dnf installed belonged to the other kernel, so iptables could not load it and the daemon quit with “Error initializing network controller”. You have two fixes, and we tested both on the same VM, the module install first and the reboot afterwards (the newer kernel already had its own modules from the Docker install):

  • reboot into the newest kernel: one command, plus a reboot window
  • install modules for the running kernel: no reboot, 45 MB of downloads

The reboot is the one you want on most servers. After it, Docker started on its own because the service was already enabled.

sudo systemctl reboot

If you cannot reboot yet, install the module packages built for the kernel you’re running instead. We needed both: with only kernel-modules installed, modprobe xt_addrtype still failed with “Module xt_addrtype not found”.

sudo dnf install -y "kernel-modules-$(uname -r)"
sudo dnf install -y "kernel-modules-extra-$(uname -r)"
sudo systemctl reset-failed docker
sudo systemctl restart docker

reset-failed clears systemd’s restart counter, because the failed starts may have hit the start limit. This route only works while the running kernel’s packages are still in the BaseOS repo. Ours was a current 10.2 kernel; if your image kernel comes from an older minor release, dnf answers “No match for argument” because those packages moved to the vault, and the reboot is your only fix. Plan the reboot anyway, since the next kernel update brings the same mismatch back if the box never boots into it.

Verify the install

Run hello-world once the service is active. It tests the whole chain, from CLI to daemon to Docker Hub to containerd, so one green result rules out the common failures:

sudo docker run --rm hello-world

You should see “Hello from Docker!” after the pull. “Cannot connect to the Docker daemon” means the service is still down, so go back to the journal; a pull timeout means the daemon is up but cannot reach Docker Hub. Then check the engine version and the storage backend.

sudo docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
sudo docker info | grep -E 'Storage Driver|driver-type|Cgroup Version|Firewall Backend'

A fresh install reports containerd’s overlayfs snapshotter and cgroup v2, and the firewall backend reads iptables until firewalld enters the picture. This excerpt is the info grep from our VM before any hardening:

 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Cgroup Version: 2
 Firewall Backend: iptables

The screenshot is the same host at the end of the lab, after the reboot and the hardening below.

Docker Engine with SELinux labels and firewalld zone verified on Rocky Linux 10

Client and server match at 29.9.1, the daemon lists SELinux, docker0 sits in its own firewalld zone and SELinux is still enforcing. The next three sections get you there.

Run Docker as your user

Only root can use /var/run/docker.sock by default, so docker ps as your user fails with “permission denied while trying to connect to the docker API”. The docker group fixes that, at a price you should accept knowingly: its members can start a container that mounts the host’s root filesystem, which makes the group root-equivalent. Add only people who already have sudo.

sudo usermod -aG docker $USER
newgrp docker

newgrp applies to the current shell. Log out and back in so new sessions get the group, then run docker ps; an empty table header means it worked.

Turn on SELinux labels for containers

Rocky Linux enforces SELinux, but Docker’s daemon ships with its own SELinux support off. On our install docker info listed only seccomp and cgroupns, and a container read a file under our home directory with no relabel at all, which tells you SELinux was not confining it. Podman labels containers by default; Docker needs one key. Before you change it, create a page under your home directory and read it from a container, so you have a before and after to compare. This read worked on our VM with the default daemon.

mkdir -p ~/site && echo '<h1>served from the host</h1>' > ~/site/index.html
sudo docker run --rm -v ~/site:/data:ro alpine cat /data/index.html

Now open the daemon config.

sudo vim /etc/docker/daemon.json

Add the settings below, merging them into any existing file. Besides SELinux, they switch logging to the rotating local driver, because json-file never rotates, and turn on live restore, so a daemon restart or a patch upgrade within the same minor release does not stop running containers (Docker does not support it across minor or major upgrades):

{
  "selinux-enabled": true,
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true
}

A JSON error stops the daemon from starting, so validate the file before the restart. json.tool echoes the file when it parses and names the bad line when it does not.

python3 -m json.tool /etc/docker/daemon.json
sudo systemctl restart docker
sudo docker info --format '{{.SecurityOptions}} {{.LoggingDriver}} {{.LiveRestoreEnabled}}'

The output should now include SELinux, the local driver and true. If you see the old options, the restart did not pick up the file.

[name=seccomp,profile=builtin name=selinux name=cgroupns] local true

From here, bind mounts need a relabel. Run the same read again and it fails with “cat: can’t open ‘/data/index.html’: Permission denied”.

sudo docker run --rm -v ~/site:/data:ro alpine cat /data/index.html
sudo ausearch -m AVC -ts recent -i

The audit log shows the denial, trimmed here to the identifying fields:

avc:  denied  { read } for  pid=73546 comm=cat name=index.html dev="sda4" ino=75502352 scontext=system_u:system_r:container_t:s0:c127,c643 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

The container runs as container_t with a private category pair, and the directory still carries user_home_t. Add :Z to the volume flag and Docker relabels it to container_file_t for that container, after which the read works.

sudo docker run --rm -v ~/site:/data:ro,Z alpine cat /data/index.html

Use lowercase :z when several containers share one directory. Never relabel /home, /etc or /var themselves, because the host services that read them would start failing, and never set SELinux to permissive to make a mount work.

Keep published ports behind firewalld

If you publish a port, Docker rewrites its destination in the nat table and forwards it to the container, so a -p port is open to the network even when the public zone has no rule for it. With firewalld running, Docker also puts docker0 in its own docker zone with a forwarding policy. Rocky Linux server installs run firewalld; the cloud image we used had none, so we installed it and restarted Docker so it could register with firewalld.

Install firewalld but leave it stopped for a moment, because the public zone it starts with allows only ssh, cockpit and dhcpv6-client.

sudo dnf install -y firewalld

If sshd listens on a port other than 22, new logins on that port fail once firewalld starts, so open it while the service is still stopped. firewall-offline-cmd writes the saved configuration directly; put your port in place of 2222. We ran exactly this order on the lab VM, and firewall-cmd --list-ports showed 2222/tcp as soon as firewalld came up.

sudo firewall-offline-cmd --add-port=2222/tcp

Start firewalld, restart Docker so it registers with it, and check where docker0 landed.

sudo systemctl enable --now firewalld
sudo systemctl restart docker
sudo firewall-cmd --get-zone-of-interface=docker0

Check that last command’s output, because we got “no zone” both times we ran this sequence. The first time, the next restart (made after we had switched live restore off) created the docker zone and the docker-forwarding policy. The second time, on a freshly installed firewalld with live restore on, our immediate retry failed with “start of the service was attempted too often”, because Docker’s unit allows three starts per minute and we had used them up. After clearing the counter, the restart created the zone with live restore still on, so live restore is not the cause. If you see “no zone”, run the same recovery:

sudo systemctl reset-failed docker && sudo systemctl restart docker

The zone query then printed docker and docker info reported the iptables+firewalld backend.

To see what that means for exposure, we published nginx on 8080, bound a second nginx to loopback on 8082, and ran a Python web server on 8081 with no firewall rule for any of them. From another machine on the LAN, we requested each port with curl -s -o /dev/null -w "%{http_code}", and only Docker’s public port answered:

remote 8080: 200
remote 8081: 000
remote 8082: 000

firewalld refused the host service as it should (curl’s 000 means no HTTP response, whether the packet was rejected or dropped), and it did nothing about the Docker port. The public zone only listed cockpit, dhcpv6-client and ssh, so reading firewall-cmd --list-all tells you nothing about what Docker exposes. Bind anything private to loopback and put a reverse proxy in front. This is the 8082 container from the test.

sudo docker run -d --name web -p 127.0.0.1:8082:80 nginx:alpine

On the host, curl localhost:8082 returned 200, while the LAN request above got no response. Remove the test container to clean up:

sudo docker rm -f web

Leave plain -p 8080:80 for services you mean to publish. For zones, rich rules and the rest of the host’s policy, see our firewalld guide for RHEL-family systems.

Run a Compose stack

Compose ships as a CLI plugin, so docker compose (with a space) is ready to use. A one-service stack that serves a host directory proves the plugin, the SELinux relabel and the loopback port together. Create the project directory, a page for nginx to serve and the Compose file.

mkdir -p ~/app/html && cd ~/app
echo '<h1>Docker on Rocky Linux</h1>' > ~/app/html/index.html
vim ~/app/compose.yaml

Paste the file we ran. The :Z and the 127.0.0.1 bind carry over the two previous sections:

services:
  app:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro,Z

Start it and fetch the page. --wait returns only when the container is up, which makes the curl straight after it reliable in a script:

docker compose up -d --wait
curl -s localhost:8080/
docker compose down

curl printed <h1>Docker on Rocky Linux</h1> and docker compose ps showed 127.0.0.1:8080->80/tcp. For multi-service files, health checks and env files, the Docker Compose guide goes further.

Install Docker on AlmaLinux 10

AlmaLinux 10 uses the same repo and the same commands, and resolves the same Docker builds; Docker’s own docs name RHEL 8, 9 and 10 and do not mention the rebuilds, which is why we checked. We checked package resolution in an AlmaLinux 10.2 container: dnf 4.20.0 accepted --add-repo and resolved docker-ce 29.9.1, containerd.io 2.4.2 and Compose 5.6.0 from the same el10 builds Rocky got, with a newer container-selinux (2.248.0 against Rocky’s 2.246.0). A container image starts without most base packages, so it resolved 29 packages instead of 12, and with no kernel installed it pulled no kernel modules. That is the one part we could not check there. On an AlmaLinux VM, run the uname -r and rpm -q kernel-modules-extra comparison from the start section before you start Docker, because the same mismatch can happen on any image that ships an installed but unbooted kernel.

Script it for more Rocky and Alma hosts

Everything above turns into a short, repeatable sequence once you know where it breaks. On each new Rocky or AlmaLinux 10 host, in this order:

  1. Remove old Docker packages, plus Podman and runc on dedicated Docker servers.
  2. Add the rhel repo with dnf config-manager --add-repo.
  3. Install the five packages and check the GPG fingerprint once.
  4. Compare uname -r with kernel-modules-extra and reboot if they differ.
  5. Write /etc/docker/daemon.json, validate it, then enable and start Docker.
  6. Confirm docker0 sits in the docker firewalld zone.
  7. Publish private services on 127.0.0.1 only.

Step 4 is the one automation misses, because the install succeeds and the failure only shows at start. An Ansible role or a cloud-init script should reboot unconditionally after the install, or compare the two versions and reboot when they differ. Step 6 needs a recheck after firewalld is added to an existing Docker host, since the restart right after firewalld started did not create the zone. Do both, and your Rocky hosts should behave the way our lab VM did; test the first AlmaLinux VM by hand before you trust the role there.

Keep reading

Best UI Applications for Managing Docker Containers Containers Best UI Applications for Managing Docker Containers Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 AlmaLinux Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 Install Docker and Run Containers on Ubuntu 24.04|22.04 Containers Install Docker and Run Containers on Ubuntu 24.04|22.04 CapRover Docker Install on Debian 13: Deploy Your First App Containers CapRover Docker Install on Debian 13: Deploy Your First App Best OpenShift Books for Admins and EX280 Candidates Books Best OpenShift Books for Admins and EX280 Candidates Install Docker & Compose on Rocky Linux 9 Docker Install Docker & Compose on Rocky Linux 9

Leave a Comment

Press ESC to close