VMHeaven

Guides

Install Docker on Ubuntu 24.04 and Debian 12/13 from the official repository

Install Docker Engine and Compose from Docker's apt repository on Ubuntu 24.04 or Debian 12/13, run it without sudo and stop it bypassing your firewall.

Published ~11 min read

To install Docker on Ubuntu 24.04 or Debian 12/13, remove the distribution’s conflicting packages, add Docker’s signing key and its apt repository on download.docker.com, then install docker-ce with the CLI, containerd.io and the Buildx and Compose plugins. The daemon starts by itself, and sudo docker run --rm hello-world confirms it works. The block below does the whole job on Ubuntu 24.04 and 22.04 and on Debian 12 and 13: it reads the distribution and its codename (noble, jammy, bookworm or trixie) from /etc/os-release.

Ubuntu 24.04 · 22.04 · Debian 12 · 13
# 1. remove conflicting packages
#    ("not installed" and "Unable to locate package" messages are harmless)
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
  sudo apt-get remove -y $pkg
done

# 2. Docker's signing key
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release                  # sets $ID (ubuntu or debian) and $VERSION_CODENAME
sudo curl -fsSL https://download.docker.com/linux/$ID/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 3. Docker's repository
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: $VERSION_CODENAME
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

# 4. Engine, CLI, containerd, Buildx and Compose
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 5. test
sudo docker run --rm hello-world
docker --version && docker compose version

If hello-world prints “Hello from Docker!”, the engine works. The sections below explain each step, cover Rocky Linux, and deal with the three things that catch people out on a fresh server: sudo, the firewall and the disk.

Which install method should you use?

There are three common ways to get Docker onto a server, and they are not equivalent:

  • The distribution’s docker.io package (apt install docker.io). Docker Engine as built and patched by Ubuntu or Debian. It usually trails Docker’s own releases, and Compose and Buildx come as separately named packages. Fine for a quick test.
  • Docker’s own apt repository, the method in this guide. Current releases, every component from one source, updates with the rest of the system, and the setup Docker’s documentation and most tutorials assume. The right default for a server you will keep.
  • The convenience script from get.docker.com. It sets up the same repository in one command, without asking anything. Docker does not recommend it for production; it is covered in docker: command not found.

Whichever you pick, use only one. docker.io and docker-ce conflict, which is why step 1 removes the distribution packages first. Skip the snap (snap install docker) on a server: it limits file access to your home directory and does not create the docker group.

How do I install Docker on Ubuntu 24.04 or 22.04?

Here is the same procedure with Ubuntu’s URLs written out, so you can see what each step does. It works unchanged on 24.04 (noble) and 22.04 (jammy).

  1. 1Remove conflicting packages

    Ubuntu’s docker.io, docker-compose-v2, containerd and runc clash with Docker’s packages. Removing them leaves images and volumes in /var/lib/docker alone.

    Ubuntu
    for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
      sudo apt-get remove -y $pkg
    done
  2. 2Add Docker's signing key
    Ubuntu
    sudo apt-get update
    sudo apt-get install -y ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc

    The key sits in /etc/apt/keyrings and is referenced only by Docker’s source, so it cannot vouch for packages from any other repository. That is the modern replacement for the deprecated apt-key add.

  3. 3Add the repository
    Ubuntu
    sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
    Types: deb
    URIs: https://download.docker.com/linux/ubuntu
    Suites: $(. /etc/os-release && echo $VERSION_CODENAME)
    Components: stable
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/docker.asc
    EOF

    This is the deb822 format Ubuntu 24.04 uses for its own sources. The one-line alternative in /etc/apt/sources.list.d/docker.list works just as well; use one of the two, not both. On a derivative such as Linux Mint, replace VERSION_CODENAME with UBUNTU_CODENAME.

  4. 4Install the packages
    Ubuntu
    sudo apt-get update
    apt-cache policy docker-ce        # the candidate must come from download.docker.com
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  5. 5Check that it runs
    Ubuntu
    systemctl status docker --no-pager       # active (running), enabled at boot
    sudo docker run --rm hello-world
    sudo docker version                      # shows both Client and Server

How do I install Docker on Debian 12 and 13?

