Skip to content
DevOps best practices 6 min read

Linux Server Best Practices

A Linux server (a computer running the Linux operating system that hosts your apps, websites, or databases) is happy to do exactly what you tell it — including things you did not mean. The difference between a server that runs quietly for years and one that gets hacked or falls over at 3 a.m. is a small set of disciplined habits. This page is a curated checklist of those habits for Ubuntu (Ubuntu 22.04 / 24.04 LTS, the long-term-support releases most servers run). Each item explains the “why” in one line, tells you when to apply it, and gives the exact commands to run on a fresh server.

Use least privilege

Least privilege means: every user and process gets only the access it needs, and nothing more. Why: if an account is compromised, the damage is limited to what that account could do. Never run your apps or your daily work as root (the all-powerful superuser).

Create a normal user, give it sudo (the command that runs one task as an admin), and use that:

sudo adduser deploy
sudo usermod -aG sudo deploy

Output:

Adding user `deploy' ...
Adding new group `deploy' (1001) ...
Adding new user `deploy' (1001) with group `deploy' ...

When to use this: always. Log in as deploy, run admin tasks with sudo, and disable direct root login. When not to: there is no good reason to do daily work as root on a server.

Use key-only SSH

SSH (Secure Shell — how you log into a remote server from your terminal) defaults to allowing password login. Passwords get guessed; bots try thousands per minute. Why switch to SSH keys: a key is a long cryptographic secret that is effectively impossible to brute-force.

Generate a key on your laptop, copy it to the server, then turn passwords off:

# On your laptop
ssh-keygen -t ed25519 -C "deploy@laptop"
ssh-copy-id deploy@your-server-ip

Then edit the SSH config on the server:

sudo nano /etc/ssh/sshd_config
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes

Reload SSH so the change takes effect:

sudo systemctl reload ssh

Security tip: keep your current SSH session open while you test a new login in a second terminal. If you lock yourself out before confirming key login works, you may need console access from your hosting provider to get back in.

When to use this: every internet-facing server. When not to: if you genuinely cannot use keys, at minimum install fail2ban to block repeated failed logins.

Deny by default with ufw

A firewall controls which network ports (numbered doors into your server) are open. The safest stance is deny-by-default: block everything, then allow only the few ports you actually use. Why: a closed port cannot be attacked. Ubuntu ships ufw (Uncomplicated Firewall) for exactly this.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH        # so you do not lock yourself out
sudo ufw allow 80,443/tcp     # web traffic, only if this is a web server
sudo ufw enable
sudo ufw status verbose

Output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing)

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW IN    Anywhere
80,443/tcp                 ALLOW IN    Anywhere

When to use this: on every server, before it touches production traffic. When not to: do not open a port “just in case” — open it only when a service needs it.

Keep the system patched

Unpatched software is the single most common way servers get compromised. Why: published vulnerabilities have ready-made exploits, and bots scan the whole internet for them. Update regularly, and let security updates apply themselves.

sudo apt update && sudo apt upgrade -y

Enable automatic security updates so you are protected even when you forget:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Update typeCommandRisk
Security patchesunattended-upgrades (automatic)Low — apply continuously
Regular packagessudo apt upgradeMedium — review during a maintenance window
Distribution upgradesudo do-release-upgradeHigh — test in staging first

When to use this: automatic security updates on every server. When not to: never auto-apply a full release upgrade (22.04 to 24.04) unattended — that one you plan and test.

Monitor disk and logs

A full disk is one of the most common causes of a server crashing — databases refuse writes, logs stop, services die. Why monitor logs: they are your only record of what went wrong, and the first place an intruder leaves footprints. Check both before they bite you.

df -h /                    # disk space, human-readable
journalctl -p err -b       # all error-level logs since last boot
du -sh /var/log/*          # what is eating space under /var/log

Output:

Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        49G   18G   29G  39% /

Stop logs from filling the disk by enabling rotation (Ubuntu does this by default via logrotate, but verify it):

sudo systemctl status logrotate.timer

When to use this: wire disk and error alerts into a monitoring tool (a simple cron-based check, or Prometheus/Netdata) on any server you care about. When not to: do not rely on noticing a problem by hand — automate the alert.

Automate with scripts and IaC

If you set a server up by typing commands from memory, you cannot reproduce it, and you will forget a step. Why automate: scripts and IaC (Infrastructure as Code — defining servers in version-controlled files instead of clicking around) make setup repeatable, reviewable, and fast to rebuild after a disaster.

Start every setup script defensively so a failed command stops the run:

#!/usr/bin/env bash
set -euo pipefail
# -e exit on error, -u error on undefined vars, -o pipefail catch pipe failures

apt update && apt upgrade -y
ufw default deny incoming && ufw allow OpenSSH && ufw --force enable

For anything beyond a single box, move to a tool like Ansible (agentless automation that configures servers over SSH) or Terraform (which provisions the servers themselves).

When to use this: the moment you have more than one server, or a server you cannot afford to lose. When not to: a five-minute throwaway test box does not need a full Ansible role — a small bash script is fine.

Document and make it reproducible

The goal: anyone on the team can rebuild this server from scratch by following a written record. Why: the person who set it up will eventually leave, forget, or be on holiday during the outage. Keep your scripts, your ufw rules, and a short README in a Git repository, and store config in version control rather than only on the box.

cd /opt/infra
git init
git add setup.sh ufw-rules.sh README.md
git commit -m "Server setup as code"

A reproducible server is one you can confidently destroy and rebuild — which is exactly what you want when recovering from an incident.

Best Practices

  • Do daily work and run apps as a normal sudo user, never as root, so a breach stays contained.
  • Switch SSH to key-only, disable PasswordAuthentication and PermitRootLogin, and test the new login before closing your session.
  • Run ufw deny-by-default and open only the exact ports a service needs.
  • Enable unattended-upgrades for security patches and plan larger upgrades in a maintenance window.
  • Monitor disk space and error logs with automated alerts, and confirm logrotate is active so logs cannot fill the disk.
  • Capture server setup in scripts or IaC, start scripts with set -euo pipefail, and keep everything in Git so any server is reproducible.
Last updated June 15, 2026
Was this helpful?