Ubuntu Server Hardening Checklist for Docker Hosts

devopsdockersecurityself-hosting
Ubuntu Server Hardening Checklist for Docker Hosts

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 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.dsshd -T reports
50-cloud-init.conf says yes, 60-hardening.conf says nopasswordauthentication yes
00-hardening.conf says no, 50-cloud-init.conf says yespasswordauthentication 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:

WhatWho patches it by defaultWhat you still need to do
Ubuntu packagesUnattended upgradesCheck it is on and read a dry run
Linux kernelUnattended upgrades installs it, nothing activates itReboot, or turn on automatic reboots
Docker EngineNobody, it is a third-party repositoryUpgrade on a schedule, since it restarts containers
Packages inside imagesNobodyRebuild from a fresh base and redeploy
Your deployment panelNobodyFollow 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:

AdviceVerdictWhy
Rely on UFWNot enoughIt does not filter published Docker ports
Change the SSH portSkipIt only reduces log noise, and it needs a socket reload and a Fail2ban change
Install Fail2ban for SSHOptionalWith keys only, it ignores wrong-key attempts for usernames sshd allows
ufw limit on SSHCarefulThe sixth login within 30 seconds was refused, even with a valid key
Block pingSkipHetzner advises against it, since it hinders troubleshooting and adds little
Set iptables to false in DockerAvoidDocker 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.04Ubuntu 26.04
OpenSSH9.610.2, with per-source penalties on by default
Fail2ban in the repository1.0.21.1.0
SSH socket activationYesYes
Docker bypasses UFWYesYes
Automatic rebootsOffOff
Docker in unattended upgradesNoNo

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.

Browse all Self-Hosting guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.