How to Install Docker on Ubuntu 24.04 Safely
In this article12 sections
Docker is convenient, and on a server it has one habit you must plan for. When you publish a container port, Docker writes its own firewall rules, and those rules are applied before UFW’s rules. A port you publish with -p 8080:80 can be reachable from the whole internet even when UFW is enabled and shows it blocks that port. Docker’s own documentation says Docker and UFW are incompatible in this respect. This guide installs Docker from Docker’s official repository on Ubuntu 24.04, then shows how to publish services without exposing them, how to check that the protection works, and where the more involved firewall fixes fit.
Before you start
- An Ubuntu 24.04 server with a sudo user.
- No other Docker packages installed. The distribution’s
docker.iopackage and Docker’s official packages conflict, so remove the old one first if it is present.
for p in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt remove -y "$p" 2>/dev/null; done
Removing packages you have not installed is harmless: the command reports them as not installed and carries on.
Step 1: Add Docker’s official repository
Install the tools that let apt fetch the repository key, then store the key in the keyring directory that Ubuntu 24.04 uses:
sudo apt update
sudo apt 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
Add the repository, written in the deb822 format that Ubuntu 24.04 uses. The Signed-By line tells apt which key to trust for this source only:
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
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
Expected: the update lists the Docker repository without any signature warning. A warning about the key means the key file did not download, so check the first step again before you continue.
Step 2: Install Docker Engine and Compose
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The Compose plugin is part of this package set, so the command is docker compose, with a space, not the older standalone docker-compose.
Check that the service is running, then run the test container:
sudo systemctl status docker --no-pager | head -5
sudo docker run --rm hello-world
Expected: the service shows as active (running), and the container prints a message that starts with “Hello from Docker!”. If the test container fails to pull, check the server’s outbound connection to Docker Hub.
Step 3: Decide who may run Docker
Docker commands need root by default, which is why most guides add your user to the docker group. Be careful with that step. Any member of the docker group can start a container that mounts the host’s file system, and that gives them root on the server. Treat the group as root access, and add only the accounts you would trust with a root password:
sudo usermod -aG docker $USER
Log out and back in before the change takes effect. If you prefer to keep Docker behind sudo, skip this step and run every Docker command with sudo.
Step 4: Check which ports are exposed
Before you publish anything, learn to read the port list. This command shows every running container and the addresses each port is bound to:
docker ps --format "table {{.Names}}\t{{.Ports}}"
A port written as 0.0.0.0:8080->80/tcp is open on every address of the server, including the public one. A port written as 127.0.0.1:8080->80/tcp is open only to the server itself. Only the second form is safe for a service that should be reachable through a reverse proxy.
Step 5: Publish services on localhost only
The simplest and most reliable protection is not to publish a container on a public address at all. Bind the port to the loopback address, and let Nginx accept the public traffic and pass it on:
docker run -d --name app -p 127.0.0.1:8080:80 nginx:alpine
The same rule in Compose:
services:
app:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
Then put Nginx in front, on the public interface, with the proxy block from how to set up free SSL on Nginx with Certbot. The container is reachable only through Nginx, which is where you control TLS, rate limits and request sizes.
Note. A container that does not publish a port at all, and talks only to other containers on the same Docker network, is safer still. Use that when a service needs only other containers.
Step 6: Test from outside the server
Do not trust a firewall status screen. Test the server from another network, because the question is whether the internet can reach the port. Use nmap from a computer that is not on the server’s network:
nmap -Pn -p 8080 203.0.113.10
Replace 203.0.113.10 with your server’s public address. Expected for a localhost-only port: filtered or closed. If the result is open, the port is reachable from the internet, whatever UFW shows. Check every port you intend to keep private, including the database and any admin tool.
Step 7: Know the options for a service that must be public
Sometimes a container has to listen publicly, for example a game server or a service that talks to clients directly. There are three approaches, and they differ in how well they are documented.
- Keep the port private and proxy it. This is the option in Step 5, and it covers almost every web service.
- Use the DOCKER-USER chain. Docker creates this chain for administrators’ own rules, and rules placed in it are checked before Docker’s own forwarding rules. Community guides wire it into UFW’s rule files. The approach works, but the rule order is subtle, so use it only after you have tested it with the external scan in Step 6, and document each rule.
- Use a tool built for this problem. Several projects publish UFW integrations for Docker. Review the code and the maintenance history before you run one on a production server.
Avoid turning off Docker’s firewall management in /etc/docker/daemon.json, for example by setting "iptables": false. Docker’s documentation says this is not appropriate for most users and is likely to break container networking. You would then have to write every NAT and forwarding rule yourself.
Step 8: Keep Docker updated
Docker and containerd receive security fixes through the same repository. Update them with the rest of your packages, and restart the affected containers after an upgrade:
sudo apt update
sudo apt install --only-upgrade -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl restart docker
Restarting the Docker service restarts the containers that have a restart policy. Check that they come back up with docker ps.
Step 9: Keep the disk clean
Docker keeps old images, stopped containers and build cache, and they can fill a small server’s disk. Check the usage first:
docker system df
Remove what you do not need with the prune commands. They delete data, so read what they will remove before you confirm:
docker image prune -a
docker builder prune
The first command removes images that no container uses. Do not run it on a server where you rely on an image you have not pulled again.
Common mistakes
- Publishing a database port publicly. A database bound to
0.0.0.0is one of the most common causes of a data breach on Docker hosts. Bind it to127.0.0.1or do not publish it. - Trusting UFW for published ports. UFW does not see the traffic Docker routes. Test with an external scan.
- Adding every user to the
dockergroup. Membership is effectively root access. - Using
latesttags in production. A rebuild can change the software under you. Pin a version tag. - Running the default image without reading it. Check the image’s documentation for the ports and the volumes it expects.
Docker publishes container ports through its own firewall rules, and those rules can bypass UFW. Before you expose a container to the internet, read how to set up a UFW firewall on Ubuntu and decide which ports should be public, then bind the rest to 127.0.0.1.
Frequently asked questions
Should I use Docker or install the software directly?
Docker is useful when several applications need different library versions, or when you want to rebuild a service the same way on another server. For one simple service, a native install can be easier to maintain. Choose by how much you will update and rebuild.
Why does my container work locally but not from the internet?
Usually because the port is bound to 127.0.0.1, which is the protected setting, and the request does not go through the proxy. Check the Nginx proxy block and the port list from Step 4.
Does the Compose file need a version line?
No. The Compose plugin ignores the old version key, so you can leave it out.
Can I run Docker on a small VPS?
Yes. Start with one or two containers, watch memory with docker stats, and add a swap file if memory runs short. The swap steps are in how to add swap space on an Ubuntu VPS.
Install Docker from its official repository, bind every service to localhost, put Nginx in front of the services that must be public, and prove the result with an external scan. A port that UFW shows as blocked can still be open, so the test from outside the server is the only check you can trust.