Exactly as on Ubuntu, with debian in both URLs. Docker publishes packages for Debian 12 (bookworm) and Debian 13 (trixie), so the codename from /etc/os-release is all that changes:

Debian 12 · 13 — after the removal loop from step 1
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo $VERSION_CODENAME)
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  • No sudo? A Debian install with a root password has no sudo. Run the commands as root without it, or see sudo: command not found.
  • iptables on Debian 12 and 13. docker-ce pulls in the iptables package, which is the nftables-backed variant on both releases. Docker uses it for its rules out of the box; nothing to configure.
  • “The repository … does not have a Release file” means Docker has no packages for that codename, or the URL says ubuntu on a Debian machine (or the other way round). Check with cat /etc/apt/sources.list.d/docker.sources.

How do I install Docker on Rocky Linux 9?

On Rocky, AlmaLinux and RHEL, add Docker’s .repo file with dnf config-manager. Unlike on Debian and Ubuntu, the service is not started for you:

Rocky Linux 9
sudo dnf remove -y podman runc        # only if present; they conflict
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world

On CentOS Stream 9 the repository path is /linux/centos/docker-ce.repo. The firewall caveat further down applies to firewalld too: ports a container publishes stay reachable even if firewalld’s public zone does not list them.

How do I run docker without sudo?

Add your user to the docker group, then start a new login session:

docker without sudo
sudo usermod -aG docker $USER
# log out and back in (or: newgrp docker for this shell only)
docker run --rm hello-world

Why are my container ports reachable even though UFW blocks them?

Because Docker writes its own iptables rules, and the traffic never passes through UFW’s. When you publish a port (-p 8080:80, or ports: in Compose), Docker adds a NAT rule that redirects incoming packets to the container’s address. Those packets then go through the kernel’s FORWARD chain, not the INPUT chain where UFW’s allow and deny rules sit. So ufw deny 8080 changes nothing, ufw status does not even show the port, and a database or admin panel published this way is open to the whole internet.

This is the most common Docker mistake on a VPS. Test from a second machine, not from the server itself:

from your own PC
nc -zv 203.0.113.10 8080          # "succeeded" = reachable from the internet

More ways to test, and how to read the results, are in how to check open ports on Linux. There are two clean fixes.

1. Publish internal services on 127.0.0.1, or not at all. A port bound to the loopback address is reachable only from the server itself, for example by a reverse proxy on the host. Containers in the same Compose project reach each other by service name over the project’s network and need no published port.

compose.yaml
services:
  app:
    image: nginx
    ports:
      - "127.0.0.1:8080:80"        # only the host can reach this
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: change-me
    # no "ports:" — app reaches it as db:5432 on the Compose network

With docker run it is -p 127.0.0.1:8080:80. As a safety net, setting "ip": "127.0.0.1" in /etc/docker/daemon.json makes loopback the default for every published port; a port meant to be public then has to say so, as 0.0.0.0:443:443.

2. Filter in the DOCKER-USER chain when a port must stay public but only for some addresses. Docker evaluates this chain before its own rules and leaves what you put in it alone. Match on the original destination port, because the address has already been rewritten by the time the packet gets there:

allow port 8080 only from 203.0.113.7
ip route show default             # your public interface, e.g. eth0 or ens18

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.7 -p tcp \
  -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP

Rules added by hand are gone after a reboot. To keep them, put them in a block at the end of /etc/ufw/after.rules and run sudo ufw reload; the open-source ufw-docker helper automates exactly that.

end of /etc/ufw/after.rules
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 ! -s 203.0.113.7 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP
-A DOCKER-USER -j RETURN
COMMIT

How do I stop container logs from filling the disk?

Limit them in /etc/docker/daemon.json. Docker’s default json-file log driver keeps everything a container writes to stdout, with no size limit, so one chatty container can fill a small disk within weeks. Rotation at 10 MB with three files caps each container at about 30 MB:

/etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
apply it
sudo dockerd --validate --config-file=/etc/docker/daemon.json   # "configuration OK"
sudo systemctl restart docker          # stops containers; restart policies bring them back
docker compose up -d --force-recreate  # in each Compose project: old containers keep old settings

If the file already exists, merge the keys instead of overwriting it, and keep the values as strings in quotes. The limits apply only to containers created after the change, which is why the last line recreates them. To see what is using space:

