← Back to DevBytes

Ubuntu Server Setup: From Installation to Production

Ubuntu Server Setup: From Installation to Production

Ubuntu Server is one of the most popular Linux distributions for deploying production workloads. Whether you are spinning up a cloud instance, provisioning a bare-metal machine, or configuring a virtual machine locally, a disciplined setup process is the foundation of a stable, secure, and maintainable server. This tutorial walks through the entire lifecycle — from a fresh install to a hardened, production-ready host — with practical commands you can run immediately.

What Is Ubuntu Server?

Ubuntu Server is Canonical's server-optimized variant of Ubuntu. Unlike the Desktop edition, it ships without a graphical user interface, pre-installed office suites, or media players. Instead, it focuses on a minimal footprint, long-term support (LTS) releases, and tools that matter for infrastructure: SSH, cloud-init, snapd, AppArmor, and a curated set of server packages.

LTS releases are supported for five years and are the recommended choice for production. At the time of writing, Ubuntu 24.04 LTS (Noble Numbat) is the current LTS, with 22.04 LTS (Jammy Jellyfish) still widely deployed.

Why It Matters

A poorly configured server is a liability. Default installations expose unnecessary services, use weak authentication, and lack monitoring or backup strategies. By following a consistent setup routine, you gain several concrete benefits:

1. Installation

You can install Ubuntu Server from an ISO image on bare metal or a VM, or launch a prebuilt cloud image on AWS, Azure, GCP, Hetzner, DigitalOcean, or Linode. The cloud route is faster and is what most developers use today.

Option A: Local or Bare-Metal Install

Download the ISO from the official Ubuntu website, flash it to a USB drive, and boot from it. The installer (Subiquity) guides you through network, storage, and user configuration. Key decisions:

To flash the ISO on another Linux machine:

# Identify your USB device
lsblk

# Flash the ISO (replace /dev/sdX with your device)
sudo dd if=ubuntu-24.04-live-server-amd64.iso of=/dev/sdX bs=4M status=progress
sync

Option B: Cloud Image

On most providers, launching an Ubuntu instance is a one-click operation. After launch, connect using the provider-assigned public IP and your SSH key. For example, on AWS:

ssh -i ~/.ssh/my-key.pem ubuntu@<public-ip>

Cloud images ship with cloud-init, which runs on first boot to apply user data, SSH keys, and package updates. You can pass a cloud-init configuration to automate the entire setup described below.

2. Initial System Update

After first login, update the package index and upgrade installed packages. This ensures you have the latest security patches.

sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y

Check the Ubuntu version to confirm your release:

lsb_release -a

3. Creating a Non-Root User

Running services as root is dangerous. Create a dedicated user with sudo privileges for administrative tasks, and separate service accounts for applications.

# Create a new user with a home directory
sudo adduser deploy

# Add the user to the sudo group
sudo usermod -aG sudo deploy

# Switch to the new user to test sudo access
su - deploy
sudo whoami

4. Configuring SSH for Security

SSH is your primary entry point, so it deserves careful configuration. The goals are: use key-based authentication, disable root login, and restrict who can connect.

Generate and Install an SSH Key

On your local machine, generate an Ed25519 key (preferred over RSA for its strength and compact size):

ssh-keygen -t ed25519 -C "admin@mycompany.com"

Copy the public key to the server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@<server-ip>

Harden the SSH Daemon

Edit the SSH configuration file on the server:

sudo nano /etc/ssh/sshd_config

Apply the following settings:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers deploy
Port 22
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2

Restart SSH and verify the configuration is valid before restarting:

sudo sshd -t
sudo systemctl restart ssh

Keep your current session open and open a second terminal to test the new configuration. This prevents you from locking yourself out.

5. Configuring the Firewall

Ubuntu ships with ufw (Uncomplicated Firewall), a front-end for iptables. Enable it and allow only the ports you need.

# Allow SSH (do this BEFORE enabling the firewall)
sudo ufw allow OpenSSH

# Allow HTTP and HTTPS for web services
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable

# Verify the rules
sudo ufw status verbose

If you run a custom application on port 8080, allow it explicitly:

sudo ufw allow 8080/tcp

6. Setting the Hostname and Timezone

A descriptive hostname helps you identify servers in logs and monitoring dashboards. Set it consistently with your naming convention.

sudo hostnamectl set-hostname web-prod-01
sudo timedatectl set-timezone UTC

Update /etc/hosts to map the hostname to the loopback address:

sudo nano /etc/hosts
127.0.1.1 web-prod-01

Using UTC as the server timezone is a best practice for multi-region deployments because it eliminates ambiguity in log timestamps.

7. Automatic Security Updates

Ubuntu provides the unattended-upgrades package to install security updates automatically. This is critical for keeping the system protected without manual intervention.

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

Edit the configuration to control what gets installed:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Ensure the security origins are uncommented:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};

You can also enable automatic reboots if required, though this should be coordinated with your maintenance windows:

Unattended-Upgrade::Automatic-Reboot "false";

8. Installing Fail2Ban

Fail2Ban monitors log files and bans IPs that show malicious signs, such as too many failed SSH login attempts. It is a simple but effective layer of defense against brute-force attacks.

sudo apt install -y fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Create a local configuration to override defaults:

sudo nano /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 22
maxretry = 3
bantime = 3600
findtime = 600

Restart Fail2Ban to apply changes:

sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

9. Swap Configuration

Even on servers with ample RAM, a small swap file prevents out-of-memory crashes during traffic spikes. Create a 2 GB swap file:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Make it persistent across reboots by adding it to /etc/fstab:

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Tune swap behavior. A low swappiness value keeps the system from swapping unless necessary:

echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

10. Installing a Web Server and Reverse Proxy

Nginx is a common choice for serving static content and acting as a reverse proxy. Install it and enable the service:

sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx

Allow HTTP and HTTPS through the firewall (if not already done):

sudo ufw allow 'Nginx Full'

Create a server block for your domain:

sudo nano /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

11. Securing Traffic with Let's Encrypt

Install Certbot to obtain a free TLS certificate from Let's Encrypt:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot modifies the Nginx configuration automatically and sets up a systemd timer for renewal. Test the renewal process:

sudo certbot renew --dry-run

12. Deploying an Application with systemd

Use systemd to manage your application as a service. This gives you automatic restarts, logging, and dependency management. Create a service file for a Node.js application as an example:

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/myapp
ExecStart=/usr/bin/node /home/deploy/myapp/server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=8080

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp

View logs with journalctl:

sudo journalctl -u myapp -f

13. Setting Up a Database

PostgreSQL is a robust, feature-rich database. Install it and perform basic hardening:

sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable postgresql

Switch to the postgres user and create a database and application user:

sudo -u postgres psql
CREATE DATABASE myapp_prod;
CREATE USER myapp_user WITH PASSWORD 'use-a-strong-password-here';
ALTER ROLE myapp_user SET client_encoding TO 'utf8';
ALTER ROLE myapp_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE myapp_user SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myapp_prod TO myapp_user;
\q

Edit pg_hba.conf to require password authentication and restrict connections to localhost:

sudo nano /etc/postgresql/16/main/pg_hba.conf
local   all             postgres                                peer
local   all             all                                     md5
host    all             all             127.0.0.1/32            md5
host    all             all             ::1/128                 md5

Restart PostgreSQL:

sudo systemctl restart postgresql

14. Monitoring and Logging

Production servers need observability. At minimum, install tools to track resource usage and centralize logs.

Resource Monitoring with htop and vnstat

sudo apt install -y htop vnstat
sudo systemctl enable --now vnstat

Prometheus Node Exporter

Node Exporter exposes system metrics for Prometheus to scrape. Install it as a systemd service:

sudo apt install -y prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter

It listens on port 9100 by default. Allow it through the firewall only if your Prometheus server is on a different host, and restrict access to that host's IP:

sudo ufw allow from <prometheus-ip> to any port 9100

Centralized Logging with journald

Systemd's journal already collects logs from all services. Configure persistent storage so logs survive reboots:

sudo nano /etc/systemd/journald.conf
[Journal]
Storage=persistent
SystemMaxUse=500M
MaxRetentionSec=30day

Restart the journal service:

sudo systemctl restart systemd-journald

15. Backup Strategy

Backups are not optional. Identify what needs to be backed up — databases, application data, configuration files — and automate the process. A simple approach uses rsync and pg_dump with a cron job.

Create a backup script:

sudo nano /usr/local/bin/backup.sh
#!/bin/bash
set -euo pipefail

BACKUP_DIR="/var/backups/myapp"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"

# Dump the database
sudo -u postgres pg_dump myapp_prod | gzip > "$BACKUP_DIR/db-$DATE.sql.gz"

# Copy application data
rsync -a /home/deploy/myapp/data "$BACKUP_DIR/data-$DATE"

# Remove backups older than 7 days
find "$BACKUP_DIR" -type f -mtime +7 -delete

echo "Backup completed at $(date)"

Make it executable and schedule it:

sudo chmod +x /usr/local/bin/backup.sh
sudo crontab -e
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

For offsite backups, sync the backup directory to an object storage service like AWS S3 using the AWS CLI or rclone.

16. Best Practices Checklist

17. Automating the Entire Setup with a Script

To avoid repeating these steps manually, combine them into a bootstrap script. This is a starting point you can extend:

#!/bin/bash
set -euo pipefail

# Update system
apt update && apt upgrade -y && apt autoremove -y

# Install essential packages
apt install -y curl wget git vim ufw fail2ban unattended-upgrades htop nginx

# Configure firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable

# Configure automatic security updates
dpkg-reconfigure -plow unattended-upgrades

# Enable fail2ban
systemctl enable --now fail2ban

# Set timezone
timedatectl set-timezone UTC

echo "Server setup complete."

Run it on a fresh install:

sudo bash bootstrap.sh

For production-scale infrastructure, move from a shell script to a configuration management tool. Ansible playbooks are idempotent, readable, and version-controllable, making them the natural next step.

Conclusion

Setting up an Ubuntu Server for production is a methodical process that pays dividends in stability and security. By updating the system, creating non-root users, hardening SSH, configuring a firewall, enabling automatic updates, installing monitoring, and establishing backups, you transform a default installation into a resilient host capable of serving real traffic. The commands in this tutorial form a repeatable baseline — one you should codify into automation as your infrastructure grows. Remember that security and reliability are ongoing practices: review your configuration regularly, apply patches promptly, and test your recovery procedures before you need them. A well-prepared server is the quiet foundation upon which every successful application runs.

— Ad —

Google AdSense will appear here after approval

← Back to all articles