{"id":999,"date":"2026-11-04T03:00:00","date_gmt":"2026-11-04T03:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=999"},"modified":"2026-11-04T03:00:00","modified_gmt":"2026-11-04T03:00:00","slug":"install-docker-compose-debian-13","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/04\/install-docker-compose-debian-13\/","title":{"rendered":"How to Install Docker Compose on Debian 13"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Compose on Debian 13 by adding Docker&#8217;s signed repository, installing the Compose v2 plugin, and launching a small Nginx and PostgreSQL stack. You can complete the setup in about ten minutes and verify it entirely from your SSH session without exposing the database 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 Debian 13 (Trixie) 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>Permission to download packages from Docker&#8217;s APT repository and images from 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>No domain is needed for this private test stack. Use a domain later if you put a TLS reverse proxy in front of a public application.<\/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 logged in as <code>root<\/code>, remove <code>sudo<\/code> from the commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm Debian 13 and refresh APT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s repository must be configured for Trixie&#8217;s <code>trixie<\/code> codename, so confirm the release and architecture before changing package 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\">You should see Debian 13 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 13 (trixie)\"\nVERSION_CODENAME=trixie\namd64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If <code>apt update<\/code> reports a broken mirror, DNS error, or unsigned repository, fix that first. Compose cannot install until Debian&#8217;s package metadata is healthy.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s official 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> approach.<\/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: trixie\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 has a Docker Engine candidate for Trixie:<\/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 reference <code>trixie<\/code>. Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/debian\/\" target=\"_blank\" rel=\"noopener\">Debian installation documentation<\/a> lists Trixie 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 the Compose plugin<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Compose v2 is a Docker CLI plugin, so install it alongside the Engine instead of using the obsolete hyphenated <code>docker-compose<\/code> command.<\/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 print <code>active<\/code>, followed by Docker client\/server versions and a Compose v2 version. Exact version 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 Engine is already installed, run the last three commands first. You only need the repository and package installation when <code>docker compose version<\/code> is missing or the Engine is not present. Avoid mixing an old <code>docker.io<\/code> installation with <code>docker-ce<\/code>; plan any migration before removing packages that may own existing containers or volumes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Let your SSH user run Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker socket is root-owned by default. Adding your account to the <code>docker<\/code> group avoids repeated <code>sudo<\/code>, but that group grants root-equivalent control over the server.<\/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 membership is loaded, then check 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 include <code>docker<\/code>, and <code>docker info<\/code> should print the server version. Until reconnecting, use <code>sudo docker compose ...<\/code> or run <code>newgrp docker<\/code> for the current shell.<\/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\">A Compose file describes the services, network, storage, and startup relationship in one place. This example keeps Nginx on the VPS loopback address and gives PostgreSQL a private network plus a named data volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create the project and a page for Nginx:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/compose-debian13\/site\ncd ~\/compose-debian13\nprintf '%s\\n' 'Docker Compose on Debian 13 is running.' &gt; site\/index.html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create an environment file for the database password. Replace <code>YOUR_STRONG_POSTGRES_PASSWORD<\/code> with a long, unique password before starting the stack; it is a placeholder, not a usable credential.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tee .env &gt;\/dev\/null &lt;&lt;'EOF'\nPOSTGRES_PASSWORD=YOUR_STRONG_POSTGRES_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:8090:80\"\n    volumes:\n      - .\/site:\/usr\/share\/nginx\/html:ro\n    depends_on:\n      db:\n        condition: service_healthy\n\n  db:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_DB: appdb\n      POSTGRES_USER: appuser\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}\n    volumes:\n      - pg_data:\/var\/lib\/postgresql\/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U appuser -d appdb\"]\n      interval: 5s\n      timeout: 5s\n      retries: 10\n\nvolumes:\n  pg_data:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>:ro<\/code> mount stops Nginx from changing the page, while <code>pg_data<\/code> keeps PostgreSQL data when containers are recreated. The database has no <code>ports<\/code> entry, so it is not published on the VPS network. See Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/reference\/compose-file\/\" target=\"_blank\" rel=\"noopener\">Compose file reference<\/a> when adding application-specific services or secrets.<\/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 rendered YAML before pulling images; this catches indentation errors and a missing password before containers are created.<\/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 return no output and status 0. The first image pull can take a few minutes. Both services should then show <code>Up<\/code> or <code>running<\/code>, with the web service bound to <code>127.0.0.1:8090<\/code> and the database marked healthy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NAME                    SERVICE   STATUS                   PORTS\ncompose-debian13-web-1  web       Up                       127.0.0.1:8090-&gt;80\/tcp\ncompose-debian13-db-1   db        Up (healthy)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker Compose on Debian 13<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Check the HTTP response, database readiness, and recent logs. These checks prove that Compose installed correctly and that both services can start together.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --fail --silent --show-error http:\/\/127.0.0.1:8090\ndocker compose exec db pg_isready -U appuser -d appdb\ndocker compose logs --tail 20 web db<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command should print the page text, and PostgreSQL should report that it is accepting connections:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Docker Compose on Debian 13 is running.\n\/var\/run\/postgresql:5432 - accepting connections<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The port is intentionally bound to loopback, so a browser on your own computer cannot reach it directly. If you need to inspect it remotely, use an SSH tunnel or put a TLS reverse proxy in front of the application; do not publish PostgreSQL. Docker notes that published container ports can bypass ordinary UFW rules, so use a deliberate <code>DOCKER-USER<\/code> policy before exposing a production service. 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 Trixie\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Debian 13 VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Run Compose on Trixie<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Launch isolated Docker Compose services 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 CLI plugin is missing, or the shell is using a different Docker installation. Refresh APT 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 <code>docker compose<\/code> with a space. The standalone <code>docker-compose<\/code> binary is a separate legacy installation and is not required here.<\/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 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.<\/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 Trixie repository, usually because the source file has the wrong suite or the last update failed. Inspect the file and refresh the 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 must contain <code>Suites: trixie<\/code>, and <code>apt update<\/code> must finish without an error. Do not replace <code>trixie<\/code> with <code>bookworm<\/code>; that installs packages for the wrong Debian release.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>dependency failed to start: container db is unhealthy<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Read the database log and confirm that <code>.env<\/code> contains <code>POSTGRES_PASSWORD<\/code> with no surrounding quotes or spaces:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose logs db\ndocker compose config<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If this project has never started, correct the value and run <code>docker compose up -d<\/code> again. PostgreSQL stores its initial password in the named volume, so changing <code>.env<\/code> does not change credentials in an existing database. Only for disposable test data, <code>docker compose down -v<\/code> removes that volume; never run it 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> mode <code>600<\/code>, never commit it to source control, and move production secrets to a managed secret store.<\/li>\n\n\n<li>Keep PostgreSQL unexposed and leave the web service on <code>127.0.0.1<\/code> until TLS, authentication, and firewall policy are ready.<\/li>\n\n\n<li>Back up <code>pg_data<\/code> and test restoration 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> 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\">Do I need the old <code>docker-compose<\/code> command on Debian 13?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Docker Compose v2 is provided as the <code>docker compose<\/code> CLI plugin, which is the maintained syntax used by this guide. The hyphenated command belongs to the legacy standalone tool and can create confusion when both installations exist.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I open the Nginx page from my laptop?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not with the sample binding, because <code>127.0.0.1<\/code> refers to the VPS itself. You can create an SSH tunnel to port 8090 for private testing, or publish only a protected reverse proxy after adding TLS and an appropriate firewall policy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does <code>docker compose down<\/code> delete PostgreSQL data?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Regular <code>down<\/code> removes the containers and Compose network but keeps the named <code>pg_data<\/code> volume. The <code>-v<\/code> option also deletes that volume, so reserve it for a disposable reset after confirming that no data is needed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I stop or restart the project later?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">From <code>~\/compose-debian13<\/code>, use <code>docker compose stop<\/code> for a temporary stop and <code>docker compose start<\/code> to bring the existing containers back. Use <code>docker compose up -d<\/code> after changing the Compose file so Compose can recreate services when necessary.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Do I need the old `docker-compose` command on Debian 13?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Docker Compose v2 is provided as the `docker compose` CLI plugin, which is the maintained syntax used by this guide. The hyphenated command belongs to the legacy standalone tool and can create confusion when both installations exist.\"}}, {\"@type\": \"Question\", \"name\": \"Can I open the Nginx page from my laptop?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Not with the sample binding, because `127.0.0.1` refers to the VPS itself. You can create an SSH tunnel to port 8090 for private testing, or publish only a protected reverse proxy after adding TLS and an appropriate firewall policy.\"}}, {\"@type\": \"Question\", \"name\": \"Does `docker compose down` delete PostgreSQL data?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Regular `down` removes the containers and Compose network but keeps the named `pg_data` volume. The `-v` option also deletes that volume, so reserve it for a disposable reset after confirming that no data is needed.\"}}, {\"@type\": \"Question\", \"name\": \"How do I stop or restart the project later?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"From `~\/compose-debian13`, use `docker compose stop` for a temporary stop and `docker compose start` to bring the existing containers back. Use `docker compose up -d` after changing the Compose file so Compose can recreate services when necessary.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Compose on Debian 13 with Docker&#8217;s official repository, launch an Nginx and PostgreSQL stack, verify it, and fix common errors.","protected":false},"author":1,"featured_media":1000,"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,98,118,120],"class_list":["post-999","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-debian-13","tag-docker","tag-docker-compose","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/999","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=999"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/999\/revisions"}],"predecessor-version":[{"id":3878,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/999\/revisions\/3878"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1000"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=999"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=999"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=999"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}