# Ubuntu Server Hardening Checklist for Docker Hosts

> A hardening checklist for Ubuntu 24.04 and 26.04 servers running Docker: firewall, SSH keys, patching, reboots, and the common advice you can skip.

Published: 2026-11-05. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/ubuntu-server-hardening

To harden an Ubuntu server running Docker, close ports at a provider firewall, require SSH keys, keep administration private, patch automatically, reboot for kernel updates, and treat the `docker` group as root.
Those steps matter more than moving the SSH port or tuning kernel settings.
Each one below has a check that proves it.

I tested these on Ubuntu 24.04 and 26.04 with Docker 29.9, using systemd-booted containers under WSL2 and a second container on the same network.
I didn't test a rented VPS, a live provider firewall, or Ubuntu's own kernels.
Replace `your-server-ip` and `deploy` with your own values.

## The Checklist

Copy this into your repository and fill it in.
A Markdown version of this page is available at `/blog/ubuntu-server-hardening.md`.

**Network**

- [ ] A provider firewall allows only `80`, `443`, and your admin path
- [ ] A scan from outside shows only those ports, over IPv4 and IPv6
- [ ] No container publishes a port it doesn't need
- [ ] The deployment panel can't be reached publicly

**SSH and accounts**

- [ ] `sshd -T` reports `passwordauthentication no` and `permitrootlogin no`
- [ ] You logged in with a new session before closing the old one
- [ ] You know which accounts and deploy keys are in the `docker` group
- [ ] You have logged in through the provider's recovery console

**Patching**

- [ ] Unattended upgrades is on, and a dry run looks right
- [ ] Reboots happen on a schedule, and `uname -r` shows the newest kernel
- [ ] Docker is patched on a schedule you chose
- [ ] Container images are rebuilt from a fresh base regularly

**Recovery**

- [ ] Every container has a restart policy and survives a reboot
- [ ] An alert from outside the server reaches you
- [ ] A stored backup restores somewhere disposable

## How Do You Close the Network?

