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:
- Security: Reduced attack surface through firewall rules, key-based SSH, and automatic updates.
- Reliability: Predictable behavior across staging and production environments.
- Reproducibility: A documented setup can be automated with tools like Ansible, Terraform, or cloud-init.
- Operational efficiency: Monitoring, logging, and alerting catch problems before users do.
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:
- Choose a minimal install to avoid unnecessary packages.
- Enable OpenSSH server during setup so you can connect remotely after first boot.
- Use LVM or ZFS for flexible disk management if you expect storage growth.
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
- Use LTS releases for production and plan upgrades every two years.
- Never log in as root; use a sudo user and separate service accounts.
- Use SSH keys exclusively; disable password authentication.
- Run the principle of least privilege for firewall rules and user permissions.
- Enable automatic security updates and monitor for required reboots.
- Document every manual change in a runbook or version-controlled repository.
- Automate provisioning with Ansible, Terraform, or cloud-init to avoid configuration drift.
- Test your backup restoration process regularly — an untested backup is not a backup.
- Monitor disk space, memory, CPU, and application health; set up alerts before thresholds are breached.
- Keep secrets out of source code and configuration files; use a secrets manager or environment files with restricted permissions.
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.