Docker turns a server into a tidy workshop: each application runs in its own container, with its own libraries, and it stops fighting with everything else on the machine. That is why so many self-hosting guides start with the same step, install Docker, and then never mention it again. This guide covers that step properly, on Ubuntu Server, using Docker's own apt repository instead of a random install script.
Installing from Docker's repository matters for one practical reason: updates. The docker.io package in Ubuntu's own archives works, but it trails the official releases and can behave differently from the version your tutorials assume. The repository method takes a few more commands up front, and then sudo apt update && sudo apt upgrade keeps Docker current like any other package.
Before you start
You need an Ubuntu Server machine you can administer, either 22.04 LTS or 24.04 LTS, 64-bit, with sudo access and an internet connection. The steps below were checked against Docker's official Ubuntu installation documentation and the repository line was generated and verified on Ubuntu 24.04 (amd64, codename noble), including a successful download of Docker's GPG key. A fresh server is ideal; if the machine already runs containers, back up anything you care about first, because step one removes conflicting packages.
One warning before touching anything: Docker and a host firewall do not play as nicely as you might expect. When you publish a container port, Docker inserts its own iptables rules, and that port can become reachable even if your firewall rules say otherwise. Keep that in mind if you already run a firewall on this server; the companion guide to setting up UFW on Ubuntu Server is worth reading alongside this one, and never publish a container port you did not mean to expose.
Step 1: Remove conflicting packages
Ubuntu may already carry Docker-related packages under different names. Docker's documentation lists docker.io, docker-compose, docker-compose-v2, docker-doc, docker-buildx, podman-docker, containerd, and runc as packages to remove first, because they can clash with the official ones. On a fresh server, most of these will simply not be installed, and apt will say so. That is fine.
for pkg in docker.io docker-doc docker-compose docker-compose-v2 docker-buildx podman-docker containerd runc; do
sudo apt-get remove $pkg
doneNote what this does not remove: images, containers, volumes, and networks stored in /var/lib/docker/ survive a package removal. If you want a truly clean start, you will need to delete that directory yourself after uninstalling, and only after you are sure nothing in it matters.
Step 2: Add Docker's apt repository
First, install the two tools the setup needs, and create the keyring directory with the right permissions:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyringsNext, download Docker's official GPG key into that directory and make it readable:
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.ascNow add the repository itself. This one-liner detects your architecture and Ubuntu codename automatically, so do not hard-code noble or jammy unless you have a reason to:
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 updateIf that final apt-get update fails with a GPG or repository error, fix the repository line before installing anything; the usual causes are a typo in the keyring path, a missing signed-by entry, or a network filter blocking download.docker.com.
Skip the popular shortcut of piping get.docker.com straight into a shell on a real server. Docker documents its convenience script as a testing and development tool, not a production installer: it makes choices for you and is not designed to upgrade an existing installation cleanly. The repository method above is only a few commands longer and leaves you with a normal, upgradable package.
Step 3: Install Docker Engine and Compose
With the repository in place, the install is a single command:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginThat pulls in the Engine (docker-ce), the client (docker-ce-cli), the runtime (containerd.io), the build extension, and Compose. Modern Compose is a plugin, invoked as docker compose with a space, not the old standalone docker-compose binary with a hyphen; if a tutorial tells you to install docker-compose separately, it is describing the old world.
Check that the service is running:
sudo systemctl status docker --no-pagerOn Ubuntu, Docker starts automatically after installation. If it ever does not, sudo systemctl start docker brings it up, and sudo systemctl enable docker makes that automatic at boot.
Step 4: Verify with hello-world
Docker ships a tiny test image for exactly this moment. Run it with sudo for now; the next step removes that requirement:
sudo docker run hello-worldA successful run prints a short message confirming the daemon pulled the image, ran it, and that it printed its output and exited. If you see "Cannot connect to the Docker daemon", the service is not running; go back to the systemctl status check. If the pull itself fails, check DNS and outbound HTTPS before suspecting Docker.
Confirm the version you actually got, so you know what you are working with when you read other guides:
docker --version
docker compose versionStep 5: Run Docker without sudo (optional, but do it)
By default, only root and members of the docker group can talk to the Docker daemon, which is why every command above used sudo. Adding your own user to the group removes that friction:
sudo usermod -aG docker $USER
newgrp dockerThe catch is the one everyone trips over: group membership only takes effect at your next login. newgrp docker starts a shell with the new group right away, which is handy for testing, but it does not change your existing SSH session permanently. If docker run hello-world still asks for sudo after this step, log out and log back in, then try again without sudo. That logout step is the fix, not a bug to work around.
There is a security trade-off here that deserves a plain sentence: anyone in the docker group can effectively gain root on the host, because a container can mount the host filesystem. So add only users you would trust with sudo anyway, and never add a shared or service account casually. Docker's own post-installation documentation makes the same point.
Verify the no-sudo setup works:
docker run hello-worldIf that prints the hello message without sudo, the installation is done.
Step 6: Run your first real container with Compose
A hello-world container proves Docker works; a Compose file proves your workflow works. Create a test directory and a minimal Compose file that serves a page on port 8080:
mkdir -p ~/docker-test && cd ~/docker-testservices:
web:
image: nginx:alpine
ports:
- "8080:80"Save that as compose.yml, the current conventional name, then start it:
docker compose up -d
curl http://localhost:8080You should see nginx's welcome page HTML. When you are done experimenting, stop and remove the test container:
docker compose down
cd ~ && rm -rf ~/docker-testIf port 8080 is already taken on your server, change the left-hand number, the host port, to something free, for example "8081:80". The right-hand number is the port inside the container and stays as the application expects.
Common pitfalls, and how to avoid them
You installed docker.io and now versions disagree. Remove it with the step-one loop, then install from Docker's repository. Mixing the two sources is the most common cause of "but the tutorial says this flag exists" confusion.
Docker works with sudo but not without it. You skipped the logout. Log out of SSH completely and log back in, or use newgrp docker for a quick test shell.
A container port is reachable despite the firewall. That is the Docker and iptables interaction mentioned at the start. Do not rely on UFW alone to hide published ports; either do not publish the port, bind it to localhost ("127.0.0.1:8080:80"), or put the service behind a reverse proxy with authentication.
Disk space quietly disappears. Images, stopped containers, and build cache live in /var/lib/docker/ and grow without ceremony. docker system df shows where the space went, and docker system prune clears stopped containers and unused images, but prune deletes things, so run it deliberately, never as a reflex.
A guide mounts the Docker socket into a container. Treat that as a red flag: a container with access to /var/run/docker.sock can start new privileged containers, which is effectively host root. Only do it for tools you understand and trust.
Verify your installation
Before you move on to deploying real services, run through this checklist. Every item should pass:
docker --versionanddocker compose versionboth print versions.docker run hello-worldworks without sudo after a fresh login.docker compose up -dstarts a test service, andcurl http://localhost:8080returns a page.docker compose downstops it, anddocker psshows no leftover test container.sudo systemctl is-enabled dockerreportsenabled, so Docker survives a reboot.
If all five pass, your server is ready for the next step, whether that is a personal project, a self-hosted app, or a larger stack. Readers planning a home server often pair Docker with a fixed network address, so a static IP setup with Netplan makes the services on it reachable at a predictable address, and anyone weighing Docker against its daemonless rival should read the guide to migrating from Docker to Podman without losing containers before committing a whole fleet to one runtime.


