{"id":997,"date":"2026-11-03T18:00:00","date_gmt":"2026-11-03T18:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=997"},"modified":"2026-11-03T18:00:00","modified_gmt":"2026-11-03T18:00:00","slug":"install-docker-compose-debian-12","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/03\/install-docker-compose-debian-12\/","title":{"rendered":"How to Install Docker Compose on Debian 12"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Debian 12 using Docker&#8217;s current Compose v2 plugin, then launch and manage a small web-and-cache stack. The setup takes about ten minutes and leaves you with commands you can reuse for real multi-container applications.<\/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 Debian 12 (Bookworm) 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 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 a Debian server with root access.<\/li>\n\n\n<li>A domain is not required for this local verification example. Add one later when putting a TLS reverse proxy in front of a public service.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands use <code>sudo<\/code> for a normal SSH account. If you are logged in as <code>root<\/code>, remove <code>sudo<\/code> and do not add a second <code>sudo<\/code> prefix.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Debian 12 and update APT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s repository must use Bookworm, so confirm the release before changing APT sources.<\/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\">Expected output includes <code>VERSION_CODENAME=bookworm<\/code> 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=\"Debian GNU\/Linux 12 (bookworm)\"\nVERSION_CODENAME=bookworm\namd64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not continue while <code>apt update<\/code> reports a broken mirror, DNS failure, or expired repository. Docker&#8217;s packages cannot fix an unhealthy Debian package configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s signed Debian repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The dedicated keyring and deb822 source file avoid the deprecated <code>apt-key<\/code> workflow and let APT verify Docker packages.<\/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\/debian\/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\/debian\nSuites: $(. \/etc\/os-release &amp;&amp; echo \"$VERSION_CODENAME\")\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\">Confirm that APT can see a Docker Engine candidate for Bookworm:<\/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 candidate should come from <code>download.docker.com<\/code> and show <code>bookworm<\/code> in its repository line. Docker lists the supported Debian releases and the repository method in its <a href=\"https:\/\/docs.docker.com\/engine\/install\/debian\/\" target=\"_blank\" rel=\"noopener\">official Debian installation guide<\/a>.<\/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 installed as <code>docker compose<\/code>, a subcommand provided by Docker&#8217;s Compose plugin; the old standalone <code>docker-compose<\/code> command is not needed.<\/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 service, matching client and server versions, and a Compose v2 version. Exact 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\">If Docker was already installed, you can skip the repository and package commands, but still run <code>docker compose version<\/code>. If the command is missing, install <code>docker-compose-plugin<\/code> from the same Docker repository.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Allow a non-root account to run Compose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s Unix socket is root-owned by default. The <code>docker<\/code> group removes repeated <code>sudo<\/code> prompts, but membership grants root-equivalent control over the VPS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this for the Linux account that will manage the project:<\/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 reconnect so the new group is loaded. Then check the group and daemon connection:<\/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 first command should include <code>docker<\/code>. Until you reconnect, prefix Docker commands with <code>sudo<\/code> or run <code>newgrp docker<\/code> in the current shell. Treat this group as administrative access; use rootless Docker when you need a stronger boundary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Create a Compose project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Compose reads a YAML file to define services, storage, and networking. This example runs Nginx on loopback and Redis with a named volume, so it demonstrates two services without exposing a new public port. The <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a> documents the available keys when you extend this example.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a project directory and a page for Nginx:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/compose-demo\/site\ncd ~\/compose-demo\nprintf '%s\\n' 'Docker Compose on Debian 12 is working.' &gt; site\/index.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>compose.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tee compose.yaml &gt;\/dev\/null &lt;&lt;'YAML'\nservices:\n  web:\n    image: nginx:alpine\n    ports:\n      - \"127.0.0.1:8080:80\"\n    volumes:\n      - .\/site:\/usr\/share\/nginx\/html:ro\n    depends_on:\n      - cache\n\n  cache:\n    image: redis:7-alpine\n    command: [\"redis-server\", \"--appendonly\", \"yes\"]\n    volumes:\n      - redis_data:\/data\n\nvolumes:\n  redis_data:\nYAML<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>web<\/code> service maps only to <code>127.0.0.1<\/code>, while <code>cache<\/code> stays on Compose&#8217;s private network. The named volume keeps Redis data outside the container lifecycle; the <code>:ro<\/code> suffix prevents Nginx from changing the website files.<\/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\">Validate the fully resolved configuration before downloading images or creating containers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose config\ndocker compose pull\ndocker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>config<\/code> should print the two services and <code>redis_data<\/code> without a YAML error. The first pull may take a few minutes; <code>up -d<\/code> returns control of the terminal while both services run in the background.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check the service state:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose ps<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output is similar to this, with both services showing <code>Up<\/code> or <code>running<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAME                  IMAGE          SERVICE   STATUS       PORTS\ncompose-demo-web-1    nginx:alpine   web       Up           127.0.0.1:8080-&gt;80\/tcp\ncompose-demo-cache-1  redis:7-alpine cache     Up<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The generated container names can differ if you choose another project directory. Use the <code>SERVICE<\/code> column when selecting a service in later commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on Debian 12<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Verify both the HTTP response and the Redis service, then inspect the project logs if either check fails.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --fail --silent --show-error http:\/\/127.0.0.1:8080\ndocker compose exec cache redis-cli ping\ndocker compose logs --tail 20<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first two commands should return the page text and <code>PONG<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Docker Compose on Debian 12 is working.\nPONG<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>web<\/code> port is deliberately reachable only from the VPS itself. To make a service public, change the mapping to <code>\"8080:80\"<\/code>, allow that exact port in your firewall, and put a TLS reverse proxy in front of it. Docker warns that published ports can bypass ordinary UFW rules; plan the <code>DOCKER-USER<\/code> policy before exposing production services. VPSLake&#8217;s <a href=\"https:\/\/vpslake.com\/ufw-firewall-generator\">UFW firewall generator<\/a> can help draft a host rule set.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Compose on Bookworm\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Debian VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Run Compose on Bookworm<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Deploy Docker Compose projects on a Debian 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 plugin is missing, or the CLI is using a different installation than the one managed by Docker&#8217;s repository. Check the package and install it with APT:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apt-cache policy docker-compose-plugin\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 legacy hyphenated <code>docker-compose<\/code> binary is a separate tool and is not required for this guide.<\/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 SSH session does not yet know about the <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 while you arrange the session change.<\/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 has not loaded the Docker Bookworm index, often because the source file has the wrong codename or an earlier <code>apt update<\/code> failed. Inspect the source and refresh metadata:<\/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: bookworm<\/code>, and <code>apt update<\/code> must finish without an error. Do not mix a Debian <code>docker.io<\/code> installation with Docker&#8217;s <code>docker-ce<\/code> packages; remove or account for conflicting packages first.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>curl: (7) Failed to connect<\/code> or the web check returns connection refused<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The web service may still be pulling its image, may have exited, or the port is bound only to the VPS loopback address. Review the state and logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose ps\ndocker compose logs web\nss -ltnp | grep ':8080'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If <code>web<\/code> is not running, the log normally identifies a bad mount or configuration. If you are testing from your own computer, <code>127.0.0.1:8080<\/code> is the VPS&#8217;s loopback\u2014not your computer&#8217;s; use an SSH tunnel or configure a protected public reverse proxy instead.<\/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 Docker&#8217;s daemon on its Unix socket. Never expose an unauthenticated Docker API on port 2375.<\/li>\n\n\n<li>Keep application ports on loopback until TLS, authentication, and firewall policy are ready.<\/li>\n\n\n<li>Back up important named volumes and use <code>docker compose down -v<\/code> only when you intentionally want to delete the project&#8217;s stored data.<\/li>\n\n\n<li>Pin production images to reviewed tags or digests, and update them 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 already included with Docker on Debian 12?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">It is included when you install Docker Engine from Docker&#8217;s official APT repository together with <code>docker-compose-plugin<\/code>. Debian&#8217;s separate packages or an older manual Docker install may not include the plugin, so verify with <code>docker compose version<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between <code>docker compose up<\/code> and <code>docker compose start<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>up<\/code> creates or recreates the services, networks, and volumes described by the Compose file, then starts them. <code>start<\/code> only starts existing stopped containers and does not create a missing project, so use <code>up -d<\/code> after changing configuration or setting up a project for the first time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does <code>docker compose down<\/code> delete the Redis data?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. The regular <code>down<\/code> command removes the containers and default network but leaves the named <code>redis_data<\/code> volume. <code>docker compose down -v<\/code> also removes that volume, so use the <code>-v<\/code> option only when resetting the data is intentional.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do Compose services find each other?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Compose puts services on a project network and provides DNS names matching their service keys. A container can reach the Redis service at <code>cache:6379<\/code>; it should not use the host&#8217;s <code>localhost<\/code>, which points back to the calling container.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Is Docker Compose already included with Docker on Debian 12?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"It is included when you install Docker Engine from Docker's official APT repository together with `docker-compose-plugin`. Debian's separate packages or an older manual Docker install may not include the plugin, so verify with `docker compose version`.\"}}, {\"@type\": \"Question\", \"name\": \"What is the difference between `docker compose up` and `docker compose start`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"`up` creates or recreates the services, networks, and volumes described by the Compose file, then starts them. `start` only starts existing stopped containers and does not create a missing project, so use `up -d` after changing configuration or setting up a project for the first time.\"}}, {\"@type\": \"Question\", \"name\": \"Does `docker compose down` delete the Redis data?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. The regular `down` command removes the containers and default network but leaves the named `redis_data` volume. `docker compose down -v` also removes that volume, so use the `-v` option only when resetting the data is intentional.\"}}, {\"@type\": \"Question\", \"name\": \"How do Compose services find each other?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Compose puts services on a project network and provides DNS names matching their service keys. A container can reach the Redis service at `cache:6379`; it should not use the host's `localhost`, which points back to the calling container.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Debian 12 with Docker&#8217;s official repository, run a two-service stack, verify it, fix common errors, and start securely.","protected":false},"author":1,"featured_media":998,"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,97,118,120],"class_list":["post-997","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-debian-12","tag-docker","tag-docker-compose","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/997","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=997"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/997\/revisions"}],"predecessor-version":[{"id":3873,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/997\/revisions\/3873"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/998"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=997"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=997"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=997"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}