{"id":1075,"date":"2026-11-13T13:00:00","date_gmt":"2026-11-13T13:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1075"},"modified":"2026-11-13T13:00:00","modified_gmt":"2026-11-13T13:00:00","slug":"install-docker-compose-rocky-linux-8","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/13\/install-docker-compose-rocky-linux-8\/","title":{"rendered":"How to Install Docker Compose on Rocky Linux 8"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Rocky Linux 8 with Docker Engine and Docker&#8217;s maintained Compose CLI plugin. In about ten minutes, you will launch a private PostgreSQL and Adminer stack, verify both services, and keep the database UI off the public network.<\/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 8 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 account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Docker&#8217;s RPM repository and container 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 needed for this private test. Use a domain and a TLS reverse proxy before exposing a real web application.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky Linux 8 uses DNF and firewalld. The commands below use <code>sudo<\/code> for a normal SSH account; if you are root, remove <code>sudo<\/code> and keep the same command order.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Rocky Linux 8 and update the host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release and architecture first prevents an Enterprise Linux repository mismatch.<\/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;Major release: %s\\n&#8217; &#8220;$(rpm -E %rhel)&#8221; uname -m sudo dnf upgrade -y ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first lines should identify Rocky Linux 8 and a 64-bit machine:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text Rocky Linux 8.x (Green Obsidian) Major release: 8 x86_64 ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The minor release and number of updated packages will vary. Resolve any DNS, mirror, or GPG-signature error before installing Docker.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Remove conflicting container packages on a fresh host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s official RPM packages can conflict with an existing Docker or Podman setup, so clear those packages only if this VPS does not already run containers you need.<\/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 report that some names are not installed. Do not run this step on a production host with active Podman workloads until you have inspected and migrated them. Removing packages does not delete Docker data stored in <code>\/var\/lib\/docker<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Add Docker&#8217;s Rocky-compatible RPM repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The RHEL-compatible Docker repository provides packages that DNF can update together, including the Compose plugin.<\/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 sudo dnf repolist &#8211;enabled | grep -i docker ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final command should list Docker&#8217;s stable repository. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">RHEL installation guide<\/a> supports RHEL 8; Rocky Linux 8 uses the same Enterprise Linux package interfaces.<\/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 from the RPM repository instead of downloading the obsolete standalone <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\">You should see an active service and 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 disposable smoke test to confirm that the daemon can pull and start an image:<\/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\">The final part of the response should contain:<\/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. Adding one trusted administrator to the <code>docker<\/code> group avoids <code>sudo<\/code>, but that group grants root-level control over the host.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_LINUX_USER<\/code> with the account you use 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 completely and reconnect so the new group membership is loaded. Then check access without <code>sudo<\/code>:<\/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 <code>docker<\/code>, and <code>docker info<\/code> should print an Engine version. If you prefer not to grant permanent group access, retain <code>sudo<\/code> on every Docker command instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Create a private PostgreSQL Compose project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This example uses Adminer as a database web client and PostgreSQL as its backend. The database has no published port, and Adminer listens only on the VPS loopback interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a project directory and a protected environment file:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash mkdir -p ~\/compose-rocky8 cd ~\/compose-rocky8 tee .env &gt;\/dev\/null &lt;&lt;&#8216;EOF&#8217; POSTGRES_DB=appdb POSTGRES_USER=appuser POSTGRES_PASSWORD=YOUR_STRONG_DATABASE_PASSWORD EOF chmod 600 .env ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_STRONG_DATABASE_PASSWORD<\/code> with a long, unique password before starting the services. Do not commit <code>.env<\/code> to a public repository.<\/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: db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>pgdata:\/var\/lib\/postgresql\/data healthcheck: test: [&#8220;CMD-SHELL&#8221;, &#8220;pg_isready -U \\&#8221;$$POSTGRES_USER\\&#8221; -d \\&#8221;$$POSTGRES_DB\\&#8221;&#8221;] interval: 5s timeout: 5s retries: 12<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">adminer: image: adminer:4 restart: unless-stopped depends_on: db: condition: service_healthy environment: ADMINER_DEFAULT_SERVER: db ports:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>&#8220;127.0.0.1:8086:8080&#8221;<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">volumes: pgdata: ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Compose substitutes the values from <code>.env<\/code> on the host, while <code>$$<\/code> passes the variable names into the database health check. The <code>depends_on<\/code> condition keeps Adminer from starting before PostgreSQL is accepting connections. The <code>pgdata<\/code> named volume preserves database files when the container is recreated. See Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a> for the service, health-check, and volume fields.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Render and start the stack<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rendering the file first catches YAML and variable-substitution errors before containers are created.<\/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 images download, both services should be running and PostgreSQL should be healthy:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text NAME                       SERVICE   STATUS                   PORTS compose-rocky8-db-1        db        Up (healthy) compose-rocky8-adminer-1   adminer   Up                       127.0.0.1:8086-&gt;8080\/tcp ~~~<\/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 Rocky Linux 8<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use an HTTP request and a PostgreSQL readiness check to prove that Compose controls the stack and the services can communicate:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash curl &#8211;fail &#8211;silent &#8211;show-error http:\/\/127.0.0.1:8086\/ | grep -o &#8216;&lt;title&gt;[^&lt;]*&lt;\/title&gt;&#8217; docker compose exec -T db pg_isready -U appuser -d appdb docker compose logs &#8211;tail=20 db adminer ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first two checks should produce output similar to:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~text &lt;title&gt;Login &#8211; Adminer&lt;\/title&gt; \/var\/run\/postgresql:5432 &#8211; accepting connections ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To open the Adminer page from your own workstation without publishing port 8086, create an SSH tunnel there:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash ssh -L 8086:127.0.0.1:8086 YOUR_LINUX_USER@SERVER_IP ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then visit <code>http:\/\/127.0.0.1:8086<\/code> locally. Use <code>db<\/code> as the server, <code>appuser<\/code> as the username, <code>appdb<\/code> as the database, and the password saved in <code>.env<\/code>.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on Rocky Linux 8\">\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 8<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Deploy private Docker Compose services on a Rocky Linux 8 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 the Docker repository, or its metadata refresh failed. Check the repository and refresh the cache:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash sudo dnf repolist &#8211;enabled | grep -i docker sudo dnf makecache sudo dnf list docker-ce &#8211;showduplicates ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The package list should include an <code>el8<\/code> build. If the first command returns nothing, repeat the repository command in Step 3 and check that <code>download.docker.com<\/code> resolves from the VPS.<\/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 CLI plugin is missing or the Docker packages came from another repository. Install the plugin after refreshing DNF metadata:<\/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 hyphenated command is the legacy standalone form and is not needed for this installation.<\/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 still has its old group list, or <code>usermod<\/code> targeted 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\">Adminer or a bind mount returns a permission error<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A browser on another machine cannot reach Adminer because the sample intentionally binds it to <code>127.0.0.1<\/code>; use the SSH tunnel from the Verify section. For a host-directory bind mount on Rocky Linux, add <code>:Z<\/code> to that mount so SELinux assigns a private container label, then recreate the service:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">~~~bash docker compose up -d &#8211;force-recreate SERVICE_NAME docker compose logs &#8211;tail=30 SERVICE_NAME ~~~<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep SELinux enabled. Correct the mount label or ownership instead of 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 database ports and the Docker API unpublished. If you later put a real web service behind a TLS reverse proxy, allow only its public 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>Limit membership in the <code>docker<\/code> group to trusted administrators because Docker socket access is effectively root-level access.<\/li>\n\n\n<li>Keep <code>.env<\/code> out of source control, review image tags before production use, and back up <code>pgdata<\/code> before running <code>docker compose down -v<\/code>, which deletes 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\">Is <code>docker compose<\/code> different from <code>docker-compose<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. <code>docker compose<\/code> is Compose v2 delivered as a Docker CLI plugin and is the current command used by this guide. <code>docker-compose<\/code> is the older standalone form kept for compatibility.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to disable SELinux for Docker Compose on Rocky Linux 8?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Keep SELinux enforcing. When you bind-mount a host directory, use the <code>:Z<\/code> option for a private label and recreate the affected container.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why is the Adminer port not open in firewalld?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The sample binds port 8086 to <code>127.0.0.1<\/code>, so it is reachable only from the VPS or through an SSH tunnel. This avoids exposing a database administration page to the internet during testing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does the <code>docker<\/code> group have administrator access?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. A member can use the Docker socket to start privileged containers and access host files, which is effectively root-level control. Add only trusted users, or keep using <code>sudo<\/code>.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Is `docker compose` different from `docker-compose`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Yes. `docker compose` is Compose v2 delivered as a Docker CLI plugin and is the current command used by this guide. `docker-compose` is the older standalone form kept for compatibility.\"}}, {\"@type\": \"Question\", \"name\": \"Do I need to disable SELinux for Docker Compose on Rocky Linux 8?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Keep SELinux enforcing. When you bind-mount a host directory, use the `:Z` option for a private label and recreate the affected container.\"}}, {\"@type\": \"Question\", \"name\": \"Why is the Adminer port not open in firewalld?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"The sample binds port 8086 to `127.0.0.1`, so it is reachable only from the VPS or through an SSH tunnel. This avoids exposing a database administration page to the internet during testing.\"}}, {\"@type\": \"Question\", \"name\": \"Does the `docker` group have administrator access?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Yes. A member can use the Docker socket to start privileged containers and access host files, which is effectively root-level control. Add only trusted users, or keep using `sudo`.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Rocky Linux 8 with Docker Engine, the Compose plugin, a private PostgreSQL test stack, and common fixes in minutes on a fresh VPS.","protected":false},"author":1,"featured_media":1076,"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,105],"class_list":["post-1075","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-8","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1075","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=1075"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1075\/revisions"}],"predecessor-version":[{"id":4015,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1075\/revisions\/4015"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1076"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1075"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1075"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1075"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}