Start with a provider firewall, because UFW alone doesn't cover Docker.
With UFW denying everything but SSH and an explicit `ufw deny 8080`, a published container still returned `200` from outside the server, and [**Docker bypasses UFW**](https://milanjovanovic.tech/blog/docker-bypasses-ufw) shows why and how to fix it.

Set the rules in [**your cloud firewall**](https://milanjovanovic.tech/blog/cloud-firewall-vs-ufw), then scan from a machine outside your provider:

```bash
nmap -Pn -p- your-server-ip
nmap -6 -Pn -p- your-ipv6-address
```

Then list what Docker publishes on the server:

```bash
docker ps --format '{{.Names}}\t{{.Ports}}'
```

Anything on `0.0.0.0` or `[::]` is public unless the firewall stops it.
On a Swarm node, also run `docker service ls`, because `docker ps` doesn't show ingress ports as published.
Bind internal services to loopback with `127.0.0.1:8080:80`, and keep apps behind your reverse proxy unpublished.

If you run Dokploy, its installer publishes the panel on `3000` and doesn't set up a firewall.
Block that port upstream and [**reach it through a tunnel**](https://milanjovanovic.tech/blog/deploy-dotnet-app-to-vps-with-dokploy).

## How Do You Lock Down SSH?

Put your settings in a file that sorts first, because `sshd` keeps the first value it reads.
I reproduced the trap with a file like the one cloud images can ship:

| Files in sshd_config.d | sshd -T reports |
| --- | --- |
| 50-cloud-init.conf says yes, 60-hardening.conf says no | passwordauthentication yes |
| 00-hardening.conf says no, 50-cloud-init.conf says yes | passwordauthentication no |

Create `/etc/ssh/sshd_config.d/00-hardening.conf`:

```text
PasswordAuthentication no
PermitRootLogin no
X11Forwarding no
AllowUsers deploy
```

Make sure `deploy` can log in with a key first, and that it's a real account, or `AllowUsers` will lock you out.
Then validate the file before you apply it.
`sshd -t` exits `0` on a good file and `255` on a typo, and it names the bad line:

```bash
sudo sshd -t && sudo systemctl restart ssh
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|x11forwarding|allowusers) '
```

```text
permitrootlogin no
passwordauthentication no
x11forwarding no
allowusers deploy
```

Open a second terminal, log in as `deploy`, and run `sudo -v` before you close the first one.
With root login off, this account is your only way to administer the server.
Also log in through your provider's recovery console once, so that you know it works before you need it.

## Who Effectively Has Root on Your Server?

Anyone in the `docker` group.
My `deploy` user couldn't read the password hashes directly, but it could through a container:

```bash
cat /etc/shadow
docker run --rm -v /etc:/h alpine wc -l /h/shadow
docker run --rm -v /:/h alpine id
```

```text
cat: /etc/shadow: Permission denied
23 /h/shadow
uid=0(root) gid=0(root) groups=0(root),...
```

That makes a CI deploy key with Docker access a root key.
You probably still need one, so limit where it can be used, keep it out of places that build untrusted pull requests, and rotate it like any other root credential.

Mounting the Docker socket into a container gives that container the same power, so that goes for panels and reverse proxies too.
Know which of your containers have it.

```bash
docker ps -q | xargs docker inspect --format \
  '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock
```

## How Do You Keep It Patched?

Five separate things need patching, and each has a different owner:

| What | Who patches it by default | What you still need to do |
| --- | --- | --- |
| Ubuntu packages | Unattended upgrades | Check it is on and read a dry run |
| Linux kernel | Unattended upgrades installs it, nothing activates it | Reboot, or turn on automatic reboots |
| Docker Engine | Nobody, it is a third-party repository | Upgrade on a schedule, since it restarts containers |
| Packages inside images | Nobody | Rebuild from a fresh base and redeploy |
| Your deployment panel | Nobody | Follow its security advisories |

[**Unattended upgrades on Ubuntu**](https://milanjovanovic.tech/blog/unattended-upgrades-ubuntu) covers the reboot settings and what a Docker upgrade did to running containers.
When a new advisory lands, [**how to check a CVE on Ubuntu**](https://milanjovanovic.tech/blog/check-ubuntu-server-cve) takes you from the ID to an answer, including the critical kernel flaw that was still unpatched in the 24.04 repositories as I wrote this.

## What Common Hardening Advice Can You Skip?

Most guides list everything at equal weight.
These are the ones I'd drop or put off on a small Docker host:

| Advice | Verdict | Why |
| --- | --- | --- |
| Rely on UFW | Not enough | It does not filter published Docker ports |
| Change the SSH port | Skip | It only reduces log noise, and it needs a socket reload and a Fail2ban change |
| Install Fail2ban for SSH | Optional | With keys only, it ignores wrong-key attempts for usernames sshd allows |
| ufw limit on SSH | Careful | The sixth login within 30 seconds was refused, even with a valid key |
| Block ping | Skip | Hetzner advises against it, since it hinders troubleshooting and adds little |
| Set iptables to false in Docker | Avoid | Docker says it is likely to break container networking |

[**Do you need Fail2ban?**](https://milanjovanovic.tech/blog/fail2ban-ubuntu-docker) has the test results behind those two rows, plus a jail that works for containers.

A formal benchmark such as CIS is worth doing if a customer or auditor requires it.
I'd finish this list first, since it's shorter and every item has a check.
I haven't tested those benchmarks here.

## What's Different on Ubuntu 26.04?

Very little changes the checklist.
These are the differences I saw between the two releases, using the packages each one installs:

|  | Ubuntu 24.04 | Ubuntu 26.04 |
| --- | --- | --- |
| OpenSSH | 9.6 | 10.2, with per-source penalties on by default |
| Fail2ban in the repository | 1.0.2 | 1.1.0 |
| SSH socket activation | Yes | Yes |
| Docker bypasses UFW | Yes | Yes |
| Automatic reboots | Off | Off |
| Docker in unattended upgrades | No | No |

## Does Hardening Replace Backups?

No, and it's why the checklist ends with recovery.
Everything above makes a compromise less likely.
A [**backup you have restored**](https://milanjovanovic.tech/blog/test-postgres-backup-restore) is what lets you recover from one.
Rebuilding on a clean server is usually the safe response to a root compromise, so make sure you could.

## Summary

Most of the protection comes from a short list: a provider firewall, key-only SSH, private administration, automatic patches, and reboots that really happen.
The checks matter as much as the settings, because `ufw status` and a config file both showed me a safe-looking server while the port stayed open or the old value stayed active.

Run the outside scan and `sshd -T` after every change, then put the rest on your [**VPS deployment checklist**](https://milanjovanovic.tech/blog/vps-deployment-checklist).
My [**self-hosting course**](https://milanjovanovic.tech/pragmatic-self-hosting) walks through this setup on a real server with a supplied app.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### How do I harden an Ubuntu server?

Close ports at a provider firewall, require SSH keys and turn off password login, keep administration private, patch the system automatically, reboot for kernel updates, and treat docker group membership as root access. Then scan the server from outside to confirm what is really exposed.

### Is UFW enough to secure an Ubuntu server running Docker?

No. Docker publishes container ports around UFW, so a published port stayed open in testing even with an explicit ufw deny rule. Use a provider firewall as your main rule set, and scan from outside to confirm the result.

### Should I change the SSH port on my server?

It reduces log noise but does not add real protection. On Ubuntu 24.04 and 26.04, it also needs a socket unit reload, and the default Fail2ban jail keeps watching port 22. Restricting SSH at a firewall or using a VPN is more effective.

### Why is PasswordAuthentication still yes after I set it to no?

sshd uses the first value it reads, and it reads files in sshd_config.d in name order. If a file such as 50-cloud-init.conf sets it to yes, a later file cannot override it. Put your settings in a file that sorts first, such as 00-hardening.conf, and check the result with sshd -T.

### Is the docker group the same as root?

Effectively, yes. In testing, a user who could not read /etc/shadow could read it through a container with a bind mount, and the container ran as root. Treat any account or deploy key in the docker group as having root access.

### What is different about hardening Ubuntu 26.04 compared with 24.04?

Ubuntu 26.04 ships OpenSSH 10.2, which drops repeat offenders by default through per-source penalties, while 24.04 has OpenSSH 9.6 without them. The Docker and UFW behavior, the default unattended-upgrades origins, and the need for a reboot after kernel updates were the same on both in testing.
