> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ubuntu Server Initial Setup Checklist (2026 Edition)
- URL: https://blog.sudowl.com/ubuntu-server-initial-setup-checklist/
- Published: 2026-03-28T10:00:00.000Z
- Updated: 2026-10-11T10:16:27.000Z
- Description: The first 20 minutes on a fresh Ubuntu 24.04 server: updates, sudo user, SSH keys, sshd drop-ins, UFW, fail2ban, auto-updates, swap, journald. In order.
- Author: Lukas
- Tags: Linux, Ubuntu

Every fresh box I get — VPS, Proxmox clone, whatever — gets the same treatment before anything else touches it. Not because it's fancy, but because the order matters: do SSH before the firewall and you lock yourself out, do the firewall before the user and you're stuck on root.

**TL;DR:** update, create a sudo user with your key, kill password + root login via an sshd drop-in, UFW with SSH allowed *first*, fail2ban, unattended-upgrades, a swap file, cap the journal, reboot. Copy-paste version:

```bash
apt update && apt full-upgrade -y
adduser lukas && usermod -aG sudo lukas
rsync --archive --chown=lukas:lukas ~/.ssh /home/lukas
printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' > /etc/ssh/sshd_config.d/00-hardening.conf
sshd -t && systemctl reload ssh
ufw allow OpenSSH && ufw --force enable
apt install -y fail2ban unattended-upgrades
reboot

```

Below is each step with the stuff that actually goes wrong. This is written against Ubuntu 24.04 LTS. 26.04 LTS is due in April — I'll check what changes when it lands, but nothing below should break.

## 1\. Update everything first

```bash
apt update && apt full-upgrade -y
apt autoremove --purge -y

```

`full-upgrade`, not `upgrade` — plain `upgrade` holds back packages that need new dependencies, which is often exactly the kernel. If `/var/run/reboot-required` exists afterwards, reboot now, not "later". Later never comes.

## 2\. Hostname and time

```bash
hostnamectl set-hostname web01
timedatectl set-timezone UTC
timedatectl status

```

I keep servers on UTC. Logs from five boxes in five timezones are miserable to correlate. `timedatectl status` should say `System clock synchronized: yes` and `NTP service: active` — that's `systemd-timesyncd`, which is fine for anything that isn't a time server.

On cloud images, cloud-init might reset the hostname on reboot. If it does, set `preserve_hostname: true` in `/etc/cloud/cloud.cfg`.

## 3\. A real user with sudo and your SSH key

```bash
adduser lukas
usermod -aG sudo lukas
rsync --archive --chown=lukas:lukas ~/.ssh /home/lukas

```

The `rsync` line copies root's `authorized_keys` (the one your provider injected) to the new user with the right ownership. If root has no key, do it from your workstation instead:

```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub lukas@your-server

```

Now open a **second terminal** and log in as the new user. Run `sudo -v`. Only if that works do you move on. Full background on users, groups and keys: [Linux user management](/linux-user-management-adduser-sudo-ssh-keys/).

## 4\. SSH: drop-in, not sshd\_config

```bash
cat > /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
EOF

sshd -t && systemctl reload ssh
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication'

```

`sshd -t` checks syntax. `sshd -T` prints the *effective* config — that's the one you trust, not the file you just wrote. On Ubuntu the service is `ssh`, not `sshd`.

## Gotcha: the cloud-init drop-in wins

OpenSSH uses the **first** value it reads for most options. `sshd_config` includes `sshd_config.d/*.conf` at the very top, in alphabetical order. Lots of cloud images ship `50-cloud-init.conf` containing `PasswordAuthentication yes`. If you name your file `99-hardening.conf`, the cloud-init one is read first and your setting is silently ignored.

That's why mine is `00-hardening.conf`. And that's why you check with `sshd -T` instead of trusting the file.

## Gotcha: changing the SSH port on 24.04

Ubuntu 22.10+ uses socket activation for SSH. `Port 2222` in a drop-in does nothing until you do:

```bash
systemctl daemon-reload
systemctl restart ssh.socket
ss -tlnp | grep ssh

```

Personally I don't move the port anymore. It cuts log noise, it doesn't stop anyone who's actually looking. Keys-only plus fail2ban does the real work. The longer list is in my [SSH hardening checklist](/ssh-hardening-ubuntu-checklist/).

