{"id":1001,"date":"2026-11-04T09:00:00","date_gmt":"2026-11-04T09:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1001"},"modified":"2026-11-04T09:00:00","modified_gmt":"2026-11-04T09:00:00","slug":"install-docker-compose-ubuntu-22-04","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/04\/install-docker-compose-ubuntu-22-04\/","title":{"rendered":"How to Install Docker Compose on Ubuntu 22.04"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Ubuntu 22.04 by adding Docker&#8217;s signed APT repository, installing Compose v2, and launching a small Apache and MariaDB stack. You can reach a working local test in about ten minutes without exposing the database or web port to the public 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 22.04 LTS (Jammy Jellyfish) 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 package repository and Docker Hub.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need an Ubuntu server with root access.<\/li>\n\n\n<li>No domain is required for this private test. You will need 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. If you are already logged in as <code>root<\/code>, remove <code>sudo<\/code> from each command.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Jammy and refresh APT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker publishes a separate package repository for each Ubuntu release, so confirm that this server really is Ubuntu 22.04 before adding the Jammy source.<\/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 check should show Jammy and a supported architecture such as <code>amd64<\/code> or <code>arm64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"Ubuntu 22.04.5 LTS\"\nVERSION_CODENAME=jammy\namd64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not continue if <code>apt update<\/code> reports a broken mirror, DNS failure, or unsigned source. A clean package index is needed before Docker&#8217;s repository can be trusted.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s signed Ubuntu repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The keyring and deb822 source file let APT verify Docker packages without the deprecated <code>apt-key<\/code> command. This source is pinned to Ubuntu 22.04&#8217;s <code>jammy<\/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: jammy\nComponents: stable\nArchitectures: $(dpkg --print-architecture)\nSigned-By: \/etc\/apt\/keyrings\/docker.asc\nEOF\nsudo apt update<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check that APT can see a Docker Engine candidate from the new source:<\/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\">The <code>Candidate<\/code> should contain a version from <code>download.docker.com<\/code>, and the repository line should mention <code>jammy<\/code>. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/ubuntu\/\" target=\"_blank\" rel=\"noopener\">Ubuntu installation guide<\/a> lists Jammy as a supported release and documents this repository method.<\/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 is maintained as a Docker CLI plugin, so install it with the Engine rather than downloading the old standalone <code>docker-compose<\/code> binary.<\/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\">You should see an active daemon, matching client and server versions, and a Compose v2 version. Patch numbers change over time, so do not compare the exact numbers:<\/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 Docker&#8217;s smoke test before creating a project. It downloads a small image, starts a container, 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\">The output should contain:<\/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<p class=\"wp-block-paragraph\">If Docker was already installed, check <code>docker compose version<\/code> first. Do not mix an old <code>docker.io<\/code> or standalone <code>docker-compose<\/code> installation with Docker&#8217;s <code>docker-ce<\/code> packages until you have identified which installation owns your existing containers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Give your SSH user controlled Docker access<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default. The <code>docker<\/code> group removes repeated <code>sudo<\/code> prompts, but membership is effectively administrative access to the VPS.<\/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\">Disconnect from SSH and open a new session so the group membership is loaded. Then confirm that the account can reach the daemon:<\/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 include <code>docker<\/code>, followed by the Docker server version. Until you reconnect, use <code>sudo docker compose ...<\/code> or run <code>newgrp docker<\/code> for the current shell. For a stricter privilege boundary, use Docker&#8217;s rootless mode instead of adding a user to this group.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Create an Apache and MariaDB Compose project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A Compose file keeps service definitions, the private network, and persistent storage together. This example serves a predictable page with Apache and starts MariaDB with a health check, while binding Apache only to the VPS loopback interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create the project directory and its test page:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/compose-jammy\/public\ncd ~\/compose-jammy\nprintf '%s\\n' 'Docker Compose on Ubuntu 22.04 is running.' &gt; public\/index.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a protected environment file. Replace both uppercase values with long, unique passwords before starting the stack; they are placeholders, not safe credentials.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tee .env &gt;\/dev\/null &lt;&lt;'EOF'\nMARIADB_ROOT_PASSWORD=YOUR_STRONG_ROOT_PASSWORD\nMARIADB_PASSWORD=YOUR_STRONG_APP_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: httpd:2.4-alpine\n    ports:\n      - \"127.0.0.1:8081:80\"\n    volumes:\n      - .\/public:\/usr\/local\/apache2\/htdocs:ro\n    depends_on:\n      database:\n        condition: service_healthy\n\n  database:\n    image: mariadb:11\n    environment:\n      MARIADB_DATABASE: compose_demo\n      MARIADB_USER: compose_app\n      MARIADB_PASSWORD: ${MARIADB_PASSWORD:?Set MARIADB_PASSWORD in .env}\n      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?Set MARIADB_ROOT_PASSWORD in .env}\n    volumes:\n      - mariadb_data:\/var\/lib\/mysql\n    healthcheck:\n      test: [\"CMD\", \"healthcheck.sh\", \"--connect\", \"--innodb_initialized\"]\n      interval: 10s\n      timeout: 5s\n      retries: 12\n\nvolumes:\n  mariadb_data:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The read-only bind mount prevents Apache from changing the page, and the named <code>mariadb_data<\/code> volume survives container replacement. MariaDB has no <code>ports<\/code> entry, so it is reachable only from the Compose network; Apache&#8217;s <code>127.0.0.1<\/code> binding likewise prevents direct public access. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a> explains the service, volume, and dependency keys you can add to a real application.<\/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 Compose configuration before pulling images so a YAML error or missing password is caught early.<\/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\"><code>config --quiet<\/code> should produce no output and return status 0. The first pull may take a few minutes. Both services should then be running, with the database marked healthy and Apache bound to loopback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAME                         IMAGE             SERVICE    STATUS                   PORTS\ncompose-jammy-web-1          httpd:2.4-alpine  web        Up                       127.0.0.1:8081-&gt;80\/tcp\ncompose-jammy-database-1     mariadb:11        database   Up (healthy)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Container names can differ if you use another directory. Select services by the <code>SERVICE<\/code> column, such as <code>web<\/code> or <code>database<\/code>, in later commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on Ubuntu 22.04<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test the page and make a database query from inside the database container. Together, these checks prove that Compose can start both services and that the persistent database accepts connections.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --fail --silent --show-error http:\/\/127.0.0.1:8081\ndocker compose exec database sh -c 'mariadb -u\"$MARIADB_USER\" -p\"$MARIADB_PASSWORD\" -e \"SELECT 1 AS compose_ok\" \"$MARIADB_DATABASE\"'\ndocker compose logs --tail 20 web database<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command should print the page text, and the query should return one:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Docker Compose on Ubuntu 22.04 is running.\ncompose_ok\n1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because port 8081 is bound to <code>127.0.0.1<\/code>, a browser on your workstation cannot reach it directly. For private browser testing, create an SSH tunnel from your workstation with <code>ssh -L 8081:127.0.0.1:8081 USER@SERVER_IP<\/code>, then open <code>http:\/\/127.0.0.1:8081<\/code> locally. If you later publish an application, put it behind TLS and apply a deliberate firewall policy; Docker warns that published container ports can bypass ordinary UFW rules. VPSLake&#8217;s <a href=\"https:\/\/vpslake.com\/ufw-firewall-generator\">UFW firewall generator<\/a> can help draft a host policy.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on Jammy\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Ubuntu VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Run Compose on Jammy<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Launch isolated Docker Compose services on an Ubuntu 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 Compose CLI plugin is missing, or your shell is using a different Docker installation than the one configured above. Refresh the Jammy package index and install the plugin:<\/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 <code>docker compose<\/code> with a space. The hyphenated <code>docker-compose<\/code> command belongs to the legacy standalone installation and is not required for Compose v2.<\/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\">The current SSH session has not loaded the new <code>docker<\/code> group, or the account was not added to it. Reconnect, run <code>id -nG<\/code>, and retry; <code>sudo docker compose ...<\/code> is a temporary workaround. Remember that <code>docker<\/code> group membership grants root-equivalent control.<\/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 Docker&#8217;s Jammy repository, commonly because the source file has the wrong suite or the previous update failed. Inspect the source and refresh the package index:<\/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 should contain <code>Suites: jammy<\/code>, and <code>apt update<\/code> must complete without an error. If <code>docker.io<\/code> is already installed, do not blindly remove it on a server with existing workloads; plan the package migration first.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>dependency failed to start: container compose-jammy-database-1 is unhealthy<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Read the database logs and check that <code>.env<\/code> contains both password variables with no spaces around the equals signs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose logs database\ndocker compose config<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">MariaDB stores its initial credentials in <code>mariadb_data<\/code>. Correcting <code>.env<\/code> does not change a password in an already initialized volume. For a disposable test only, <code>docker compose down -v<\/code> deletes that volume before a clean <code>docker compose up -d<\/code>; never use <code>-v<\/code> when the database 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 move production secrets to a managed secret store.<\/li>\n\n\n<li>Keep application ports on loopback until authentication, TLS, and the firewall policy are ready. Do not expose MariaDB to the public internet.<\/li>\n\n\n<li>Back up <code>mariadb_data<\/code> and test restoring it before treating the stack as production data.<\/li>\n\n\n<li>Pin production images to reviewed tags or digests, then update deliberately with <code>docker compose pull<\/code> followed by <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 included when I install Docker on Ubuntu 22.04?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">It is available as the <code>docker-compose-plugin<\/code> package when you install Docker Engine from Docker&#8217;s official APT repository. Verify the installation with <code>docker compose version<\/code>; the old <code>docker-compose<\/code> executable is a separate legacy tool.<\/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 recognizes both names, but <code>compose.yaml<\/code> is the current default filename and makes the newer workflow clear. If an existing project uses <code>docker-compose.yml<\/code>, you can keep it or select another file with <code>docker compose -f FILE up -d<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does <code>docker compose down<\/code> delete the MariaDB data?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Normal <code>down<\/code> removes the containers and Compose network but retains the named <code>mariadb_data<\/code> volume. Adding <code>-v<\/code> also deletes that volume, so reserve it for resetting disposable test data.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I expose the web service safely?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the sample bound to loopback and place a TLS reverse proxy in front of it, usually on ports 80 and 443. Allow only the required public ports in UFW, account for Docker&#8217;s packet-filtering behavior, and never publish MariaDB just to make the application work.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Is Docker Compose included when I install Docker on Ubuntu 22.04?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"It is available as the `docker-compose-plugin` package when you install Docker Engine from Docker's official APT repository. Verify the installation with `docker compose version`; the old `docker-compose` executable is a separate legacy tool.\"}}, {\"@type\": \"Question\", \"name\": \"Why does this guide use `compose.yaml` instead of `docker-compose.yml`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Compose v2 recognizes both names, but `compose.yaml` is the current default filename and makes the newer workflow clear. If an existing project uses `docker-compose.yml`, you can keep it or select another file with `docker compose -f FILE up -d`.\"}}, {\"@type\": \"Question\", \"name\": \"Does `docker compose down` delete the MariaDB data?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Normal `down` removes the containers and Compose network but retains the named `mariadb_data` volume. Adding `-v` also deletes that volume, so reserve it for resetting disposable test data.\"}}, {\"@type\": \"Question\", \"name\": \"How do I expose the web service safely?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Keep the sample bound to loopback and place a TLS reverse proxy in front of it, usually on ports 80 and 443. Allow only the required public ports in UFW, account for Docker's packet-filtering behavior, and never publish MariaDB just to make the application work.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Ubuntu 22.04 with Docker&#8217;s official repository, launch a two-service stack, verify it locally, and fix common errors.","protected":false},"author":1,"featured_media":1002,"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,101],"class_list":["post-1001","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-docker","tag-docker-compose","tag-ubuntu-22-04","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1001","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=1001"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1001\/revisions"}],"predecessor-version":[{"id":3883,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1001\/revisions\/3883"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1002"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1001"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1001"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1001"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}