{"id":1055,"date":"2026-11-11T03:00:00","date_gmt":"2026-11-11T03:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1055"},"modified":"2026-11-11T03:00:00","modified_gmt":"2026-11-11T03:00:00","slug":"install-docker-rocky-linux-9","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/11\/install-docker-rocky-linux-9\/","title":{"rendered":"How to Install Docker on Rocky Linux 9 (Step-by-Step)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker on Rocky Linux 9 from Docker\u2019s 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>A 64-bit Rocky Linux 9 VPS, preferably <code>x86_64<\/code>, with at least 2 GB of RAM and 10 GB of free disk space.<\/li>\n\n\n<li>SSH access as <code>root<\/code>, or a regular account that can run commands with <code>sudo<\/code>.<\/li>\n\n\n<li>Outbound HTTPS access to Docker\u2019s package repository and Docker Hub.<\/li>\n\n\n<li>No domain is needed for the installation or local test. Use a domain when you later put a public web application behind HTTPS.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need a Rocky Linux server with root access.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky Linux 9 uses DNF and normally runs firewalld. The commands below use <code>sudo<\/code>; omit it when you are already in a root shell.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Rocky Linux 9 and update the host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release before changing repositories prevents an Enterprise Linux package mismatch.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_ID|ID_LIKE)=' \/etc\/os-release\nuname -m\nsudo dnf upgrade -y\nsudo dnf install -y ca-certificates curl dnf-plugins-core<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command should identify Rocky Linux 9, and the architecture should normally be <code>x86_64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"Rocky Linux 9.x (Blue Onyx)\"\nVERSION_ID=\"9.x\"\nID_LIKE=\"rhel centos fedora\"\nx86_64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Minor-release and package versions change over time. Fix any DNF mirror, DNS, or GPG-signature error before proceeding.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Clear conflicting container packages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf remove -y docker \\\n  docker-client \\\n  docker-client-latest \\\n  docker-common \\\n  docker-latest \\\n  docker-latest-logrotate \\\n  docker-logrotate \\\n  docker-engine \\\n  podman \\\n  runc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>\/var\/lib\/docker\/<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Add Docker\u2019s Rocky-compatible RPM repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky Linux 9 is compatible with the RHEL 9 package interface, so Docker\u2019s RHEL repository provides the maintained Engine packages without an old CentOS-specific workaround.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf config-manager --add-repo https:\/\/download.docker.com\/linux\/rhel\/docker-ce.repo\nsudo dnf makecache\ndnf list docker-ce --showduplicates | sort -r | head -n 6<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The package list should show <code>docker-ce<\/code> entries from <code>docker-ce-stable<\/code> with an <code>el9<\/code> suffix. For the repository and package names, see Docker\u2019s <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">RHEL installation guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Install Engine, Buildx, and Compose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installing the daemon, CLI, runtime, image builder, and Compose plugin together keeps the Docker toolchain aligned.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl enable --now docker<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If DNF asks to accept Docker\u2019s GPG key, confirm that its fingerprint matches the one in Docker\u2019s official instructions before accepting it. Then check the service and client tools:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl is-active docker\nsudo systemctl is-enabled docker\nsudo docker version --format 'Client {{.Client.Version}} \/ Server {{.Server.Version}}'\nsudo docker compose version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output shows an active, enabled service. Exact versions vary as Docker publishes updates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nenabled\nClient 29.x.y \/ Server 29.x.y\nDocker Compose version v2.x.y<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Run the built-in Docker smoke test<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>hello-world<\/code> image checks registry access, the daemon socket, image extraction, container creation, and process startup in one command.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo docker run --rm hello-world<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Near the end, you should see:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Hello from Docker!\nYour installation appears to be working correctly.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The container exits after printing the message. <code>--rm<\/code> removes its stopped container record, but the downloaded image remains cached.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Allow one trusted account to use Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default; adding one administrator to the <code>docker<\/code> group removes repeated <code>sudo<\/code> prompts but grants that account effectively root-level control of the host.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this as the account that should manage containers, replacing <code>YOUR_ADMIN_USER<\/code> with its Linux login name:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo groupadd --force docker\nsudo usermod -aG docker YOUR_ADMIN_USER<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Log out of SSH and reconnect as <code>YOUR_ADMIN_USER<\/code> so the new group is loaded. Then test the socket without <code>sudo<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>id -nG\ndocker info --format 'Server {{.ServerVersion}}'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The group list should contain <code>docker<\/code>, and the second command should print the Engine version. <code>newgrp docker<\/code> can reload group membership in the current terminal, but a new SSH session is less error-prone.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Test a local web container<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A loopback-only port mapping proves Docker networking works while keeping the disposable NGINX service inaccessible from the public network.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker run -d --name rocky9-docker-check --publish 127.0.0.1:18090:80 nginx:alpine\ndocker ps --filter name=rocky9-docker-check --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}'\ncurl --head --max-time 10 http:\/\/127.0.0.1:18090<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The container should be running and curl should return an HTTP success response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAMES               STATUS      PORTS\nrocky9-docker-check Up ...      127.0.0.1:18090-&gt;80\/tcp\nHTTP\/1.1 200 OK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Remove this disposable container after the check:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker stop rocky9-docker-check\ndocker rm rocky9-docker-check<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker on Rocky Linux 9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run this final check as the Docker administrator to confirm startup at boot, daemon communication, Compose, and the storage driver:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-active docker\nsystemctl is-enabled docker\ndocker version --format 'Client {{.Client.Version}} \/ Server {{.Server.Version}}'\ndocker compose version\ndocker info --format 'Containers {{.Containers}} | Images {{.Images}} | Storage {{.Driver}}'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Successful output includes <code>active<\/code>, <code>enabled<\/code>, client and server versions, Compose v2, and commonly <code>overlay2<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nenabled\nClient 29.x.y \/ Server 29.x.y\nDocker Compose version v2.x.y\nContainers 0 | Images 2 | Storage overlay2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Docker automatically creates firewall rules for bridge networking. Read its <a href=\"https:\/\/docs.docker.com\/engine\/network\/packet-filtering-firewalls\/\" target=\"_blank\" rel=\"noopener\">packet-filtering and firewalls documentation<\/a> before changing Docker\u2019s rules or publishing a production service.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Deploy Docker on Rocky 9\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Rocky Linux VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Deploy Docker on Rocky 9<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Get a root-access Rocky Linux VPS for containers, Compose applications, and development work.<\/p>\n\t<\/div>\n\t<a class=\"vl-cta-banner__button\" href=\"\/buy-vps\" aria-label=\"View Linux VPS plans\">View Linux VPS plans<\/a>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><code>No match for argument: docker-ce<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">DNF cannot see Docker\u2019s repository, or the host is not Rocky Linux 9. Confirm the release and repository metadata, then retry the repository command from Step 3:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_ID)=' \/etc\/os-release\nsudo dnf repolist --enabled | grep -i docker\nsudo dnf makecache\ndnf list docker-ce --showduplicates<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The list should include Docker\u2019s stable repository and an <code>el9<\/code> candidate. Avoid switching to a random CentOS repository URL.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>package ... conflicts with ...<\/code> during installation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Podman, <code>runc<\/code>, 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>permission denied while trying to connect to the Docker daemon socket<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The SSH session predates the group change, or <code>usermod<\/code> targeted a different account. Reconnect as <code>YOUR_ADMIN_USER<\/code>, run <code>id -nG<\/code>, and retry. Use <code>sudo docker info<\/code> only as a temporary diagnostic while correcting the group membership.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>curl: (7) Failed to connect to 127.0.0.1 port 18090<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The image may still be downloading, the container may have exited, or another local process may own the port. Inspect the container and listener:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps -a --filter name=rocky9-docker-check\ndocker logs rocky9-docker-check\nsudo ss -ltnp | grep ':18090'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the port is busy, remove the failed test and repeat it with <code>--publish 127.0.0.1:28090:80<\/code>, then request <code>http:\/\/127.0.0.1:28090<\/code>. A loopback binding does not need a public firewalld rule.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardening<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>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.<\/li>\n\n\n<li>Leave SELinux enabled. For a bind mount, use a private <code>:Z<\/code> label such as <code>-v \/srv\/app:\/app:Z<\/code> so the container receives the correct SELinux context.<\/li>\n\n\n<li>Keep internal services bound to <code>127.0.0.1<\/code> until a TLS reverse proxy is ready. For a public web service, open only HTTP and HTTPS in firewalld:<\/li>\n\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>  sudo firewall-cmd --permanent --add-service=http\n  sudo firewall-cmd --permanent --add-service=https\n  sudo firewall-cmd --reload<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Docker and firewalld cooperate through Docker-managed zones and forwarding rules. Do not disable Docker\u2019s iptables\/nftables handling to force a simpler-looking firewall configuration; bridge networking can stop working.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Does Docker support Rocky Linux 9?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Docker publishes RHEL 9 packages, and Rocky Linux 9 provides the compatible Enterprise Linux interfaces used by this repository method. The <code>el9<\/code> build and exact Engine version change as Docker releases updates.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why use Docker\u2019s repository instead of Rocky\u2019s packages?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u2019s own Linux documentation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can Podman and Docker run on the same Rocky VPS?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to open port 18090 in firewalld?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Does Docker support Rocky Linux 9?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Why use Docker\u2019s repository instead of Rocky\u2019s packages?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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\u2019s own Linux documentation.\"}}, {\"@type\": \"Question\", \"name\": \"Can Podman and Docker run on the same Rocky VPS?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Do I need to open port 18090 in firewalld?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker on Rocky Linux 9 from the official RPM repository, add Compose v2, run a container test, and fix common VPS errors on a fresh server.","protected":false},"author":1,"featured_media":1056,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"csco_singular_sidebar":"","csco_page_header_type":"","csco_page_load_nextpost":"","footnotes":""},"categories":[5,9],"tags":[119,118,120,100],"class_list":["post-1055","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-docker","tag-docker-compose","tag-rocky-linux-9","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1055","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/comments?post=1055"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1055\/revisions"}],"predecessor-version":[{"id":3980,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1055\/revisions\/3980"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1056"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1055"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1055"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1055"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}