## 5\. UFW — SSH first, then enable

```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable
ufw status verbose

```

If you `ufw enable` before allowing SSH over a remote session, you're done for and you get to find out how good your provider's web console is. Open ports for actual services only when they exist. More rules and patterns: [UFW firewall rules](/ufw-firewall-rules-ubuntu/).

## Gotcha: Docker ignores UFW

If this box will run Docker, know this up front: `docker run -p 8080:80` writes its own iptables rules that bypass UFW entirely. `ufw status` will show nothing open and the port is reachable from the internet anyway. Bind to localhost (`-p 127.0.0.1:8080:80`) and put a reverse proxy in front. Covered in the [Docker install post](/docker-install-ubuntu/).

## 6\. fail2ban

```bash
apt install -y fail2ban
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

[sshd]
enabled = true
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd

```

`backend = systemd` matters — on 24.04 sshd logs go to the journal, and depending on your rsyslog setup the default file-based backend can end up watching a log nothing reads. Add your home IP to `ignoreip` if it's static. Nginx jails etc: [fail2ban for SSH and nginx](/fail2ban-setup-ssh-nginx/).

## 7\. Automatic security updates

Ubuntu Server usually has `unattended-upgrades` installed and on. Verify instead of assuming:

```bash
apt install -y unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades

```

You want both values at `"1"`. Whether to allow automatic reboots is a real decision, not a default — I wrote it up separately: [automatic security updates on Ubuntu and Debian](/ubuntu-automatic-updates-unattended-upgrades/).

## 8\. Swap file

Small VPS images often come with zero swap. One runaway process and the OOM killer picks something important.

```bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swap.conf
sysctl --system
free -h

```

Swap is not extra RAM. It's a buffer so the box degrades instead of killing things. Low swappiness keeps the kernel from swapping eagerly. On ZFS root, don't put a swapfile on ZFS — use a separate partition or zram.

## 9\. Cap the journal

journald defaults to up to 10% of the filesystem, capped at 4G. On a 20 GB disk that's a lot of space for logs nobody reads.

```bash
mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSystemMaxUse=500M\n' > /etc/systemd/journald.conf.d/size.conf
systemctl restart systemd-journald
journalctl --disk-usage

```

Plain log files in `/var/log` are handled separately by logrotate, which is already set up for everything installed via apt.

## 10\. Look at what's listening

```bash
ss -tulpn

```

On a fresh 24.04 you should see sshd, `systemd-resolved` on 127.0.0.53, maybe chrony or timesyncd. Anything bound to `0.0.0.0` that you didn't install, find out what it is now.

## 11\. The optional stuff I do anyway

```bash
apt install -y htop ncdu curl git rsync tmux

```

- **Snap:** I leave it alone on servers. Ripping it out saves a bit of RAM and causes weird breakage the next time something depends on it. Not worth it.
- **Unused services:** `systemctl list-unit-files --state=enabled` and disable what you don't need. On cloud images that's often nothing.
- **Backups:** before there's data on it. Not after the first incident.

## 12\. Reboot and re-verify

```bash
reboot

```

After it comes back, log in as your user (not root), and run:

```bash
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication'
sudo ufw status
sudo fail2ban-client status sshd
swapon --show
uname -r

```

If root login is `no`, password auth is `no`, UFW is active, fail2ban has a jail and swap shows up — the box is ready for its actual job. The point of the reboot is to prove all of this survives one. Finding out during an outage is worse.

## Gotcha: locking yourself out

Three ways I see people lock themselves out, in order of frequency:

- Password auth disabled before the key login was tested. Always test in a second session.
- `ufw enable` before `ufw allow OpenSSH`.
- `AllowUsers` set with a typo in the username.

Keep the original root session open until you've logged in fresh as the new user, with sudo working. Close it only after that.

---

### Related posts

- [KeyHelp: The Free Plesk Alternative I Moved To (and Its API Traps)](https://blog.diengdoh.com/keyhelp-free-plesk-alternative/?ref=blog.sudowl.com)
- [Running PHP 7.4, 8.3 and 8.4 Side by Side on One Server](https://blog.diengdoh.com/multiple-php-versions-one-server/?ref=blog.sudowl.com)
- [systemd Timers Instead of cron](https://blog.diengdoh.com/systemd-timers-instead-of-cron/?ref=blog.sudowl.com)