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 -Treportspasswordauthentication noandpermitrootlogin no - You logged in with a new session before closing the old one
- You know which accounts and deploy keys are in the
dockergroup - 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 -rshows 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 shows why and how to fix it.
Set the rules in your cloud firewall, then scan from a machine outside your provider:
nmap -Pn -p- your-server-ip
nmap -6 -Pn -p- your-ipv6-address
Then list what Docker publishes on the server:
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.
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:
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:
sudo sshd -t && sudo systemctl restart ssh
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|x11forwarding|allowusers) '
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:
cat /etc/shadow
docker run --rm -v /etc:/h alpine wc -l /h/shadow
docker run --rm -v /:/h alpine id
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.
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 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 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? 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 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.
My self-hosting course 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.

