Production Linux server setup for 2026.

OS choice

  • Ubuntu 24.04 LTS: most familiar, biggest community.
  • Debian 12: stable, smaller, “boring” in best sense.
  • Rocky / Alma 9: RHEL-compatible.
  • Fedora: bleeding edge.
  • Alpine: minimal, good for containers.

For most: Ubuntu LTS.

Initial setup

# 1. Update
apt update && apt upgrade -y
apt install -y vim git curl wget htop tmux fail2ban ufw \
              unattended-upgrades zsh

# 2. Hostname
hostnamectl set-hostname myserver.example.com

# 3. Time
timedatectl set-timezone UTC
timedatectl status

# 4. Create admin user
useradd -m -s /bin/bash -G sudo alice
echo 'alice ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/alice
mkdir -p /home/alice/.ssh
cp ~/.ssh/authorized_keys /home/alice/.ssh/
chown -R alice:alice /home/alice/.ssh
chmod 700 /home/alice/.ssh

# 5. SSH lockdown (see hardening cheatsheet)
# 6. Firewall
ufw allow OpenSSH
ufw allow http
ufw allow https
ufw enable

# 7. fail2ban
systemctl enable --now fail2ban

# 8. Disable root login
passwd -l root

Disk layout (typical)

/boot          1G
/              30G (root)
/var           50G
/home          10G+
swap           4G (or zram)
/data          remaining (LVM)

sysctl

# /etc/sysctl.d/99-server.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 2097152
vm.swappiness = 10
kernel.dmesg_restrict = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1

ulimit

# /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 32768

Unattended upgrades

apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

Auto-installs security patches.

Time sync

timedatectl set-ntp on
timedatectl status

Monitoring agent

node-exporter (Prometheus)

curl -L https://github.com/prometheus/node_exporter/releases/latest/download/node_exporter-1.8.0.linux-amd64.tar.gz | tar xz
mv node_exporter*/node_exporter /usr/local/bin/

cat > /etc/systemd/system/node_exporter.service <<EOF
[Unit]
Description=Node Exporter
[Service]
User=nobody
ExecStart=/usr/local/bin/node_exporter
Restart=always
[Install]
WantedBy=multi-user.target
EOF

systemctl enable --now node_exporter

Vector / Promtail (logs)

Ship to Loki.

Backup setup

See backups cheatsheet. Recommended: restic to S3-compatible backend.

0 2 * * * /opt/backup.sh

Docker (optional but common)

curl -fsSL https://get.docker.com | sh
usermod -aG docker alice
systemctl enable --now docker

/etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" },
  "live-restore": true
}

TLS automation

apt install certbot
certbot certonly --webroot -w /var/www -d example.com --email [email protected] --agree-tos

Auto-renewal via certbot.timer (installed by default).

Web stack (Caddy alternative)

Caddy = simpler than nginx + auto-TLS:

apt install caddy

cat > /etc/caddy/Caddyfile <<EOF
example.com {
    reverse_proxy localhost:8000
}
EOF

systemctl reload caddy

SSH key audit

# Show authorized keys for each user
for u in $(getent passwd | cut -d: -f1); do
    f=/home/$u/.ssh/authorized_keys
    [ -r "$f" ] && echo "=== $u ===" && cat "$f"
done

.bashrc / .zshrc essentials

alias ll='ls -lah'
alias ..='cd ..'
alias ...='cd ../..'
alias g='git'
alias k='kubectl'
alias d='docker'
alias dcu='docker compose up'
alias dcd='docker compose down'

export EDITOR=vim
export HISTSIZE=10000
export HISTFILESIZE=10000
shopt -s histappend

# Better history
HISTCONTROL=ignoredups:erasedups
PROMPT_COMMAND='history -a'

Useful packages

apt install -y \
    htop btop ncdu mtr-tiny \
    rsync restic \
    jq yq \
    ripgrep fd-find bat fzf \
    vim neovim tmux \
    iotop sysstat dstat \
    net-tools dnsutils whois nmap tcpdump \
    apt-transport-https ca-certificates \
    gpg curl wget

Daily ops checklist

  • Disk usage < 80% (df -h).
  • No failed services (systemctl --failed).
  • No errors in journal (journalctl -p err --since today).
  • Cert expiry > 14 days.
  • Recent backup verified.
  • Memory not swap-thrashing.
  • CPU steal time low (cloud VM).

Provisioning

For repeatable: Ansible playbook with all of the above. Or Terraform + cloud-init.

Cloud-init example:

#cloud-config
users:
  - name: alice
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...

package_update: true
package_upgrade: true
packages:
  - fail2ban
  - ufw
  - htop

runcmd:
  - ufw allow OpenSSH
  - ufw allow http
  - ufw allow https
  - ufw enable
  - systemctl enable --now fail2ban

Disaster recovery test

Quarterly:

  1. Spin up new VM.
  2. Run provisioning.
  3. Restore latest backup.
  4. Switch DNS / LB.
  5. Time the whole thing.

Read this next

That’s 20 Linux cheatsheets. Next category: AI/LLM.

If you want my full server provisioning playbook, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .