Skip to content
Launch offerNew accounts: we double your first deposit of $100 or more, up to $250 bonus.Claim my bonus
LiteKeep
Sign inDeploy a server

Step-by-step tutorial, 2026

Install Docker on Ubuntu. Then stop it opening holes in your firewall.

Installing Docker takes five minutes. The part most guides skip is what happens next: your firewall stops applying to your containers.

  • 14 min read
  • 7 chapters
  • Updated

ufw status

Status: active
22/tcp     ALLOW
8080/tcp   DENY
  1. Requestto port 8080
  2. Docker’s nat ruleredirects it to the container
  3. ufw (INPUT)never sees it
  4. Containeranswers 200 OK

What we measured: ufw denied 8080, and the container still answered from outside.

The short answer

Install from Docker’s own apt repository: add the key and repo, then sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin, and test with sudo docker run hello-world. Full commands below.

On a VPS, do one more thing: publish ports as 127.0.0.1:8080:80, not 8080:80. We tested it: a normally published port answered from outside even with ufw set to deny it.

You install Docker, start a container with -p 8080:80, and your firewall says port 8080 is closed. It isn’t.

We tested it on a fresh Ubuntu 24.04 with Docker 29.9.2. With ufw denying every incoming port except SSH, an ordinary web server was blocked. The Docker container answered from outside. Then we added ufw deny 8080. Still answered.

This isn’t a bug. It’s how Docker works: published ports bypass your firewall rules. Most install guides never mention it, and some even open the Docker API to the network.

So this tutorial installs Docker the current official way, then fixes the three things that bite on a public server. Twenty minutes, and your containers are only as public as you decide.

In this guide you’ll learn

  • The official install for Ubuntu 22.04, 24.04 and 26.04, without deprecated apt-key
  • Our firewall test: seven setups, which ones ufw actually blocked
  • A free checker: paste docker ps, see which ports the internet can reach
  • Three fixes, from one-line to server-wide
  • Why the docker group is root, and what to do about it
  • Logs that fill your disk, and the one setting that stops it

Chapter 1

Install Docker the official way

Docker supports Ubuntu 22.04, 24.04, 26.04 and 26.10, 64-bit. Run these as your sudo user.

  1. Remove conflicting packages

    sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)

    On a fresh server this removes nothing. That’s fine.

  2. Add Docker’s repository

    sudo apt update
    sudo apt install 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
    sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
    Types: deb
    URIs: https://download.docker.com/linux/ubuntu
    Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
    Components: stable
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/docker.asc
    EOF
    sudo apt update
  3. Install

    sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  4. Check it works

    sudo docker run hello-world
    docker compose version

    On Ubuntu, Docker starts on boot by default.

Three ways to install, and which to use on a server
MethodVersionUse it?
Docker’s apt repo (above)Current releaseYes: the documented method
Ubuntu’s docker.io packageUbuntu’s own build, often olderWorks, but Compose and docs lag behind
get.docker.com scriptCurrent releaseNo: meant for testing, not servers

The convenience script is meant for testing and development, not a server you rely on. And if a guide leads with apt-key add, skip it: that’s deprecated, and the keyring file above replaces it.

Chapter 2

What we found: ufw doesn’t see Docker ports

Ubuntu 24.04, Docker 29.9.2, ufw set to deny all incoming except SSH. We sent each request from a separate machine.

Could the outside world reach it? (our test, October 2026)
SetupReached from outside?
Plain web server on port 9090, no DockerNo: ufw blocked it
docker run -p 8080:80Yes, 200 OK
Same, plus ufw deny 8080/tcpYes, 200 OK
docker run -p 127.0.0.1:8080:80No (works from the server itself)
-p 8080:80 plus a DOCKER-USER drop ruleNo
DOCKER-USER rule allowing one IPYes, from that IP only
Compose "8080:80" with the daemon.json belowNo

Why: ufw filters traffic in the INPUT chain. Docker redirects traffic for a published port earlier, in the nat table, and sends it on to the container. Those packets are routed before the firewall rules can be applied.

And by default, a published port listens on every address, 0.0.0.0 and [::]. Publishing is open to the world unless you say otherwise.

Chapter 3

Which of your ports are public?

Run docker ps on your server and paste the output. The checker lists every published port, says who can reach it, and writes the localhost-only version.

No server handy?

Paste your docker ps output on the left.

A public 80 or 443 is usually fine: that’s your reverse proxy or website. A public database or admin panel is the one to fix today.

Moving to a fresh server anyway? Deploy a Lite 6, set the daemon.json from the next chapter before your first container, and nothing is ever public by accident.

Chapter 4

Three ways to fix it

All three worked in our test. Start with the first.

1. Publish to localhost, put a reverse proxy in front

# docker run
docker run -d -p 127.0.0.1:8080:80 nginx

# compose.yaml
services:
  app:
    ports:
      - "127.0.0.1:8080:80"

Only the server itself can reach port 8080. A reverse proxy (Caddy, nginx, Traefik) on 80 and 443 is then the only thing facing the internet, and it adds HTTPS.

2. Make localhost the default for the whole server

Put this in /etc/docker/daemon.json, then sudo systemctl restart docker:

{
  "ip": "127.0.0.1",
  "default-network-opts": {
    "bridge": { "com.docker.network.bridge.host_binding_ipv4": "127.0.0.1" }
  },
  "log-driver": "local"
}

"ip" covers the default network; the host_binding_ipv4 option covers user-defined networks, which is what Compose creates. In our test a Compose service with plain "8080:80" then listened on 127.0.0.1 only. Ports you really want public, like your proxy’s 443, now need 0.0.0.0:443:443 written out.

3. Allow-list in the DOCKER-USER chain

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.10 -j DROP

Docker processes DOCKER-USER before its own rules, so this drops traffic to published ports from everyone except 203.0.113.10. Swap in your interface (ip route shows it) and your IP.

Mythufw deny on the port will close it.

Fact

We tried. The port still answered from outside.

MythSet "iptables": false and ufw will work again.

Fact

That isn’t meant for most setups, and it will likely break container networking.

Chapter 5

The docker group is root

Every guide tells you to run sudo usermod -aG docker $USER so you can drop sudo. Few say what it means.

Here’s the thing: the docker group grants root-level privileges. Anyone in it can start a container that mounts / and change any file on the server.

  1. Only youHow to check: Add only the accounts you’d trust with sudo. Never a web app’s user.
  2. Never expose the APIHow to check: Ports 2375 and 2376 are the Docker API. Open to the internet, that’s root for anyone who finds it. Our checker flags them.
  3. Want less?How to check: Rootless mode runs the daemon as a normal user. It has limits, so read them first.

Chapter 6

Stop logs filling your disk

By default, container logs go to JSON files that can grow large and exhaust disk space. On a 40 GB VPS, a chatty container can fill it.

The local logging driver rotates and compresses logs by itself, keeping 100 MB per container. It’s the "log-driver": "local" line in the daemon.json above. Restart Docker, then recreate containers: the setting only applies to containers created after the change.

sudo systemctl restart docker
docker compose up -d --force-recreate

Chapter 7

Keeping Docker and images updated

Docker itself and your containers update separately.

  • Docker Engine comes from apt. Ubuntu’s automatic updates skip Docker’s repo unless you add it; our unattended upgrades tutorial shows the line, and why restarting Docker automatically needs care.
  • Your images never update through apt. Pull and recreate:
docker compose pull
docker compose up -d
docker image prune

The last line removes old images that nothing uses. It asks before deleting.

Now it’s your move

Docker on a VPS is two jobs: install it properly, then decide what the internet can see. Run the checker after every new service.

Planning what to run? Our VPS use-case planner sizes a server for a stack of apps. Then lock down logins with the SSH hardening tutorial.

Ready to ship it? Deploy a Lite 6, paste the install block, and your compose stack is live soon after the server is ready (usually within 15 minutes). Billed by the hour, never more than the monthly price.