where did the disk go?
docker system df                                   # images, containers, volumes, build cache
sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail
docker image prune -a                              # unused images (asks first)
docker builder prune                               # build cache

If the disk is already full, start with No space left on device.

Should you use docker compose or docker-compose?

Use docker compose, with a space. That is Compose v2, a plugin of the Docker CLI that the docker-compose-plugin package installs. The hyphenated docker-compose is Compose v1, a separate Python program that stopped receiving updates in 2023. The YAML files are the same; only the command changed. If an old script still calls docker-compose, update the script, or add a small wrapper:

/usr/local/bin/docker-compose (optional)
printf '#!/bin/sh\nexec docker compose "$@"\n' | sudo tee /usr/local/bin/docker-compose >/dev/null
sudo chmod +x /usr/local/bin/docker-compose

How do I update or uninstall Docker?

Docker updates come with a normal system upgrade, because the packages come from an apt repository. Unattended upgrades on Debian and Ubuntu take packages only from the distribution’s own archives by default, so Docker itself is not updated automatically. Run the upgrade yourself now and then:

update
sudo apt-get update
sudo apt-get install --only-upgrade docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin
# or simply: sudo apt-get upgrade

An upgrade restarts the daemon, and with it every container. Give your services a restart policy (restart: unless-stopped in Compose) so they come back on their own. Setting "live-restore": true in daemon.json keeps containers running while the daemon restarts, for example during a patch update.

To remove Docker completely, first copy out any volume data you still need: the second line below deletes every image, container and volume for good.

uninstall
sudo apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin \
  docker-compose-plugin docker-ce-rootless-extras
sudo rm -rf /var/lib/docker /var/lib/containerd     # deletes ALL images, containers and volumes
sudo rm /etc/apt/sources.list.d/docker.sources /etc/apt/keyrings/docker.asc

What does a VPS need to run Docker?

Its own kernel. Docker relies on namespaces, cgroups, overlay filesystems and netfilter, and a KVM virtual machine controls all of them, just as a physical server does. On container-based virtualisation such as OpenVZ or LXC, Docker often fails to start or needs workarounds; KVM vs OpenVZ explains the difference and how to tell which one you have. The daemon itself is light. What decides the size of the server is the containers you run, plus disk space for their images.

On VMHeaven, the Ubuntu 24.04 and 22.04, Debian 13 and 12 and Rocky Linux 9 templates take the commands above as written. Ubuntu 20.04 and Debian 11 are still offered, but both are past the end of their regular support, so start a new Docker host on a current release. The entry Standard KVM plan, with 2 cores, 4 GB RAM and 20 GB of storage from €4.99 a month in Amsterdam, runs a handful of small containers such as a reverse proxy, a web app and its database. The Ubuntu VPS and Debian VPS pages list the plans by distribution.

Frequently asked

Is Ubuntu's docker.io package good enough?

For a test or a hobby box, yes: it is Docker Engine, built and security-patched by Ubuntu. It usually trails Docker's own releases, Compose and Buildx come as separately named packages, and Docker's documentation and most tutorials assume Docker's own packages. For a server you will keep, install docker-ce from Docker's apt repository instead, and remove docker.io first, because the two conflict.

Does Docker support Debian 13?

Yes. Docker's apt repository has packages for Debian 13 (trixie) as well as Debian 12 (bookworm). The installation is the same on both: add the repository at download.docker.com/linux/debian with the codename from /etc/os-release, then install docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin and docker-compose-plugin.

Why is my container port open even though UFW blocks it?

Docker writes its own iptables rules for published ports. Incoming packets are redirected to the container and pass through the FORWARD chain, not the INPUT chain where UFW's rules sit, so UFW never sees them. Publish internal services on 127.0.0.1 only (for example -p 127.0.0.1:8080:80), leave the port unpublished if only other containers need it, or filter public ports in the DOCKER-USER chain.

How much RAM does Docker need?

The Docker daemon and containerd are light and need little memory of their own; the containers you run decide the size. A reverse proxy and a small web app fit in a few hundred megabytes, while databases, Java services and anything with a large cache want gigabytes. Check real usage with docker stats and set a memory limit per container so one service cannot starve the others.

Related articles

All guides