Infrastructure engineering for East Africa's operators, platforms and regulators See the work →
Production Linux Server Setup Checklist for Kenya
Linux 8 min read

Production Linux Server Setup Checklist for Kenya

Our production Linux server checklist, from distro choice and SSH hardening to monitoring and backups, with the commands we run before a server goes live.

JM
Josphat Mutai
Cloud Infrastructure, Kubernetes, DevOps, Linux
Updated 31 October 2026
linuxserver hardeningUbuntuRocky Linux

Every production Linux server should pass the same checklist before it takes real traffic, whether it is a single web app or one node of a microservice platform. This is the one we work from.

It is also how CloudSpinx deploys, hardens and manages production Linux servers for businesses in Kenya and across East Africa, so every command below is one we would run on your box. If you are inheriting a server from another provider, or spinning up something new on AWS, Azure or a local host, start here.

Picking a Distro

We default to Ubuntu 24.04 LTS for most clients. Fight us.

LTS releases get five years of standard security maintenance without paying anyone, and every tutorial, Stack Overflow answer and vendor install guide assumes Ubuntu first. That second point matters more than people admit at 2am.

Rocky Linux 9 gets picked when the client needs RHEL binary compatibility, usually because they run commercial software that only certifies against RHEL. Debian gets picked when we want maximum stability and the client does not care about the newest packages.

That Rocky choice now has a hardware condition attached. Rocky Linux 10 and RHEL 10 will not start on a processor older than Intel's Haswell, or on a Proxmox VM left at its default CPU type, which is why we check the CPU before the Ubuntu or Rocky question.

We stopped recommending CentOS when Red Hat ended CentOS Linux 8 early, a decision that burned a lot of people in Kenya who had built their whole stack on it. If you are still running CentOS 7, its end of life was June 2024. Migrate now. Our Linux infrastructure team does that work.

Arch, Fedora or openSUSE for production? No.

The First 15 Minutes on a Fresh Server

Here is what we run on first login. It assumes Ubuntu 24.04; the ideas apply everywhere.

Update everything first

sudo apt update && sudo apt upgrade -y
sudo reboot

Boring. Essential. Inherited servers months behind on security patches are common enough that this is never optional.

Timezone and locale

sudo timedatectl set-timezone Africa/Nairobi
sudo localectl set-locale LANG=en_US.UTF-8

Africa/Nairobi is UTC+3 with no daylight saving. If cron jobs fire at odd hours, check this first: a server left on UTC runs a job you scheduled for 2am at 5am Nairobi time.

Create your admin user and get off root

sudo adduser deployer
sudo usermod -aG sudo deployer

Now set up SSH key access for this user. From your local machine, copy your public key across:

# Run this from your laptop, not the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub deployer@your-server-ip

If ssh-copy-id is not available, do it by hand on the server:

sudo mkdir -p /home/deployer/.ssh
sudo cp ~/.ssh/authorized_keys /home/deployer/.ssh/
sudo chown -R deployer:deployer /home/deployer/.ssh
sudo chmod 700 /home/deployer/.ssh
sudo chmod 600 /home/deployer/.ssh/authorized_keys

Before moving on, test that you can SSH in as the new user with your key. Open a second terminal:

ssh deployer@your-server-ip

If that works, carry on. If it does not, fix it now while your current session is still open.

Never run applications as root. When the web app, the database and the cron jobs all run as root, one compromised PHP script hands an attacker the whole machine, and separating them afterwards is far more work than doing it on day one.

SSH Hardening

Only do this after key login works for the new user. Disable passwords before the key is in place and you lock yourself out.

Edit /etc/ssh/sshd_config:

Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deployer

This catches people out: Ubuntu reads every file in /etc/ssh/sshd_config.d/ before the main file, and for most settings the first value wins. Some cloud images ship a drop-in there that sets PasswordAuthentication yes, which silently beats your line above. Check the directory, then ask sshd what it will actually enforce:

ls /etc/ssh/sshd_config.d/
sudo sshd -T | grep -Ei '^(port|passwordauthentication|permitrootlogin) '

Then apply it. On Ubuntu 24.04, SSH is socket-activated and the listening port is generated from the config by systemd, so restarting the daemon alone leaves you on port 22:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart ssh.service

On Rocky Linux with SELinux enforcing, sshd cannot bind a non-standard port until SELinux is told about it. Run sudo semanage port -a -t ssh_port_t -p tcp 2222 before sudo systemctl restart sshd.

Changing the port is not real security, and we know it. It moves you off the port every scanner hits first, which keeps the logs readable. The real protection is key-only login with root disabled.

Do not close your current session yet. Open a new terminal and test the new port:

ssh -p 2222 deployer@your-server-ip

If that works, you are done. If not, your original session is still open and you can fix the config.

Firewall Configuration

Ubuntu with ufw

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw enable
sudo ufw status verbose

Rocky Linux with firewalld

sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload

Open only what you need. MySQL on 3306, PostgreSQL on 5432 and Redis on 6379 open to the whole internet is how data breaches happen, and we still find them in infrastructure audits. If the app server needs the database, use private networking or an SSH tunnel. Our cybersecurity team covers this in depth.

Fail2ban

sudo apt install fail2ban -y

Create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = ufw

