{"id":1073,"date":"2026-11-13T09:00:00","date_gmt":"2026-11-13T09:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1073"},"modified":"2026-11-13T09:00:00","modified_gmt":"2026-11-13T09:00:00","slug":"install-docker-compose-rocky-linux-10","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/13\/install-docker-compose-rocky-linux-10\/","title":{"rendered":"How to Install Docker Compose on Rocky Linux 10"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Rocky Linux 10 by adding Docker&#8217;s Rocky repository, installing the Compose CLI plugin, and starting a private test container. You can finish the setup and confirm a working Compose project in about ten minutes.<\/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 10 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 a normal Linux account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Docker&#8217;s package repository and image registry.<\/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\n<li>No domain is required for this private test. Use a domain later if you place a public application behind a TLS reverse proxy.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands use <code>sudo<\/code> for a regular SSH account. Rocky Linux 10 uses DNF 5 and firewalld; do not substitute Ubuntu&#8217;s <code>apt<\/code> or <code>ufw<\/code> commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Rocky Linux 10 and update the host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release and architecture first prevents you from installing packages for the wrong Enterprise Linux version.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash . \/etc\/os-release printf &#8216;%s\\n&#8217; &#8220;$PRETTY_NAME&#8221; printf &#8216;Version: %s\\n&#8217; &#8220;$VERSION_ID&#8221; uname -m sudo dnf upgrade -y ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first lines should identify Rocky Linux 10 and a supported 64-bit architecture:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text Rocky Linux 10.x (Red Quartz) Version: 10.x x86_64 ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The minor release and number of upgraded packages will vary. Fix any DNS, mirror, or GPG-signature error before adding Docker&#8217;s repository.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Remove conflicting container packages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky Linux may arrive with Podman or an older Docker package. Removing conflicting runtimes on a new server avoids a package transaction that leaves Docker partially installed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this only on a fresh or disposable host. If the server already runs Podman containers, inspect and migrate those workloads before removing Podman.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf remove -y docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine \\ podman \\ runc ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">DNF may say that some packages are not installed. This command does not remove Docker images, containers, or volumes stored under <code>\/var\/lib\/docker<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Add Docker&#8217;s Rocky Linux repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Rocky-specific repository provides EL10 builds of Docker Engine, Buildx, and the Compose plugin, while leaving DNF able to receive future updates.<\/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\/rocky\/docker-ce.repo sudo dnf makecache dnf repolist &#8211;enabled | grep -i docker ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last command should show Docker&#8217;s stable repository, usually named <code>docker-ce-stable<\/code>. Docker documents the repository approach in its <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">RHEL installation guide<\/a>, and the official repository publishes a separate Rocky Linux 10 package stream.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Install Docker Engine and the Compose plugin<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Compose v2 is a Docker CLI plugin, so install it with Engine instead of downloading the old hyphenated <code>docker-compose<\/code> binary.<\/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\">A successful check prints an active service and version strings. Exact versions change as Docker releases 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 disposable smoke test. It pulls a small image, prints a confirmation, and removes the stopped container.<\/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:<\/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 5: Let your SSH account run Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default. Add only the account that will administer containers; membership in the <code>docker<\/code> group provides root-equivalent control of the host.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_LINUX_USER<\/code> 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 of SSH and reconnect so the new group membership is loaded. Then verify non-root access:<\/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 contain <code>docker<\/code>, and <code>docker info<\/code> should print the Engine version. You can run <code>newgrp docker<\/code> for a one-shell test, or keep using <code>sudo docker compose<\/code> if you do not want permanent group access. Docker explains the security implications in its <a href=\"https:\/\/docs.docker.com\/engine\/install\/linux-postinstall\/\" target=\"_blank\" rel=\"noopener\">Linux post-installation guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Create a private Compose test project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This small Apache service proves that Compose can read a YAML file, mount a host directory with an SELinux label, and publish a web response only on the server&#8217;s loopback address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash mkdir -p ~\/compose-rocky10\/site cd ~\/compose-rocky10 printf &#8216;%s\\n&#8217; &#8216;Rocky Linux 10 Docker Compose is working.&#8217; &gt; site\/index.html ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>compose.yaml<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~yaml services: web: image: httpd:2.4-alpine restart: unless-stopped ports:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>&#8220;127.0.0.1:8085:80&#8221; volumes:<\/li>\n\n\n<li>.\/site:\/usr\/local\/apache2\/htdocs:ro,Z<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>:Z<\/code> suffix gives SELinux a private label for the bind mount. The <code>127.0.0.1<\/code> binding makes Apache reachable on the VPS itself but not directly from the public network, so this test needs no new firewalld rule.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Render and start the project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Render the configuration before startup so YAML errors are caught without creating a container.<\/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\"><code>config --quiet<\/code> should be silent and return status 0. After the image downloads, the service should show as running:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text NAME                  IMAGE             SERVICE   STATUS          PORTS compose-rocky10-web-1 httpd:2.4-alpine  web       Up              127.0.0.1:8085-&gt;80\/tcp ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The generated container name can differ if you use another directory or set a Compose project name.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on Rocky Linux 10<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use an HTTP request, an Apache configuration check, and the Compose status table to prove that the plugin controls the service and the bind mount is readable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash curl &#8211;fail &#8211;silent &#8211;show-error http:\/\/127.0.0.1:8085 docker compose exec -T web httpd -t docker compose ps ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first two commands should include:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text Rocky Linux 10 Docker Compose is working. Syntax OK ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because the example listens on loopback, a browser on your workstation cannot reach it directly. To view the page locally, create an SSH tunnel from your workstation with <code>ssh -L 8085:127.0.0.1:8085 USER@SERVER_IP<\/code>, then open <code>http:\/\/127.0.0.1:8085<\/code>.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on Rocky Linux 10\">\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\">Run Compose on Rocky Linux 10<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Deploy Docker Compose projects on a Rocky Linux 10 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\">No match for argument: docker-ce<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">DNF is not reading Docker&#8217;s Rocky repository, or its metadata refresh failed. 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\">The package list should contain an EL10 build. If the first command returns nothing, repeat the repository command from Step 3 and make sure the server can resolve <code>download.docker.com<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>docker: 'compose' is not a docker command<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Compose plugin is missing or the Docker packages came from a different repository. Install the plugin from Docker&#8217;s repository and check the subcommand again:<\/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 <code>docker compose<\/code> with a space. The legacy <code>docker-compose<\/code> executable is not required for this setup.<\/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\">The current SSH session still has its old group list, or <code>usermod<\/code> was run for a different account. Reconnect, confirm that <code>id -nG<\/code> includes <code>docker<\/code>, and retry. <code>sudo docker compose ...<\/code> is a temporary workaround.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Apache returns 403 Forbidden or cannot read the mounted site<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SELinux is usually blocking an unlabeled bind mount. Confirm the Compose file contains <code>:Z<\/code> on the <code>site<\/code> mount, then recreate the service:<\/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; fixing the mount label is safer than disabling the policy.<\/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 on <code>127.0.0.1<\/code>. For a real public website, put a TLS reverse proxy in front of the application and allow only the required 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>Never publish the Docker API or a database port to the public network. Use Compose&#8217;s private network for service-to-service traffic.<\/li>\n\n\n<li>Keep the <code>docker<\/code> group limited to trusted administrators, store secrets outside public repositories, and review image tags before production deployments.<\/li>\n\n\n<li>Back up persistent application data before running <code>docker compose down -v<\/code>, because <code>-v<\/code> 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 Compose without installing Docker Engine?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can install the plugin package by itself, but Compose needs the Docker CLI and a reachable Engine to start containers. Installing the Engine and plugin together avoids an incomplete setup.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does this guide use <code>docker compose<\/code> instead of <code>docker-compose<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The space-separated command is Compose v2, which Docker distributes as a CLI plugin and updates through the package repository. The hyphenated command is the older standalone installation kept mainly for compatibility.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does the test project need a firewall rule?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Its port is bound to <code>127.0.0.1<\/code>, so it is available only on the VPS and through an SSH tunnel. If you later bind a reverse proxy publicly, add only the HTTP and HTTPS services required by that proxy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is membership in the <code>docker<\/code> group safe for every user?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. A user who can access the Docker socket can start privileged containers and control host filesystems, which is effectively root-level access. Add only trusted administrators, or keep using <code>sudo<\/code> and consider rootless Docker for stronger separation.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Can I install Compose without installing Docker Engine?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"You can install the plugin package by itself, but Compose needs the Docker CLI and a reachable Engine to start containers. Installing the Engine and plugin together avoids an incomplete setup.\"}}, {\"@type\": \"Question\", \"name\": \"Why does this guide use `docker compose` instead of `docker-compose`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"The space-separated command is Compose v2, which Docker distributes as a CLI plugin and updates through the package repository. The hyphenated command is the older standalone installation kept mainly for compatibility.\"}}, {\"@type\": \"Question\", \"name\": \"Does the test project need a firewall rule?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Its port is bound to `127.0.0.1`, so it is available only on the VPS and through an SSH tunnel. If you later bind a reverse proxy publicly, add only the HTTP and HTTPS services required by that proxy.\"}}, {\"@type\": \"Question\", \"name\": \"Is membership in the `docker` group safe for every user?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. A user who can access the Docker socket can start privileged containers and control host filesystems, which is effectively root-level access. Add only trusted administrators, or keep using `sudo` and consider rootless Docker for stronger separation.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Rocky Linux 10 with Docker Engine, the Compose plugin, a private test stack, verification, and common VPS fixes today.","protected":false},"author":1,"featured_media":1074,"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,99],"class_list":["post-1073","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-10","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1073","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=1073"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1073\/revisions"}],"predecessor-version":[{"id":4014,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1073\/revisions\/4014"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1074"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1073"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1073"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1073"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}