Install Docker on Debian 11 with Docker Engine, the Compose v2 plugin, and a working test container in about ten minutes. You will also run a small Nginx container locally and learn the commands to inspect, stop, and remove containers safely.
Prerequisites
- A 64-bit Debian GNU/Linux 11 (Bullseye) VPS. Debian 11 is an older release, so plan an upgrade to a supported Debian version for long-lived production workloads.
- SSH access as
root, or a normal account with workingsudoprivileges. - At least 2 GB of RAM and 10 GB of free disk space for the engine, images, logs, and your first workloads.
- Outbound HTTPS access so APT can reach Docker’s package repository and Docker Hub can provide images.
- A VPSLake Linux VPS if you need a Debian server with root access.
Docker’s package repository still has a Bullseye suite, but Debian 11 itself is no longer the best choice for a new server. Keep the suite name as bullseye in the commands below; do not substitute bookworm or trixie on a Debian 11 host.
Step 1: Confirm Debian 11 and update APT
Confirm the release before adding a repository so packages cannot be mixed accidentally between Debian versions.
cat /etc/os-release
dpkg --print-architecture
sudo apt update
The release should identify Bullseye, and the architecture should normally be amd64 or arm64.
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
amd64
If apt update reports repository errors, fix those first. Docker’s repository cannot make a broken Debian mirror or DNS configuration work.
Step 2: Add Docker’s signed APT repository
The keyring and deb822 source file restrict APT to packages signed by Docker’s repository key; this replaces the obsolete apt-key workflow.
Install the two tools needed to download and validate the repository metadata:
sudo apt 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
Create a source entry that explicitly targets Bullseye:
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<'EOF'
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: bullseye
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
The Architectures line matches the most common VPS architecture. If dpkg --print-architecture returned arm64, change that one line to Architectures: arm64 before running sudo apt update again.
Check that APT sees Docker packages from the expected host:
apt-cache policy docker-ce
You should see a candidate package and a repository URL containing download.docker.com:
docker-ce:
Candidate: 29.x.y-1~debian.11~bullseye
500 https://download.docker.com/linux/debian bullseye/stable amd64 Packages
The exact version changes as Docker publishes updates; do not copy the sample version.
Docker’s Debian installation documentation explains the repository layout and package choices.
Step 3: Install Docker Engine and Compose
Install the daemon, CLI, container runtime, Buildx builder, and Compose v2 plugin together so the commands in this guide use one compatible package set.
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
On Debian, the package normally starts the daemon automatically. The second command makes the desired boot behavior explicit and also starts Docker immediately.
Check the service and installed clients:
sudo systemctl is-active docker
docker --version
docker compose version
Expected output is similar to this, with release numbers that may differ:
active
Docker version 29.x.y, build ...
Docker Compose version v2.x.y
If APT reports a conflict with an existing docker.io, docker-compose, containerd, or runc installation, stop and review that installation before removing packages. Do not delete /var/lib/docker unless you intentionally want to remove existing images, containers, and volumes.
Step 4: Allow a sudo user to run Docker
Docker’s Unix socket is root-owned by default. The docker group is convenient for administration, but membership grants privileges equivalent to root, so add only trusted accounts.
Run this as the non-root account that should manage containers:
DOCKER_USER="$(id -un)"
sudo groupadd --force docker
sudo usermod -aG docker "$DOCKER_USER"
Log out of SSH completely and reconnect so the new group is loaded. As a temporary alternative, start a new shell with newgrp docker. Confirm the account can access the socket:
id -nG
docker info --format '{{.ServerVersion}}'
The group list should contain docker, and the second command should print the Docker Engine version. If you stay logged in as root, you can skip this step and prefix later Docker commands with sudo when needed.
Step 5: Run a known-good test image
The hello-world image checks the complete path from the CLI to the daemon, image registry, container runtime, and terminal output.
docker run --rm hello-world
Docker downloads the image if it is not already cached, creates a short-lived container, prints a confirmation, and removes that container because of --rm.
Hello from Docker!
This message shows that your installation appears to be working correctly.
The image is not a server and will exit immediately. That is expected; it is an installation test, not a long-running workload.
Step 6: Run and manage a web container
A detached Nginx container demonstrates image pulls, port publishing, status checks, logs, and cleanup without exposing a new public firewall port.
docker run -d --name demo-web -p 127.0.0.1:8080:80 nginx:alpine
docker ps --filter name=demo-web
curl -I http://127.0.0.1:8080
The 127.0.0.1 binding means only processes on the VPS can reach port 8080, so no inbound firewall rule is required for this demonstration. The status should show Up, and curl should receive an HTTP response:
CONTAINER ID IMAGE COMMAND STATUS PORTS
... nginx:alpine "/docker-entrypoint…" Up ... 127.0.0.1:8080->80/tcp
HTTP/1.1 200 OK
Useful management commands are:
docker logs demo-web
docker ps -a
docker stop demo-web
docker rm demo-web
docker ps lists running containers, while docker ps -a also shows stopped ones. Stopping preserves a container for later use; removing it deletes that container’s writable layer, but does not delete the nginx:alpine image.
Verify Docker on Debian 11
Run this final check after reconnecting as your normal Docker administrator. It confirms the daemon, Compose plugin, test image, and absence of the demo container.
systemctl is-enabled docker
systemctl is-active docker
docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
docker compose version
docker image ls hello-world
docker ps -a --filter name=demo-web
You want enabled, active, matching client/server version output, a Compose v2 version, and no demo-web row after the cleanup commands. Docker is ready for application images or a Compose project.
Troubleshooting
E: Package 'docker-ce' has no installation candidate
APT did not load Docker’s Bullseye index, or the source file uses the wrong suite or architecture. Check the source and refresh the cache:
cat /etc/apt/sources.list.d/docker.sources
dpkg --print-architecture
sudo apt update
apt-cache policy docker-ce
The source must say Suites: bullseye, and its architecture must match dpkg --print-architecture. Fix any apt update error before retrying the install.
NO_PUBKEY or a signature verification error
APT cannot read or validate the repository key. Re-download the key and ensure it is world-readable, then update the cache:
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 apt update
If the error mentions a system clock, check date -u and repair time synchronization before trusting repository metadata.
permission denied while trying to connect to the Docker daemon socket
Your current SSH session does not include the new docker group. Start a fresh login session or run newgrp docker, then confirm with id -nG. Use sudo docker ... temporarily if you have not yet changed sessions.
Cannot connect to the Docker daemon
The service is stopped or failed during startup. Inspect the unit and recent journal entries, then start it again:
sudo systemctl status docker --no-pager
sudo journalctl -u docker -n 50 --no-pager
sudo systemctl restart docker
If it fails again, look for a conflicting containerd, invalid daemon configuration under /etc/docker/, or a VPS kernel that does not provide the required container features.
The container works locally but not from the Internet
The example intentionally binds port 8080 to 127.0.0.1. Binding a container to 0.0.0.0 exposes it on every host interface, and Docker’s packet-filter rules can bypass ordinary UFW expectations. Before publishing a public port, read Docker’s firewall guidance, permit only the needed service, and place an HTTP service behind TLS and an authenticated reverse proxy.
Hardening
- Keep application ports bound to
127.0.0.1unless they must be public; publish only the host ports you have intentionally filtered. - Keep the Docker Engine and images updated, and remove unused images with
docker image pruneafter reviewing what will be deleted. - Add log rotation before running noisy production workloads; unbounded container logs can consume the VPS disk.
- Treat every member of the
dockergroup as an administrator. For stronger isolation, evaluate Docker rootless mode instead of granting socket access.
FAQ
Can I install Docker on Debian 11 today?
Yes, the commands above target Docker’s Bullseye repository, but Debian 11 is an older operating system release. For a new production VPS, choose a currently supported Debian release and use its matching suite instead of keeping Bullseye indefinitely.
Is docker compose different from docker-compose?
Yes. docker compose is the current Compose v2 CLI plugin installed by the Docker repository packages. The older hyphenated docker-compose command is a separate legacy tool and is not needed for this setup.
Why does Docker need containerd.io?
Docker Engine uses containerd as part of its runtime stack to manage image and container lifecycle operations. Installing the package supplied by Docker keeps that dependency aligned with the Engine packages rather than mixing it with an unrelated distribution version.
How do I expose a container safely?
Start by binding it to loopback and put a reverse proxy on the host if it needs HTTPS, authentication, or rate limiting. If you publish directly on a public interface, account for Docker’s firewall chains and allow only the required source addresses and destination port.