Install Docker on AlmaLinux 9 with Docker Engine, Compose v2, and a verified container in about ten minutes. The final smoke test stays on localhost, so you can confirm the daemon and networking without exposing an unnecessary port to the internet.
Prerequisites
- A 64-bit AlmaLinux 9 VPS with at least 2 GB of RAM and 10 GB of free disk space.
- SSH access as
root, or a regular Linux account with workingsudoprivileges. - Outbound HTTPS access to Docker’s RPM repository and Docker Hub.
- No domain is needed for this installation or its local test. Add one later if you publish an application through a TLS reverse proxy.
- A VPSLake Linux VPS if you need an AlmaLinux server with root access.
The commands below use sudo for a normal SSH account. If you are already logged in as root, omit sudo; AlmaLinux 9 uses DNF and normally uses firewalld, not Ubuntu’s apt and ufw tools.
Step 1: Confirm the release and update AlmaLinux
Checking the major release before adding a repository prevents DNF from selecting packages for the wrong Enterprise Linux version.
grep -E '^(PRETTY_NAME|VERSION_ID|ID_LIKE)=' /etc/os-release
uname -m
sudo dnf -y upgrade
The release should identify AlmaLinux 9 and the architecture should normally be x86_64:
PRETTY_NAME="AlmaLinux 9.x"
VERSION_ID="9.x"
ID_LIKE="rhel centos fedora"
x86_64
The minor version changes as AlmaLinux publishes updates. Resolve a DNF mirror, DNS, or GPG-signature error before moving on.
Step 2: Clear packages that conflict with Docker
AlmaLinux can have Podman or a standalone runtime installed already, while Docker’s package set supplies its own containerd and related components.
On a new VPS where those tools are not being used, remove the packages that Docker identifies as conflicts:
sudo dnf remove -y docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine \
podman \
runc
DNF may report that some packages are not installed. That is harmless. Do not remove Podman or runc from an active production server until you have checked which services depend on them; removing packages does not by itself delete existing data in /var/lib/docker/.
Step 3: Add Docker’s RHEL-compatible repository
Docker distributes the AlmaLinux-compatible Engine packages through its RHEL repository, which is the maintained source for this installation.
Install the DNF repository plugin, add the signed repository, and refresh package metadata:
sudo dnf install -y dnf-plugins-core ca-certificates
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf makecache
Before installing, confirm that DNF can see an Enterprise Linux 9 build:
dnf list docker-ce --showduplicates | sort -r | head -n 5
The list should contain docker-ce entries from docker-ce-stable with an el9 suffix. Docker’s RHEL installation guide documents the repository, package names, and service setup.
Step 4: Install Docker Engine and Compose
This package set installs the daemon, command-line client, containerd runtime, Buildx builder, and the current docker compose plugin together.
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
Check that systemd started the daemon and that the installed tools can talk to it:
sudo systemctl is-active docker
sudo systemctl is-enabled docker
sudo docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
sudo docker compose version
Expected output has an active, enabled service and version strings. Exact patch numbers will change as Docker releases updates:
active
enabled
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Step 5: Run Docker’s installation check
The official hello-world image checks the full path from the CLI to the daemon, image registry, container creation, and process startup.
sudo docker run --rm hello-world
Near the end of the output, look for:
Hello from Docker!
Your installation appears to be working correctly.
The container exits after printing the message, and --rm removes its stopped container record. The downloaded image remains in Docker’s local cache.
Step 6: Give one trusted account Docker access
The Docker socket is root-owned by default. Membership in the docker group avoids repeated sudo, but it also gives that account root-equivalent control over the host.
Run this block while logged in as the normal account that should manage Docker:
sudo groupadd --force docker
sudo usermod -aG docker "$USER"
Sign out of SSH completely and reconnect so the new group membership is loaded. Then verify that the account can reach the daemon without sudo:
id -nG
docker info --format 'Server {{.ServerVersion}}'
The group list should include docker, and the second command should print the Engine version. newgrp docker can activate the membership in the current shell, but a fresh SSH login avoids carrying stale group credentials.
Step 7: Test container networking on localhost
A loopback-only web container proves that Docker can publish a port while keeping the test inaccessible from outside the VPS.
docker run -d --name almalinux9-docker-check --publish 127.0.0.1:18080:80 nginx:alpine
docker ps --filter name=almalinux9-docker-check --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
curl --head --max-time 10 http://127.0.0.1:18080
You should see a running container and an HTTP success response:
NAMES STATUS PORTS
almalinux9-docker-check Up ... 127.0.0.1:18080->80/tcp
HTTP/1.1 200 OK
Because the binding uses 127.0.0.1, do not open port 18080 in firewalld. Remove the disposable test after checking it:
docker stop almalinux9-docker-check
docker rm almalinux9-docker-check
Verify Docker on AlmaLinux 9
Run this compact check to confirm boot startup, daemon communication, Compose, and the storage driver:
systemctl is-active docker
systemctl is-enabled docker
docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
docker compose version
docker info --format 'Containers {{.Containers}} | Images {{.Images}} | Storage {{.Driver}}'
Successful output includes active, enabled, Docker client/server versions, a Compose v2 version, and commonly the overlay2 storage driver:
active
enabled
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Containers 0 | Images 2 | Storage overlay2
Docker is ready for a Compose project or application image. For the interaction between published ports, Docker-created rules, and firewalld, read Docker’s packet-filtering and firewalls documentation.
Troubleshooting
No match for argument: docker-ce
DNF has not loaded the Docker repository, or the repository metadata is stale. Confirm the enabled repo and refresh the cache:
sudo dnf repolist --enabled | grep -i docker
sudo dnf makecache
dnf list docker-ce --showduplicates
You should see Docker’s stable repository and an el9 package candidate. If the repo is absent, repeat the dnf config-manager --add-repo command in Step 3.
package podman ... conflicts with ...
Podman or a separate runc package is still installed and owns a file expected by Docker’s package set. On a new VPS, rerun Step 2; on a working host, identify the dependent service and plan the runtime change before removing anything.
permission denied while trying to connect to the Docker daemon socket
The SSH session was started before the account joined the docker group, or the group command was run as a different user. Reconnect and check id -nG; use sudo docker info temporarily or run newgrp docker for the current shell.
curl: (7) Failed to connect to 127.0.0.1 port 18080
The test container may have exited, the image may still be downloading, or another process may already own port 18080. Inspect the container and logs:
docker ps -a --filter name=almalinux9-docker-check
docker logs almalinux9-docker-check
docker port almalinux9-docker-check
If the port is busy, rerun the container with another loopback port such as 127.0.0.1:28080:80 and curl http://127.0.0.1:28080. No public firewall rule is needed for either binding.
Hardening
- Keep Docker administration on its Unix socket. Never expose an unauthenticated Docker API on TCP port 2375; remote access needs deliberate TLS configuration and narrow firewall rules.
- Treat the
dockergroup as administrative access. Use Docker rootless mode when the workload needs a stronger privilege boundary. - Publish internal services on
127.0.0.1and place a TLS reverse proxy in front of them. For a public web service, open only the matching firewalld services:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
- Keep SELinux enabled. For a bind mount, use Docker’s
:Zlabel for a private container mount, such as-v /srv/app:/app:Z, instead of disabling the policy.
FAQ
Does Docker support AlmaLinux 9?
Docker’s documented RHEL procedure lists RHEL 9, and AlmaLinux 9 provides the compatible Enterprise Linux package interfaces used by that repository. The repository uses el9 packages; their patch versions change as Docker publishes updates.
Should I remove Podman before installing Docker?
On a fresh VPS, removing unused Podman and standalone runc packages avoids package conflicts. If Podman is serving workloads, keep it until you have planned a migration because Docker and Podman are separate container engines.
Where does Docker keep its data on AlmaLinux 9?
Docker normally stores images, writable layers, and container metadata under /var/lib/docker, while containerd keeps runtime data under /var/lib/containerd. Monitor the filesystem that holds these paths before pulling large images.
Do I need to open a port for Docker?
No port is needed for the installation or the localhost smoke test. When you publish a real application, expose only its required port and review Docker’s packet-filtering behavior alongside firewalld; a published port is a network decision, not just a container setting.