[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Fail2ban watches the logs and bans addresses that keep failing. Any server with a public IP gets brute-forced from the moment it boots, and fail2ban handles that quietly. See who it has caught:

sudo fail2ban-client status sshd

Automatic Security Updates

Nobody remembers to patch. Configure unattended-upgrades on every Ubuntu server:

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

Edit /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Mail "alerts@yourcompany.co.ke";

We set the reboot for 3am EAT. Pick whatever hour your own traffic graphs say is quietest. For Rocky Linux, the equivalent is dnf-automatic:

sudo dnf install dnf-automatic -y
sudo systemctl enable dnf-automatic-install.timer

Monitoring Setup

A server without monitoring is a server you hear about from your customers. That phone call at 7am on a Monday: "The system is down." Nobody wants that call.

We run Prometheus with node_exporter on every server we manage through our managed IT services.

Install node_exporter

wget https://github.com/prometheus/node_exporter/releases/download/v1.12.1/node_exporter-1.12.1.linux-amd64.tar.gz
tar xzf node_exporter-1.12.1.linux-amd64.tar.gz
sudo mv node_exporter-1.12.1.linux-amd64/node_exporter /usr/local/bin/
sudo useradd --no-create-home --shell /bin/false node_exporter

Check the releases page for a newer version before you copy this.

Create /etc/systemd/system/node_exporter.service:

[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
User=node_exporter
ExecStart=/usr/local/bin/node_exporter
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable node_exporter
sudo systemctl start node_exporter

If Prometheus feels like overkill for one server, start smaller: a shell script that checks disk, memory and CPU and sends an alert to a Slack webhook or an SMS through the Africa's Talking API. Graduate to Prometheus and Grafana when the second and third servers arrive. Whatever you use, watch at least disk usage (alert at 80%, act at 90%), memory and swap activity, load average over 5 and 15 minutes, SSH logins both failed and successful, TLS certificate expiry, and whether nginx, postgres and your app are actually running.

Log Management

Left alone, journald will eventually eat the disk. Cap it in /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=500M
MaxRetentionSec=30day
Compress=yes
sudo systemctl restart systemd-journald

For application logs, make sure logrotate covers them. Packages set this up themselves; custom apps usually do not, and a log that nobody rotates will fill a small disk on a schedule you do not choose.

For centralized logging across several servers, we ship logs to a central Grafana Loki instance. Our cloud infrastructure team can run it as part of a managed monitoring stack.

Backup Strategy

No backups, no sympathy. That is the rule on our team.

At minimum:

# Database backup - PostgreSQL example
pg_dump -U postgres mydb | gzip > /backups/mydb-$(date +%Y%m%d).sql.gz

# Sync to remote storage
aws s3 sync /backups/ s3://your-backup-bucket/$(hostname)/

Put this in a cron job:

# /etc/cron.d/backups
0 2 * * * deployer /opt/scripts/backup.sh >> /var/log/backups.log 2>&1

Here is the part most people skip: test the restore. A backup you have never restored from is a hope, not a backup. Schedule a restore test, and on our managed hosting plans we run them for you.

For businesses holding personal data, section 41 of Kenya's Data Protection Act lists the ability to restore access to personal data in a timely manner after a physical or technical incident among the safeguards a data controller must consider. A restore you have actually tested is how you show you have it.

Performance Baseline

Before handing the server to developers, record what normal looks like:

free -h
nproc
cat /proc/cpuinfo | grep "model name" | head -1

# Disk performance
sudo hdparm -Tt /dev/sda

# Current resource usage
vmstat 1 5
iostat -x 1 5

Save the output. When someone says "the server is slow" three months from now, you will want something to compare against.

Kernel Tuning for Web Servers

For a busy web application, these sysctl settings are where we start:

# /etc/sysctl.d/99-performance.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
vm.swappiness = 10
fs.file-max = 2097152
sudo sysctl -p /etc/sysctl.d/99-performance.conf

vm.swappiness defaults to 60. On a server with enough RAM, a lower value keeps the kernel from pushing memory out to disk early, and on lower-tier cloud instances disk I/O is far slower than RAM. The kernel docs are clear that the right value is workload-dependent, so measure before and after rather than trusting anyone's number, ours included.

The 15-Minute Audit for Inherited Servers

When a client moves to CloudSpinx from another provider, we audit before we touch anything. The quick version:

# Who can log in?
cat /etc/passwd | grep -v nologin | grep -v /bin/false

# What is listening on the network?
sudo ss -tlnp

# When was the last update?
stat /var/cache/apt/pkgcache.bin

# Any unexpected cron jobs?
for user in $(cut -f1 -d: /etc/passwd); do crontab -l -u $user 2>/dev/null; done

# Check for rootkits
sudo apt install rkhunter -y && sudo rkhunter --check --sk

# Disk usage by directory
du -sh /* 2>/dev/null | sort -rh | head -20

# Failed login attempts (the unit is ssh on Ubuntu)
sudo journalctl -u ssh | grep "Failed" | tail -20

# Automatic updates configured?
systemctl status unattended-upgrades 2>/dev/null || echo "No automatic updates"

The usual findings are dull and dangerous: user accounts nobody removed, services listening on the internet that should not be, and a backup job that stopped months ago without anyone noticing.

Interior of a modern data center with organized server racks

Everything above is the floor, not the ceiling. PCI DSS for payment processing (M-Pesa integrations, card payments), multi-server architectures behind a load balancer, Kubernetes, automated CI/CD pipelines and CIS Benchmark Level 2 hardening each go further, and that is where our Linux infrastructure team earns its keep. Reach us at hello@cloudspinx.co.ke or WhatsApp 0713 403 044.

Download: Production Linux Server Setup Checklist for Kenya

Enter your work email. We'll send the PDF download link to your inbox.

WhatsApp