Install Docker Compose on Rocky Linux 9 by adding Docker’s RPM repository, installing the Compose CLI plugin, and launching a private two-service test project. You can finish the setup and confirm that Compose works in about ten minutes.
Prerequisites
- A 64-bit Rocky Linux 9 VPS with at least 2 GB of RAM and 10 GB of free disk space.
- SSH access as
root, or a regular Linux account with workingsudoprivileges. - Outbound HTTPS access to Docker’s package repository and container registry.
- A VPSLake Linux VPS if you need a Rocky Linux server with root access.
- No domain is needed for the private test. Use a domain and a TLS reverse proxy before publishing a real application.
Rocky Linux 9 uses DNF and firewalld. The commands below use sudo for a normal SSH account; if you are logged in as root, remove sudo but keep the same order.
Step 1: Confirm Rocky Linux 9 and update the host
Checking the release and architecture first keeps you from applying an Enterprise Linux repository intended for another major version.
~~~bash . /etc/os-release printf ‘%s\n’ “$PRETTY_NAME” printf ‘Major release: %s\n’ “$(rpm -E %rhel)” uname -m sudo dnf upgrade -y ~~~
The first lines should identify Rocky Linux 9 and a 64-bit machine:
~~~text Rocky Linux 9.x (Blue Onyx) Major release: 9 x86_64 ~~~
The minor release and number of upgraded packages will vary. Resolve any DNS, mirror, or GPG-signature error before continuing.
Step 2: Remove conflicting container packages
Docker’s official packages can conflict with an existing Docker or Podman installation, so clear those packages on a new or disposable VPS before adding Docker’s repository.
Run this only after checking that the server does not host Podman workloads you need. DNF may say that some package names are not installed, which is harmless; Docker data under /var/lib/docker is not removed by this package operation.
~~~bash sudo dnf remove -y docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine \ podman \ runc ~~~
Step 3: Add Docker’s RPM repository
The RHEL-compatible Docker repository supplies Engine, Buildx, and the Compose plugin as DNF packages that can receive normal updates. This is Docker’s recommended repository approach, documented in its RHEL installation guide.
~~~bash sudo dnf install -y dnf-plugins-core ca-certificates sudo dnf config-manager –add-repo https://download.docker.com/linux/rhel/docker-ce.repo sudo dnf makecache dnf repolist –enabled | grep -i docker dnf list –showduplicates docker-ce | tail -n 5 ~~~
The repository check should show Docker’s stable channel, and the package list should include an el9 build. If no repository or package appears, stop here and fix the repository connection before installing anything.
Step 4: Install Docker Engine and the Compose plugin
Compose v2 is delivered as a Docker CLI plugin, so install it with the Engine rather than downloading the obsolete standalone docker-compose binary.
~~~bash sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl enable –now docker sudo systemctl is-active docker sudo docker version –format ‘Client {{.Client.Version}} / Server {{.Server.Version}}’ sudo docker compose version ~~~
You should see an active service and version strings. Exact versions change as Docker publishes updates:
~~~text active Client 29.x / Server 29.x Docker Compose version vX.Y.Z ~~~
Run Docker’s disposable smoke test before creating a project:
~~~bash sudo docker run –rm hello-world ~~~
The final part of the response should include:
~~~text Hello from Docker! Your installation appears to be working correctly. ~~~
Step 5: Allow your SSH account to run Docker
The Docker socket is root-owned by default. Adding one trusted administrator to the docker group avoids sudo, but group membership provides effectively root-level control over the host.
Replace YOUR_LINUX_USER with the account you use for SSH:
~~~bash sudo usermod -aG docker YOUR_LINUX_USER ~~~
Log out of SSH completely and reconnect so the new group list is loaded. Then verify access without sudo:
~~~bash id -nG docker info –format ‘Server {{.ServerVersion}}’ ~~~
The group list should contain docker, and docker info should print the Engine version. If you do not want permanent group access, leave the account unchanged and use sudo docker compose for the remaining commands.
Step 6: Create a private Compose test project
This project runs Nginx and a small BusyBox checker. The checker reaches Nginx through Compose’s internal service name, while Nginx publishes its page only on the server’s loopback address.
Create a project directory and a page for Nginx:
~~~bash mkdir -p ~/compose-rocky9/site cd ~/compose-rocky9 printf ‘%s\n’ ‘Rocky Linux 9 Docker Compose is working.’ > site/index.html ~~~
Create compose.yaml:
~~~bash tee compose.yaml >/dev/null <<‘EOF’ services: web: image: nginx:alpine restart: unless-stopped ports:
- “127.0.0.1:8091:80” volumes:
- ./site:/usr/share/nginx/html:ro,Z
checker: image: busybox:1.36 restart: unless-stopped depends_on: web: condition: service_started command: [“sh”, “-c”, “while true; do sleep 3600; done”] healthcheck: test: [“CMD-SHELL”, “wget -q -O – http://web/ | grep -q ‘Rocky Linux 9 Docker Compose is working.'”] interval: 5s timeout: 3s retries: 12 EOF ~~~
The :Z suffix gives the private bind mount an SELinux label, and 127.0.0.1 prevents the test page from being reachable directly from the internet. The checker has no published port; it validates the web service over Compose’s private network. See Docker’s Compose file reference for the service and health-check fields.
Step 7: Render and start the project
Render the file before startup so YAML or interpolation errors are caught before containers are created.
~~~bash docker compose config –quiet docker compose pull docker compose up -d docker compose ps ~~~
config --quiet should produce no output and return status 0. After the images download, both services should be running and the checker should become healthy:
~~~text NAME SERVICE STATUS PORTS compose-rocky9-web-1 web Up 127.0.0.1:8091->80/tcp compose-rocky9-checker-1 checker Up (healthy) ~~~
The generated names may differ if you run the project from another directory or set a Compose project name. Because port 8091 is bound to loopback, this test does not require a new firewalld rule.
Verify Docker Compose on Rocky Linux 9
Use the host request, the internal service request, and the health table to prove that Compose can start the project and that its network works:
~~~bash curl –fail –silent –show-error http://127.0.0.1:8091 docker compose exec -T checker wget -q -O – http://web/ docker compose ps ~~~
The first two commands should return the same page text, and the status table should keep the checker healthy:
~~~text Rocky Linux 9 Docker Compose is working. ~~~
To view the private page from your workstation, create an SSH tunnel there with ssh -L 8091:127.0.0.1:8091 YOUR_LINUX_USER@SERVER_IP, then open http://127.0.0.1:8091 locally.
Troubleshooting
No match for argument: docker-ce
DNF is not reading Docker’s repository, or its metadata refresh failed. Confirm the repository and rebuild the cache:
~~~bash sudo dnf repolist –enabled | grep -i docker sudo dnf makecache dnf list –showduplicates docker-ce ~~~
The list should contain an el9 package from Docker’s stable channel. If the first command returns nothing, repeat the repository command in Step 3 and make sure download.docker.com resolves from the VPS.
docker: 'compose' is not a docker command
The plugin is missing, or the Docker packages came from a different repository. Refresh metadata and install the plugin explicitly:
~~~bash sudo dnf makecache sudo dnf install -y docker-compose-plugin docker compose version ~~~
Use docker compose with a space. The hyphenated docker-compose command is the legacy standalone form and is not required for this installation.
permission denied while trying to connect to the Docker daemon socket
The current SSH session has not loaded the new group membership, or usermod targeted another account. Reconnect, confirm that id -nG includes docker, and retry. sudo docker compose ... is a temporary workaround while you correct the account.
The checker is unhealthy or Nginx returns connection refused
The web container may still be starting, the image pull may have failed, or another process may already use port 8091. Inspect both services and the host port:
~~~bash docker compose ps docker compose logs –tail=30 web checker ss -ltnp | grep ‘:8091’ ~~~
If another service owns the port, change only the host side of the mapping in compose.yaml, such as 127.0.0.1:8092:80, then recreate the project with docker compose up -d. If Nginx returns 403 or the logs mention a bind-mount denial, SELinux may be refusing an unlabeled host directory. Confirm that the site volume ends in :ro,Z, then recreate the web service:
~~~bash docker compose up -d –force-recreate web docker compose logs –tail=30 web ~~~
Keep SELinux enforcing; changing the mount label is safer than disabling the security policy.
Hardening
- Keep the sample on
127.0.0.1. If a real application will be public, place it behind a TLS reverse proxy and allow only the proxy’s services in firewalld:
~~~bash sudo firewall-cmd –permanent –add-service=http sudo firewall-cmd –permanent –add-service=https sudo firewall-cmd –reload sudo firewall-cmd –list-services ~~~
- Do not publish the Docker API, databases, or internal service ports. Use Compose’s private network for service-to-service traffic.
- Limit
dockergroup membership to trusted administrators, keep secrets out of Compose files and public repositories, and review image tags before production use. - Back up persistent application data before using
docker compose down -v; the-voption removes named volumes.
FAQ
Can I install Docker Compose without Docker Engine?
You can install the plugin package separately, but Compose needs the Docker CLI and a reachable Engine to start containers. Installing Engine and the plugin together avoids a partial setup and makes docker compose version useful immediately.
Why does this guide use docker compose instead of docker-compose?
docker compose is the current CLI plugin command, while docker-compose is the older standalone executable. The plugin is updated through Docker’s RPM repository and is the better default for new projects.
Does the sample need a firewalld rule for port 8091?
No. Docker binds the sample to 127.0.0.1, so it is not directly reachable on the VPS’s public interface. Use the SSH tunnel in the verification section for remote browser access; add public firewall services only when a TLS reverse proxy is ready.
Will these services return after a reboot?
Docker is enabled to start at boot, and both services use restart: unless-stopped. They should return after a normal reboot unless you deliberately stopped or removed the project.