{"id":1003,"date":"2026-11-04T13:00:00","date_gmt":"2026-11-04T13:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1003"},"modified":"2026-11-04T13:00:00","modified_gmt":"2026-11-04T13:00:00","slug":"install-docker-compose-ubuntu-24-04","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/04\/install-docker-compose-ubuntu-24-04\/","title":{"rendered":"How to Install Docker Compose on Ubuntu 24.04"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Ubuntu 24.04 by configuring Docker&#8217;s signed repository, installing Compose v2, and running a small web-and-cache stack. You will have a repeatable <code>compose.yaml<\/code> project to inspect and manage in about ten minutes, without publishing its test services 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 Ubuntu 24.04 LTS (Noble Numbat) 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 normal user with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to <code>download.docker.com<\/code> and Docker Hub so APT and Compose can retrieve packages and images.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need a clean Ubuntu server with root access.<\/li>\n\n\n<li>No domain is needed for this private test. A domain becomes useful when you later put a public service behind a TLS reverse proxy.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands assume a sudo-enabled SSH user. If you are logged in as <code>root<\/code>, remove <code>sudo<\/code> from the commands. The Docker group gives members root-equivalent control over the host, so use it only for accounts you trust.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Ubuntu 24.04 and update APT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker publishes packages for named Ubuntu releases, so verify that this server reports <code>noble<\/code> before adding a source for it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_CODENAME)=' \/etc\/os-release\ndpkg --print-architecture\nsudo apt update<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The release should be Ubuntu 24.04 LTS with an architecture such as <code>amd64<\/code> or <code>arm64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"Ubuntu 24.04.3 LTS\"\nVERSION_CODENAME=noble\namd64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If <code>apt update<\/code> reports a repository, DNS, or signature error, fix that first. A stale or partially updated package index can make a correct Docker repository appear empty.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s signed Noble repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The keyring and deb822 source file let APT verify Docker packages while keeping this installation tied to Ubuntu 24.04&#8217;s <code>noble<\/code> suite.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y ca-certificates curl\nsudo install -m 0755 -d \/etc\/apt\/keyrings\nsudo curl -fsSL https:\/\/download.docker.com\/linux\/ubuntu\/gpg -o \/etc\/apt\/keyrings\/docker.asc\nsudo chmod a+r \/etc\/apt\/keyrings\/docker.asc\nsudo tee \/etc\/apt\/sources.list.d\/docker.sources &gt;\/dev\/null &lt;&lt;'EOF'\nTypes: deb\nURIs: https:\/\/download.docker.com\/linux\/ubuntu\nSuites: noble\nComponents: stable\nArchitectures: amd64 arm64 armhf ppc64el s390x\nSigned-By: \/etc\/apt\/keyrings\/docker.asc\nEOF\nsudo apt update<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The architecture list matches Docker&#8217;s supported Ubuntu packages; APT will select the architecture reported by the machine. Confirm that the repository offers an Engine candidate before installing it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apt-cache policy docker-ce<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A successful check shows a <code>Candidate<\/code> version and a package source containing <code>download.docker.com<\/code> and <code>noble<\/code>. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/ubuntu\/\" target=\"_blank\" rel=\"noopener\">Ubuntu installation documentation<\/a> is the authoritative reference for future repository changes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Install Docker Engine and Compose v2<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Compose v2 is a Docker CLI plugin, so installing <code>docker-compose-plugin<\/code> gives you the maintained <code>docker compose<\/code> command rather than the obsolete hyphenated executable.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl enable --now docker\nsudo systemctl is-active 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\">The service check should return <code>active<\/code>, followed by Docker client\/server versions and a Compose v2 version. Exact patch numbers change as Docker publishes updates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nClient 29.x.y \/ Server 29.x.y\nDocker Compose version v2.x.y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the vendor&#8217;s small smoke test before creating a project. It downloads <code>hello-world<\/code>, starts it, prints a confirmation, and exits.<\/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\">Look for this confirmation in the output:<\/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<h2 class=\"wp-block-heading\">Step 4: Enable Compose for your SSH user<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s Unix socket is root-owned by default; adding your account to the <code>docker<\/code> group avoids <code>sudo<\/code> on every Compose command, but it also grants administrative control of 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\">Start a new SSH session so the group change is loaded, then check both membership and daemon access:<\/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 contain <code>docker<\/code>, and the second command should print a server version. Until reconnecting, use <code>sudo docker compose ...<\/code> or run <code>newgrp docker<\/code> for the current shell. For a stronger privilege boundary, investigate Docker rootless mode instead of using the group.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Create a two-service Compose project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This project runs Nginx and Redis together so you can see service status, a private Compose network, a health check, and persistent storage in one small example. Nginx is bound to loopback, while Redis has no published port at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a page and move into a dedicated directory:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/compose-noble\/site\ncd ~\/compose-noble\nprintf '%s\\n' 'Docker Compose on Ubuntu 24.04 is running.' &gt; site\/index.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a local environment file for the Redis password. Replace the placeholder with a long, unique value; it is intentionally not a usable credential.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tee .env &gt;\/dev\/null &lt;&lt;'EOF'\nREDIS_PASSWORD=YOUR_STRONG_REDIS_PASSWORD\nEOF\nchmod 600 .env<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now create <code>compose.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services:\n  web:\n    image: nginx:alpine\n    ports:\n      - \"127.0.0.1:8088:80\"\n    volumes:\n      - .\/site:\/usr\/share\/nginx\/html:ro\n    depends_on:\n      cache:\n        condition: service_healthy\n\n  cache:\n    image: redis:7-alpine\n    environment:\n      REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}\n    command: [\"sh\", \"-c\", \"exec redis-server --requirepass \\\"$$REDIS_PASSWORD\\\"\"]\n    volumes:\n      - redis_data:\/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"redis-cli -a \\\"$$REDIS_PASSWORD\\\" ping | grep PONG\"]\n      interval: 5s\n      timeout: 3s\n      retries: 12\n\nvolumes:\n  redis_data:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The read-only site mount stops Nginx from changing your files. The named volume survives container replacement, and the Redis service remains on Compose&#8217;s private network because it has no <code>ports<\/code> entry. <code>$$REDIS_PASSWORD<\/code> makes Compose pass the variable to the container instead of expanding it on the host while it renders the file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a> documents the service, volume, health check, and dependency keys used here.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Validate and start the stack<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Render the configuration first so YAML indentation, variable substitution, and the required password are checked before images are pulled.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose config --quiet\ndocker compose pull\ndocker compose up -d\ndocker compose ps<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command should produce no output and return status 0. After the images download, both services should be running and Redis should be healthy; the generated project name may differ if you choose another directory:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAME                  SERVICE   STATUS                   PORTS\ncompose-noble-web-1   web       Up                       127.0.0.1:8088-&gt;80\/tcp\ncompose-noble-cache-1 cache     Up (healthy)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on Ubuntu 24.04<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Check the served page, query Redis from inside its container, and inspect recent logs. These checks prove that the Compose plugin can render the file, start both services, and pass the protected Redis setting.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --fail --silent --show-error http:\/\/127.0.0.1:8088\ndocker compose exec cache sh -c 'redis-cli -a \"$REDIS_PASSWORD\" ping'\ndocker compose logs --tail 20 web cache<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The page and Redis check should return:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Docker Compose on Ubuntu 24.04 is running.\nPONG<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because Nginx listens only on <code>127.0.0.1<\/code>, a browser on your workstation cannot reach it directly. For private testing, create an SSH tunnel from your workstation with <code>ssh -L 8088:127.0.0.1:8088 USER@SERVER_IP<\/code>, then open <code>http:\/\/127.0.0.1:8088<\/code> locally.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not publish Redis or casually expose Docker ports. Docker documents that published container ports can bypass ordinary UFW filtering; when you later expose an application, allow only the required ports and apply a deliberate Docker-aware policy. VPSLake&#8217;s <a href=\"https:\/\/vpslake.com\/ufw-firewall-generator\">UFW firewall generator<\/a> can help draft the host rules.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on Noble\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Ubuntu 24.04 VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Run Compose on Noble<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Launch isolated Docker Compose services on an Ubuntu 24.04 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\"><code>docker: 'compose' is not a docker command<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin is missing, or the shell is calling a different Docker installation. Refresh the Noble package index and install the plugin from Docker&#8217;s repository:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install -y docker-compose-plugin\ndocker compose version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use a space in <code>docker compose<\/code>. The <code>docker-compose<\/code> command belongs to the legacy standalone tool and is not needed for this setup.<\/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\">Your current session has not loaded the new group membership, or the account was added to a different user than the one running the command. Open a new SSH session, run <code>id -nG<\/code>, and retry. <code>sudo docker compose ...<\/code> is a temporary workaround, but remember that membership in <code>docker<\/code> is effectively root access.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>E: Package 'docker-ce' has no installation candidate<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">APT is not reading the Noble source, often because the source file has the wrong suite or <code>apt update<\/code> failed. Inspect the file and refresh the cache:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat \/etc\/apt\/sources.list.d\/docker.sources\nsudo apt update\napt-cache policy docker-ce<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The file must contain <code>Suites: noble<\/code>, and the update must finish without errors. If the VPS already has Ubuntu&#8217;s <code>docker.io<\/code> packages or a legacy Compose installation, inventory existing containers first; do not remove a working production Docker installation without a migration plan.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>dependency failed to start<\/code> or Redis is <code>unhealthy<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Compose could not authenticate to Redis during its health check. Check the rendered configuration and logs, then make sure <code>.env<\/code> contains <code>REDIS_PASSWORD<\/code> with no spaces around the equals sign:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose config\ndocker compose logs cache<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The named volume can retain data and configuration from an earlier initialization. For a disposable test only, <code>docker compose down -v<\/code> removes <code>redis_data<\/code> before you start again; never use <code>-v<\/code> when the volume contains data you need.<\/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 <code>.env<\/code> at mode <code>600<\/code>, exclude it from source control, and use a managed secret store for production credentials.<\/li>\n\n\n<li>Leave application ports on loopback until authentication, TLS, and a reviewed firewall policy are ready. Never publish Redis directly to the public internet.<\/li>\n\n\n<li>Configure Docker log rotation or a centralized logging driver so long-running containers do not consume the VPS disk.<\/li>\n\n\n<li>Pin production images to reviewed tags or immutable digests, then update them deliberately with <code>docker compose pull<\/code> and <code>docker compose up -d<\/code>.<\/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 Docker Compose installed with Docker Engine on Ubuntu 24.04?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not necessarily. The official Docker repository provides the separate <code>docker-compose-plugin<\/code> package, which adds the <code>docker compose<\/code> subcommand. Always verify it with <code>docker compose version<\/code> after installation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does this guide use <code>compose.yaml<\/code> instead of <code>docker-compose.yml<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Compose v2 supports both filenames, but <code>compose.yaml<\/code> is the current, concise default for a new project. If you inherit another filename, pass it explicitly with <code>docker compose -f FILE up -d<\/code> rather than renaming a shared deployment file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Will <code>docker compose down<\/code> delete the Redis data?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A normal <code>down<\/code> removes the containers and network but keeps the named <code>redis_data<\/code> volume. The <code>-v<\/code> option also deletes that volume, so reserve it for a disposable reset after confirming that no data must be retained.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How can I open the sample page from my own computer?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the service private and use an SSH tunnel such as <code>ssh -L 8088:127.0.0.1:8088 USER@SERVER_IP<\/code>. For a public deployment, put the application behind a TLS reverse proxy, allow only ports 80 and 443 in the firewall, and keep Redis on the internal Compose network.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Is Docker Compose installed with Docker Engine on Ubuntu 24.04?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Not necessarily. The official Docker repository provides the separate `docker-compose-plugin` package, which adds the `docker compose` subcommand. Always verify it with `docker compose version` after installation.\"}}, {\"@type\": \"Question\", \"name\": \"Why does this guide use `compose.yaml` instead of `docker-compose.yml`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Compose v2 supports both filenames, but `compose.yaml` is the current, concise default for a new project. If you inherit another filename, pass it explicitly with `docker compose -f FILE up -d` rather than renaming a shared deployment file.\"}}, {\"@type\": \"Question\", \"name\": \"Will `docker compose down` delete the Redis data?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"A normal `down` removes the containers and network but keeps the named `redis_data` volume. The `-v` option also deletes that volume, so reserve it for a disposable reset after confirming that no data must be retained.\"}}, {\"@type\": \"Question\", \"name\": \"How can I open the sample page from my own computer?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Keep the service private and use an SSH tunnel such as `ssh -L 8088:127.0.0.1:8088 USER@SERVER_IP`. For a public deployment, put the application behind a TLS reverse proxy, allow only ports 80 and 443 in the firewall, and keep Redis on the internal Compose network.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Ubuntu 24.04 with Docker&#8217;s official APT repository, run a two-service stack, verify it, and fix common errors fast.","protected":false},"author":1,"featured_media":1004,"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,102],"class_list":["post-1003","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-docker","tag-docker-compose","tag-ubuntu-24-04","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1003","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=1003"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1003\/revisions"}],"predecessor-version":[{"id":3884,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1003\/revisions\/3884"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1004"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1003"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1003"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1003"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}