Docker lets you package an application with everything it needs to run and start it as an isolated container, and Docker Compose lets you define several related containers - a web server and a cache, for example - in a single file. This guide installs Docker Engine and Compose on a BinaryLane Ubuntu 24.04 VPS, covers the container and Compose basics you'll use day to day, and flags a firewall interaction that will silently break container networking if you've also hardened the server with a custom nftables ruleset.


Prerequisites:

  • A BinaryLane VPS running Ubuntu 24.04 LTS
  • Root or sudo access
  • Outbound HTTPS access - if this server has BinaryLane's port blocking enabled, disable it first, as Docker needs to reach Docker Hub and Docker's package repository


Rescue Console note: none of the steps below touch SSH or networking in a way that can lock you out on their own. If you've also applied a custom firewall (see the nftables section near the end) and something goes wrong, the BinaryLane Rescue Console gives you local access independent of the network.




TABLE OF CONTENTS




Remove conflicting packages

Ubuntu ships older Docker-related packages under different names. Remove them first so they don't conflict with the official Docker packages:

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

It's fine if some or all of these report "not installed" - that just means there's nothing to clean up.

Add Docker's official repository

sudo apt-get update -y
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 "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -y

Install Docker Engine and Compose

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

This installs Docker Engine itself plus the docker compose CLI plugin (no hyphen - docker compose, not the older standalone docker-compose binary).

Verify the installation

docker --version
docker compose version
sudo systemctl is-active docker
sudo docker run --rm hello-world

You should see Docker's "Hello from Docker!" message, confirming the daemon can pull images and run containers end to end.

Container lifecycle basics

A few commands you'll use constantly:

docker run -d --name test-nginx -p 8080:80 nginx:alpine   # start a container in the background
docker ps                                                  # list running containers
docker logs test-nginx                                     # view its output
docker exec -it test-nginx sh                              # open a shell inside it
docker stop test-nginx                                     # stop it
docker start test-nginx                                    # start it again (same container)
docker rm -f test-nginx                                    # remove it entirely

stop/start keep the container, including anything written to its writable layer. rm deletes it permanently. Data you actually care about should go in a volume (see below), not the container's writable layer - the writable layer disappears with rm.

Docker Compose basics

Compose lets you define multiple containers, their networking, and their storage in one file. Example - a web server that talks to a cache:

# docker-compose.yml
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    depends_on:
      - cache
  cache:
    image: redis:alpine
    volumes:
      - redis-data:/data
volumes:
  redis-data:
docker compose up -d      # create and start everything defined above
docker compose ps          # see what's running
docker compose down        # stop and remove containers + the network (volumes are kept)
docker compose down -v     # same, but also delete volumes

Networking

Compose puts every service in a file on one private network and gives each one a DNS name matching its service key. The web service above can reach the cache at the hostname cache - no IP addresses, no manual linking:

docker compose exec web getent hosts cache
# 172.18.0.2   cache  cache

This works for any two services in the same Compose file, automatically.

Volumes and persistence

Named volumes (like redis-data above) live outside the container and survive container removal - the difference between data that's actually durable and data that disappears the moment you run docker compose down:

docker compose exec cache redis-cli set mykey "hello"
docker compose down          # containers + network removed; volume is NOT
docker compose up -d         # fresh containers, same volume re-attached
docker compose exec cache redis-cli get mykey
# "hello" - the data survived because it lived in the named volume, not the container

Only down -v (or docker volume rm) actually deletes volume data.

Docker and a custom nftables firewall

If this server has also been hardened with a custom nftables ruleset (see the nftables Firewall Configuration guide), there are two real gotchas here - both confirmed on a live test deploy, not theoretical.


Important: A locked-down forward chain silently breaks published container ports for external traffic. Docker publishes container ports (-p 8080:80) using DNAT rules it manages itself, in its own nftables tables (visible via nft list tables as ip nat / ip filter, added via the iptables-nft compatibility layer). Traffic arriving from outside the server for a published port has to pass through the kernel's forward hook to reach the container's bridge network - if your own nftables config adds a forward base chain with policy drop and no exceptions, it will drop that traffic even though Docker's own rules are completely intact and correctly configured. Testing with curl localhost:8080 from the server itself will misleadingly look fine, because locally-generated traffic takes the output path, not forward - you have to test from an external client to see the problem.


The fix - explicitly allow forwarding for Docker's bridge interfaces in your own forward chain:

chain forward {
    type filter hook forward priority 0; policy drop;
    ct state established,related accept
    iifname "docker0" accept
    oifname "docker0" accept
    iifname "br-*" accept
    oifname "br-*" accept
}

Important: flush ruleset in your nftables config will wipe Docker's rules too, not just your own. Docker's iptables-nft rules live in the same overall nftables ruleset as any rules you write by hand. If your /etc/nftables.conf starts with flush ruleset (a very common pattern in generic hardening guides), reapplying it - on every boot, or any time you run nft -f /etc/nftables.conf by hand - deletes Docker's nat/filter tables along with yours. Every container's published ports stop working until you run systemctl restart docker, which makes Docker re-add its rules.


The fix - scope the flush to your own table, not the whole ruleset:

#!/usr/sbin/nft -f
destroy table inet filter   # only removes *this* table if it exists - leaves Docker's tables alone

table inet filter {
    chain input { ... }
    chain forward { ... }
}

If you've already hit this - containers unreachable, nft list tables shows no ip nat/ip filter - the fix is just systemctl restart docker.

Cleanup

To remove everything Docker-related from a server:

sudo docker system prune -a --volumes
sudo apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras



Verified end-to-end on a fresh Ubuntu 24.04 BinaryLane VPS: package install, hello-world, a two-service Compose stack with cross-container DNS resolution, volume persistence across down/up, and both nftables interactions above - confirmed broken without the fix, confirmed working with it, tested from both localhost and an external client.