{"id":1045,"date":"2026-11-09T18:00:00","date_gmt":"2026-11-09T18:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1045"},"modified":"2026-11-09T18:00:00","modified_gmt":"2026-11-09T18:00:00","slug":"install-docker-almalinux-9","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/09\/install-docker-almalinux-9\/","title":{"rendered":"How to Install Docker on AlmaLinux 9 (Step-by-Step)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/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 AlmaLinux 9 VPS 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 Linux account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Docker&#8217;s RPM repository and Docker Hub.<\/li>\n\n\n<li>No domain is needed for this installation or its local test. Add one later if you publish an application through a TLS reverse proxy.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need an AlmaLinux server with root access.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands below use <code>sudo<\/code> for a normal SSH account. If you are already logged in as <code>root<\/code>, omit <code>sudo<\/code>; AlmaLinux 9 uses DNF and normally uses firewalld, not Ubuntu&#8217;s <code>apt<\/code> and <code>ufw<\/code> tools.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm the release and update AlmaLinux<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the major release before adding a repository prevents DNF from selecting packages for the wrong Enterprise Linux version.<\/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 -y upgrade<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The release should identify AlmaLinux 9 and the architecture should normally be <code>x86_64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"AlmaLinux 9.x\"\nVERSION_ID=\"9.x\"\nID_LIKE=\"rhel centos fedora\"\nx86_64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The minor version changes as AlmaLinux publishes updates. Resolve a DNF mirror, DNS, or GPG-signature error before moving on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Clear packages that conflict with Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">AlmaLinux can have Podman or a standalone runtime installed already, while Docker&#8217;s package set supplies its own containerd and related components.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On a new VPS where those tools are not being used, remove the packages that Docker identifies as conflicts:<\/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 packages are not installed. That is harmless. Do not remove Podman or <code>runc<\/code> from an active production server until you have checked which services depend on them; removing packages does not by itself delete existing data in <code>\/var\/lib\/docker\/<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Add Docker&#8217;s RHEL-compatible repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker distributes the AlmaLinux-compatible Engine packages through its RHEL repository, which is the maintained source for this installation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Install the DNF repository plugin, add the signed repository, and refresh package metadata:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y dnf-plugins-core ca-certificates\nsudo dnf config-manager --add-repo https:\/\/download.docker.com\/linux\/rhel\/docker-ce.repo\nsudo dnf makecache<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Before installing, confirm that DNF can see an Enterprise Linux 9 build:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dnf list docker-ce --showduplicates | sort -r | head -n 5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The list should contain <code>docker-ce<\/code> entries from <code>docker-ce-stable<\/code> with an <code>el9<\/code> suffix. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">RHEL installation guide<\/a> documents the repository, package names, and service setup.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Install Docker Engine and Compose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This package set installs the daemon, command-line client, containerd runtime, Buildx builder, and the current <code>docker compose<\/code> plugin together.<\/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\">Check that systemd started the daemon and that the installed tools can talk to it:<\/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 has an active, enabled service and version strings. Exact patch numbers will change as Docker releases 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 Docker&#8217;s installation check<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The official <code>hello-world<\/code> image checks the full path from the CLI to the daemon, image registry, container creation, and process startup.<\/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 of the output, look for:<\/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, and <code>--rm<\/code> removes its stopped container record. The downloaded image remains in Docker&#8217;s local cache.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Give one trusted account Docker access<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default. Membership in the <code>docker<\/code> group avoids repeated <code>sudo<\/code>, but it also gives that account root-equivalent control over the host.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this block while logged in as the normal account that should manage Docker:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo groupadd --force docker\nsudo usermod -aG docker \"$USER\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Sign out of SSH completely and reconnect so the new group membership is loaded. Then verify that the account can reach the daemon 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 include <code>docker<\/code>, and the second command should print the Engine version. <code>newgrp docker<\/code> can activate the membership in the current shell, but a fresh SSH login avoids carrying stale group credentials.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Test container networking on localhost<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A loopback-only web container proves that Docker can publish a port while keeping the test inaccessible from outside the VPS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker run -d --name almalinux9-docker-check --publish 127.0.0.1:18080:80 nginx:alpine\ndocker ps --filter name=almalinux9-docker-check --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}'\ncurl --head --max-time 10 http:\/\/127.0.0.1:18080<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see a running container and an HTTP success response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAMES                    STATUS        PORTS\nalmalinux9-docker-check  Up ...        127.0.0.1:18080-&gt;80\/tcp\nHTTP\/1.1 200 OK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because the binding uses <code>127.0.0.1<\/code>, do not open port 18080 in firewalld. Remove the disposable test after checking it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker stop almalinux9-docker-check\ndocker rm almalinux9-docker-check<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker on AlmaLinux 9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run this compact check to confirm boot startup, 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>, Docker client\/server versions, a Compose v2 version, and commonly the <code>overlay2<\/code> storage driver:<\/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 is ready for a Compose project or application image. For the interaction between published ports, Docker-created rules, and firewalld, read Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/network\/packet-filtering-firewalls\/\" target=\"_blank\" rel=\"noopener\">packet-filtering and firewalls documentation<\/a>.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Deploy Docker on AlmaLinux\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">AlmaLinux VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Deploy Docker on AlmaLinux<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Get an AlmaLinux VPS with root access for containers, APIs, and development workloads.<\/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 has not loaded the Docker repository, or the repository metadata is stale. Confirm the enabled repo and refresh the cache:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo 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\">You should see Docker&#8217;s stable repository and an <code>el9<\/code> package candidate. If the repo is absent, repeat the <code>dnf config-manager --add-repo<\/code> command in Step 3.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>package podman ... conflicts with ...<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Podman or a separate <code>runc<\/code> package is still installed and owns a file expected by Docker&#8217;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.<\/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 was started before the account joined the <code>docker<\/code> group, or the group command was run as a different user. Reconnect and check <code>id -nG<\/code>; use <code>sudo docker info<\/code> temporarily or run <code>newgrp docker<\/code> for the current shell.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>curl: (7) Failed to connect to 127.0.0.1 port 18080<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps -a --filter name=almalinux9-docker-check\ndocker logs almalinux9-docker-check\ndocker port almalinux9-docker-check<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the port is busy, rerun the container with another loopback port such as <code>127.0.0.1:28080:80<\/code> and curl <code>http:\/\/127.0.0.1:28080<\/code>. No public firewall rule is needed for either binding.<\/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 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.<\/li>\n\n\n<li>Treat the <code>docker<\/code> group as administrative access. Use Docker rootless mode when the workload needs a stronger privilege boundary.<\/li>\n\n\n<li>Publish internal services on <code>127.0.0.1<\/code> and place a TLS reverse proxy in front of them. For a public web service, open only the matching firewalld services:<\/li>\n\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo firewall-cmd --permanent --add-service=http\nsudo firewall-cmd --permanent --add-service=https\nsudo firewall-cmd --reload<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Keep SELinux enabled. For a bind mount, use Docker&#8217;s <code>:Z<\/code> label for a private container mount, such as <code>-v \/srv\/app:\/app:Z<\/code>, instead of disabling the policy.<\/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 AlmaLinux 9?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s documented RHEL procedure lists RHEL 9, and AlmaLinux 9 provides the compatible Enterprise Linux package interfaces used by that repository. The repository uses <code>el9<\/code> packages; their patch versions change as Docker publishes updates.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I remove Podman before installing Docker?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On a fresh VPS, removing unused Podman and standalone <code>runc<\/code> 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where does Docker keep its data on AlmaLinux 9?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Docker normally stores images, writable layers, and container metadata under <code>\/var\/lib\/docker<\/code>, while containerd keeps runtime data under <code>\/var\/lib\/containerd<\/code>. Monitor the filesystem that holds these paths before pulling large images.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to open a port for Docker?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s packet-filtering behavior alongside firewalld; a published port is a network decision, not just a container setting.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Does Docker support AlmaLinux 9?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Should I remove Podman before installing Docker?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Where does Docker keep its data on AlmaLinux 9?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Do I need to open a port for Docker?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker on AlmaLinux 9 with the official RPM repository, Compose v2, non-root access, a local test, and fixes for common VPS errors quickly.","protected":false},"author":1,"featured_media":1046,"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":[95,119,118,120],"class_list":["post-1045","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-almalinux-9","tag-containers","tag-docker","tag-docker-compose","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1045","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=1045"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1045\/revisions"}],"predecessor-version":[{"id":3960,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1045\/revisions\/3960"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1046"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1045"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1045"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1045"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}