Install Docker on Rocky Linux 9 from Docker’s RPM repository and leave the VPS ready for Compose projects. In about ten minutes, you can verify the daemon, grant one account Docker access, and test container networking without publishing a port to the internet.
Prerequisites
- A 64-bit Rocky Linux 9 VPS, preferably
x86_64, with at least 2 GB of RAM and 10 GB of free disk space. - SSH access as
root, or a regular account that can run commands withsudo. - Outbound HTTPS access to Docker’s package repository and Docker Hub.
- No domain is needed for the installation or local test. Use a domain when you later put a public web application behind HTTPS.
- A VPSLake Linux VPS if you need a Rocky Linux server with root access.
Rocky Linux 9 uses DNF and normally runs firewalld. The commands below use sudo; omit it when you are already in a root shell.
Step 1: Confirm Rocky Linux 9 and update the host
Checking the release before changing repositories prevents an Enterprise Linux package mismatch.
grep -E '^(PRETTY_NAME|VERSION_ID|ID_LIKE)=' /etc/os-release
uname -m
sudo dnf upgrade -y
sudo dnf install -y ca-certificates curl dnf-plugins-core
The first command should identify Rocky Linux 9, and the architecture should normally be x86_64:
PRETTY_NAME="Rocky Linux 9.x (Blue Onyx)"
VERSION_ID="9.x"
ID_LIKE="rhel centos fedora"
x86_64
Minor-release and package versions change over time. Fix any DNF mirror, DNS, or GPG-signature error before proceeding.
Step 2: Clear conflicting container packages
The official Docker packages include their expected containerd and runtime components, so remove distribution packages that can claim the same files on a fresh server.
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 names are not installed; that is fine. On a server already running Podman, inspect its services before removing it. Uninstalling the packages does not automatically erase Docker data stored in /var/lib/docker/.
Step 3: Add Docker’s Rocky-compatible RPM repository
Rocky Linux 9 is compatible with the RHEL 9 package interface, so Docker’s RHEL repository provides the maintained Engine packages without an old CentOS-specific workaround.
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf makecache
dnf list docker-ce --showduplicates | sort -r | head -n 6
The package list should show docker-ce entries from docker-ce-stable with an el9 suffix. For the repository and package names, see Docker’s RHEL installation guide.
Step 4: Install Engine, Buildx, and Compose
Installing the daemon, CLI, runtime, image builder, and Compose plugin together keeps the Docker toolchain aligned.
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
If DNF asks to accept Docker’s GPG key, confirm that its fingerprint matches the one in Docker’s official instructions before accepting it. Then check the service and client tools:
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 shows an active, enabled service. Exact versions vary as Docker publishes updates:
active
enabled
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Step 5: Run the built-in Docker smoke test
The hello-world image checks registry access, the daemon socket, image extraction, container creation, and process startup in one command.
sudo docker run --rm hello-world
Near the end, you should see:
Hello from Docker!
Your installation appears to be working correctly.
The container exits after printing the message. --rm removes its stopped container record, but the downloaded image remains cached.
Step 6: Allow one trusted account to use Docker
The Docker socket is root-owned by default; adding one administrator to the docker group removes repeated sudo prompts but grants that account effectively root-level control of the host.
Run this as the account that should manage containers, replacing YOUR_ADMIN_USER with its Linux login name:
sudo groupadd --force docker
sudo usermod -aG docker YOUR_ADMIN_USER
Log out of SSH and reconnect as YOUR_ADMIN_USER so the new group is loaded. Then test the socket without sudo:
id -nG
docker info --format 'Server {{.ServerVersion}}'
The group list should contain docker, and the second command should print the Engine version. newgrp docker can reload group membership in the current terminal, but a new SSH session is less error-prone.
Step 7: Test a local web container
A loopback-only port mapping proves Docker networking works while keeping the disposable NGINX service inaccessible from the public network.
docker run -d --name rocky9-docker-check --publish 127.0.0.1:18090:80 nginx:alpine
docker ps --filter name=rocky9-docker-check --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
curl --head --max-time 10 http://127.0.0.1:18090
The container should be running and curl should return an HTTP success response:
NAMES STATUS PORTS
rocky9-docker-check Up ... 127.0.0.1:18090->80/tcp
HTTP/1.1 200 OK
Remove this disposable container after the check:
docker stop rocky9-docker-check
docker rm rocky9-docker-check
Verify Docker on Rocky Linux 9
Run this final check as the Docker administrator to confirm startup at boot, 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, client and server versions, Compose v2, and commonly overlay2:
active
enabled
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Containers 0 | Images 2 | Storage overlay2
Docker automatically creates firewall rules for bridge networking. Read its packet-filtering and firewalls documentation before changing Docker’s rules or publishing a production service.
Troubleshooting
No match for argument: docker-ce
DNF cannot see Docker’s repository, or the host is not Rocky Linux 9. Confirm the release and repository metadata, then retry the repository command from Step 3:
grep -E '^(PRETTY_NAME|VERSION_ID)=' /etc/os-release
sudo dnf repolist --enabled | grep -i docker
sudo dnf makecache
dnf list docker-ce --showduplicates
The list should include Docker’s stable repository and an el9 candidate. Avoid switching to a random CentOS repository URL.
package ... conflicts with ... during installation
Podman, runc, or a distribution Docker package is still installed and owns a file required by the official package set. On a new VPS, rerun Step 2; on an active host, identify the service that depends on the package before removing it.
permission denied while trying to connect to the Docker daemon socket
The SSH session predates the group change, or usermod targeted a different account. Reconnect as YOUR_ADMIN_USER, run id -nG, and retry. Use sudo docker info only as a temporary diagnostic while correcting the group membership.
curl: (7) Failed to connect to 127.0.0.1 port 18090
The image may still be downloading, the container may have exited, or another local process may own the port. Inspect the container and listener:
docker ps -a --filter name=rocky9-docker-check
docker logs rocky9-docker-check
sudo ss -ltnp | grep ':18090'
If the port is busy, remove the failed test and repeat it with --publish 127.0.0.1:28090:80, then request http://127.0.0.1:28090. A loopback binding does not need a public firewalld rule.
Hardening
- Keep the Docker API on its Unix socket. Never expose an unauthenticated daemon on TCP port 2375; remote access requires deliberate TLS configuration and narrow source restrictions.
- Leave SELinux enabled. For a bind mount, use a private
:Zlabel such as-v /srv/app:/app:Zso the container receives the correct SELinux context. - Keep internal services bound to
127.0.0.1until a TLS reverse proxy is ready. For a public web service, open only HTTP and HTTPS in firewalld:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
- Docker and firewalld cooperate through Docker-managed zones and forwarding rules. Do not disable Docker’s iptables/nftables handling to force a simpler-looking firewall configuration; bridge networking can stop working.
FAQ
Does Docker support Rocky Linux 9?
Docker publishes RHEL 9 packages, and Rocky Linux 9 provides the compatible Enterprise Linux interfaces used by this repository method. The el9 build and exact Engine version change as Docker releases updates.
Why use Docker’s repository instead of Rocky’s packages?
The Docker repository supplies the Docker Engine package family together with containerd, Buildx, and the Compose plugin. That keeps the command names and release cadence consistent with Docker’s own Linux documentation.
Can Podman and Docker run on the same Rocky VPS?
They can coexist in some configurations, but both add container tooling and may compete for runtime packages. Use one deliberately; if an existing Podman workload matters, inventory it before following the removal step.
Do I need to open port 18090 in firewalld?
No. The test binds to loopback, so only processes on the VPS can reach it. For a real public container, publish only the required ports, keep SSH restricted, and use HTTPS for web traffic.