{"id":1071,"date":"2026-11-13T03:00:00","date_gmt":"2026-11-13T03:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1071"},"modified":"2026-11-13T03:00:00","modified_gmt":"2026-11-13T03:00:00","slug":"install-docker-compose-almalinux-9","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/13\/install-docker-compose-almalinux-9\/","title":{"rendered":"How to Install Docker Compose on AlmaLinux 9"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on AlmaLinux 9 with Docker Engine and the maintained Compose CLI plugin. In roughly ten minutes, you will start a two-service test project, check its health, and keep its web port private.<\/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 root or as a normal Linux account with working sudo privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Docker&#8217;s package repository and container registry.<\/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\n<li>No domain is required for the private test project. Add a domain later when you put a real application behind a TLS reverse proxy.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">AlmaLinux 9 uses DNF and firewalld. The commands below install Docker&#8217;s RHEL-compatible packages; if you are already using Podman or another container runtime, check its workloads before changing packages.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm the release and update packages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release avoids adding an Enterprise Linux 8 repository to an AlmaLinux 9 machine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash . \/etc\/os-release printf &#8216;%s\\n&#8217; &#8220;$PRETTY_NAME&#8221; uname -m sudo dnf upgrade -y ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first two checks should identify AlmaLinux 9 and a 64-bit architecture:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text AlmaLinux 9.x (Emerald Puma) x86_64 ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The minor release and update count will vary. Resolve any DNS, mirror, or package-signature error before continuing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s RPM repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker repository provides the Engine, CLI, Buildx, and Compose plugin as packages that DNF can update together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf install -y dnf-plugins-core ca-certificates sudo dnf config-manager &#8211;add-repo https:\/\/download.docker.com\/linux\/rhel\/docker-ce.repo sudo dnf makecache dnf repolist &#8211;enabled | grep -i docker ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You should see Docker&#8217;s stable repository in the final output. These are the repository steps from Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">RHEL installation guide<\/a>, using AlmaLinux 9&#8217;s RHEL-compatible package interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Install Docker Engine and Compose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installing the plugin with the Engine gives the Docker CLI a compose subcommand and avoids the unsupported legacy standalone executable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl enable &#8211;now docker sudo systemctl is-active docker sudo docker version &#8211;format &#8216;Client {{.Client.Version}} \/ Server {{.Server.Version}}&#8217; sudo docker compose version ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The service should report active, followed by version strings. Exact versions change as Docker publishes updates:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text active Client 29.x \/ Server 29.x Docker Compose version vX.Y.Z ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run Docker&#8217;s small smoke test. It downloads a public image, starts a temporary container, prints a message, and removes the container when it exits.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo docker run &#8211;rm hello-world ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Near the end, expect output containing:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text Hello from Docker! Your installation appears to be working correctly. ~~~<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Allow your SSH account to use Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default, so add only the trusted account that will manage containers instead of making the socket broadly writable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replace YOUR_LINUX_USER with the username used for SSH:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo usermod -aG docker YOUR_LINUX_USER ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log out and reconnect so the new group membership is loaded, then check it:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash id -nG docker info &#8211;format &#8216;Server {{.ServerVersion}}&#8217; ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The group list should include docker, and docker info should print the Engine version. The Docker group grants root-equivalent control over the host, so add only accounts you trust. If you prefer not to use the group, keep the sudo prefix on later Docker commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Create a small Compose project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This project pairs Caddy, which serves a local page, with Redis, which stays on Compose&#8217;s private network. The loopback binding lets you test the result without exposing port 8090 to the internet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash mkdir -p ~\/compose-alma9\/site cd ~\/compose-alma9 printf &#8216;%s\\n&#8217; &#8216;AlmaLinux 9 Docker Compose is working.&#8217; &gt; site\/index.html ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create the Caddy configuration:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash tee Caddyfile &gt;\/dev\/null &lt;&lt;&#8216;EOF&#8217; :8090 { root * \/srv file_server } EOF ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create compose.yaml:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash tee compose.yaml &gt;\/dev\/null &lt;&lt;&#8216;EOF&#8217; services: web: image: caddy:2-alpine restart: unless-stopped ports:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>&#8220;127.0.0.1:8090:8090&#8221; volumes:<\/li>\n\n\n<li>.\/Caddyfile:\/etc\/caddy\/Caddyfile:ro,Z<\/li>\n\n\n<li>.\/site:\/srv:ro,Z<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">cache: image: redis:7-alpine restart: unless-stopped command: [&#8220;redis-server&#8221;, &#8220;&#8211;save&#8221;, &#8220;&#8221;, &#8220;&#8211;appendonly&#8221;, &#8220;no&#8221;] healthcheck: test: [&#8220;CMD&#8221;, &#8220;redis-cli&#8221;, &#8220;ping&#8221;] interval: 5s timeout: 3s retries: 10 EOF ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The :Z mount option assigns SELinux labels suitable for private container mounts. Redis has no published port, and the sample disables persistence because it is only an installation test; use a password, persistent storage, and a backup plan for a real cache or database. The service and volume syntax follows Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Render and start the services<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rendering the file before startup catches YAML and interpolation errors without creating containers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash docker compose config &#8211;quiet docker compose pull docker compose up -d docker compose ps ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first command is silent when the file is valid. After the images download, the table should show a running web service and a healthy cache:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text NAME                   SERVICE   STATUS                   PORTS compose-alma9-web-1    web       Up                       127.0.0.1:8090-&gt;8090\/tcp compose-alma9-cache-1  cache     Up (healthy) ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Container names may differ if you choose another directory or set a Compose project name.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on AlmaLinux 9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use one HTTP request, one Redis health check, and a short log view to prove that the plugin can control the project and that both services are responding.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash curl &#8211;fail &#8211;silent http:\/\/127.0.0.1:8090 docker compose exec -T cache redis-cli ping docker compose logs &#8211;tail=20 web cache ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first two commands should return:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text AlmaLinux 9 Docker Compose is working. PONG ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because Caddy listens only on loopback, a remote browser cannot reach it directly. To view it from your workstation, create an SSH tunnel with ssh -L 8090:127.0.0.1:8090 USER@SERVER_IP, then open http:\/\/127.0.0.1:8090 locally.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on AlmaLinux 9\">\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\">Run Compose on AlmaLinux 9<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Deploy private Docker Compose services on an AlmaLinux 9 VPS with root access.<\/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\">docker: &#8216;compose&#8217; is not a docker command<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The CLI plugin is missing, or Docker packages came from a different repository. Refresh the metadata and install the plugin from Docker&#8217;s repository:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf makecache sudo dnf install -y docker-compose-plugin docker compose version ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use docker compose with a space. The hyphenated docker-compose command is the older standalone tool and is not needed here.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">No match for argument: docker-ce<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">DNF cannot see the Docker repository or could not refresh its metadata. Confirm the repository and rebuild the cache:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf repolist &#8211;enabled | grep -i docker sudo dnf makecache dnf list &#8211;showduplicates docker-ce ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the first command returns nothing, repeat Step 2. A listed package with an el9 suffix confirms that DNF is reading the AlmaLinux 9-compatible package stream.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">permission denied while trying to connect to the Docker daemon socket<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Your current SSH session has not loaded the docker group, or usermod was run for another account. Reconnect, run id -nG, and retry. sudo docker compose &#8230; is a temporary workaround while you correct the group membership.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Caddy returns 403 Forbidden or cannot read \/srv<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SELinux is blocking an unlabeled bind mount. Confirm the Compose file has :Z on both bind mounts, then recreate the web container so Docker applies the label:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash docker compose up -d &#8211;force-recreate web docker compose logs &#8211;tail=30 web ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep SELinux enabled; do not solve a mount-label problem by disabling it.<\/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 test services bound to 127.0.0.1. If you later publish a web application through a TLS reverse proxy, allow only the required HTTP and HTTPS services in firewalld:<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo firewall-cmd &#8211;permanent &#8211;add-service=http sudo firewall-cmd &#8211;permanent &#8211;add-service=https sudo firewall-cmd &#8211;reload ~~~<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Do not publish Redis or the Docker API to the public network. Use Compose&#8217;s private network for service-to-service traffic.<\/li>\n\n\n<li>Keep the docker group limited to trusted administrators, store secrets outside a public repository, and pin production images to reviewed tags or digests.<\/li>\n\n\n<li>Back up application data before using docker compose down -v; the -v option removes named volumes.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Can I install the Compose plugin without Docker Engine?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can install the package, but Compose needs the Docker CLI and a reachable Docker Engine to start containers. Installing the Engine and plugin together avoids a partial setup and makes docker compose version immediately useful.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is docker-compose different from docker compose?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. docker compose is the current Docker CLI plugin and is distributed by Docker&#8217;s RPM repository. docker-compose is the legacy standalone command, so new AlmaLinux 9 projects should use the space-separated form.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does the sample require a firewalld rule for port 8090?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. The sample maps port 8090 to 127.0.0.1, so it is reachable only from the server itself or through an SSH tunnel. If you bind to all interfaces, use a reverse proxy with TLS and add the corresponding firewalld rule instead of exposing an unprotected test service.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Will the Compose services restart after a reboot?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, Docker is enabled as a system service and both sample services use restart: unless-stopped. They will return after a normal reboot unless you intentionally stopped them or removed the project.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Can I install the Compose plugin without Docker Engine?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"You can install the package, but Compose needs the Docker CLI and a reachable Docker Engine to start containers. Installing the Engine and plugin together avoids a partial setup and makes docker compose version immediately useful.\"}}, {\"@type\": \"Question\", \"name\": \"Is docker-compose different from docker compose?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Yes. docker compose is the current Docker CLI plugin and is distributed by Docker's RPM repository. docker-compose is the legacy standalone command, so new AlmaLinux 9 projects should use the space-separated form.\"}}, {\"@type\": \"Question\", \"name\": \"Does the sample require a firewalld rule for port 8090?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. The sample maps port 8090 to 127.0.0.1, so it is reachable only from the server itself or through an SSH tunnel. If you bind to all interfaces, use a reverse proxy with TLS and add the corresponding firewalld rule instead of exposing an unprotected test service.\"}}, {\"@type\": \"Question\", \"name\": \"Will the Compose services restart after a reboot?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Yes, Docker is enabled as a system service and both sample services use restart: unless-stopped. They will return after a normal reboot unless you intentionally stopped them or removed the project.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on AlmaLinux 9 with Docker Engine, the Compose plugin, a test stack, verification commands, and common VPS fixes in minutes.","protected":false},"author":1,"featured_media":1072,"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-1071","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\/1071","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=1071"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1071\/revisions"}],"predecessor-version":[{"id":4010,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1071\/revisions\/4010"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1072"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1071"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1071"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1071"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}