· 6 min read

Docker Install on Ubuntu and Debian: Compose Setup Guide

To install Docker on Ubuntu and Debian, add Docker's official apt repository, install the docker-ce packages plus the Compose plugin, and verify with hello-world. The whole process takes about five minutes on a fresh server, and it gives you current releases instead of the older docker.io package from the distro archive.

This guide covers the repository setup, your first container, a multi-container web app with Docker Compose, and the security settings that are easy to miss. It assumes a fresh Ubuntu (22.04/24.04) or Debian (11/12) server with sudo access. If the server is brand new, do the basics from our Ubuntu initial setup and hardening guide first: non-root user, SSH keys, firewall.

Step 1: Remove conflicting packages

Distro-provided packages conflict with Docker's official ones. Remove them if present (it is fine if apt says some are not installed):

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
  sudo apt-get remove -y $pkg
done

This does not delete existing images or volumes in /var/lib/docker/.

Step 2: Add the official Docker repository

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

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Debian

Same steps, but the URL and key path use debian:

sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Step 3: Install Docker Engine and the Compose plugin

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

Docker Compose is now a plugin, so the command is docker compose (with a space), not the legacy docker-compose binary. Check both components:

sudo systemctl status docker --no-pager
docker compose version

The service is enabled at boot by default on Debian and Ubuntu.

Step 4: Run your first container

sudo docker run --rm hello-world

If you see "Hello from Docker!", the daemon, image pull, and container runtime all work. Now try something useful, such as a throwaway web server:

sudo docker run -d --name test-web -p 127.0.0.1:8080:80 nginx:alpine
curl -I http://127.0.0.1:8080
sudo docker logs test-web
sudo docker rm -f test-web

Note the 127.0.0.1: prefix in the port mapping. We explain why in the security section.

Running docker without sudo (read this first)

You can add your user to the docker group:

sudo usermod -aG docker $USER
newgrp docker

Be aware that membership in the docker group is effectively root access on the host, because a member can start a container that mounts the host filesystem. Only add users you would trust with root. On shared servers, keep using sudo.

Step 5: Deploy a web app with Docker Compose

Compose describes a multi-container application in one file. Here is a small stack: an nginx web front end and a PostgreSQL database that is only reachable from inside the stack.

mkdir -p ~/myapp/html && cd ~/myapp
echo '<h1>It works</h1>' > html/index.html

Create .env for secrets (and keep it out of version control):

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-this-long-random-value
EOF
chmod 600 .env

Create compose.yaml:

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: appdb
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - dbdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  dbdata:

Points worth noting:

Start and inspect it:

docker compose up -d
docker compose ps
docker compose logs -f web
curl http://127.0.0.1:8080

Daily operations:

docker compose pull && docker compose up -d   # update images
docker compose down                           # stop, keep volumes
docker compose down -v                        # stop AND delete volumes (data loss)

Security tips that matter

1. Published ports bypass UFW

This is the most common surprise. Docker writes its own iptables rules, so -p 8080:80 can be reachable from the internet even if UFW says port 8080 is denied. Options:

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -p tcp --dport 8080 -j DROP

Replace eth0 and the source range with your own. Persist rules with your normal firewall tooling, and test from an outside host with nmap or curl.

2. Never expose the Docker API

Do not enable tcp://0.0.0.0:2375. It is unauthenticated root access. If you need remote management, use SSH (docker context create with an ssh:// endpoint) instead. Likewise, avoid mounting /var/run/docker.sock into containers unless you fully trust the image.

3. Drop privileges per container

Add hardening options to services that support them:

    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    read_only: true
    tmpfs:
      - /tmp

Not every image works with read_only or a fully dropped capability set. Add back only what is required (for example cap_add: [NET_BIND_SERVICE]) and test. Run as a non-root user where the image allows it (user: "1000:1000"). Avoid privileged: true.

4. Limit resources and log growth

By default the json-file log driver has no size limit, so one noisy container can fill the disk. Set limits in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

The new defaults apply only to containers created after the restart, so recreate existing ones with docker compose up -d --force-recreate. In Compose you can also set mem_limit and cpus per service.

5. Keep images and the host patched

Use official or well-maintained images, pin versions, rebuild regularly, and scan images with a tool such as Trivy. Enable unattended-upgrades on the host. Keep secrets in .env files with tight permissions or Docker secrets, not baked into images.

Common mistakes and fixes

Symptom Cause Fix
permission denied ... docker.sock User not in docker group, or session not refreshed newgrp docker or log out and in
docker-compose: command not found Using the legacy v1 command Use docker compose
port is already allocated Another service on that host port ss -tlnp \| grep 8080, change the mapping
apt cannot find the repo Derivative distro with unknown codename Set the codename manually to the matching Ubuntu/Debian release
Disk fills up Old images, stopped containers, logs docker system df, then docker system prune (review first)
Container cannot reach another Wrong hostname or different network Use the service name and check docker network ls

Where to run it

Containers are light, but production stacks still need predictable CPU, disk I/O, and a network you can protect. A VDS from Nixcon Group LLC runs on VMware vSphere and gives you full root, so Docker installs exactly as described above. If your containers serve public traffic such as web apps or game servers, pair them with DDoS protection and firewall so that attack traffic is filtered before it reaches your host's iptables and Docker's chains.

Frequently asked questions

Do I need to install Docker Compose separately on Ubuntu or Debian?

No. When you install the docker-compose-plugin package from Docker's official repository, Compose is available as the docker compose command. The old standalone docker-compose v1 binary is no longer needed.

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

Docker adds its own iptables rules for published ports, and these are evaluated before UFW's rules for forwarded container traffic. Bind the port to 127.0.0.1, publish only intended ports, or use the DOCKER-USER chain for restrictions.

Is it safe to add my user to the docker group?

It is convenient but equivalent to giving that user root access on the host. Only add trusted administrators. On shared systems, use sudo for Docker commands instead.

Should I use docker.io from the Ubuntu or Debian repositories?

It works for simple testing, but the package is often older and conflicts with Docker's official packages. For production, the official docker-ce repository provides current releases along with the Compose and Buildx plugins.

Run your containers on a root-access VDS from Nixcon Group LLC.Learn more