Skip to main content

Ubuntu server setup

Quick reference: Ubuntu Server Cheatsheet

Steps that take a blank Ubuntu VPS (24.04/26.04 LTS) to a server that runs an app: lock down access first, then install the database, the reverse proxy, and the process manager.

1. Prepare: user and updates​

Do not work directly as root. Create a user with sudo, update the system, and turn on automatic security updates:

adduser deploy
usermod -aG sudo deploy

sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

2. SSH key​

Key authentication is safer and easier than a password, and it blocks automated password guessing.

On your machine (the client) — create a key pair, then copy the public key to the server:

ssh-keygen -t ed25519 -C "you@example.com" # older systems: -t rsa -b 4096
ssh-copy-id deploy@server_ip

ssh-copy-id adds the key to ~/.ssh/authorized_keys with the right permissions.

On the server — once you can log in with the key, turn off password login and root login:

/etc/ssh/sshd_config.d/00-hardening.conf
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
sudo sshd -t # check the syntax
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin' # see the actual values
sudo systemctl restart ssh
warning
  • With sshd, the first value read wins. Ubuntu cloud images often have /etc/ssh/sshd_config.d/50-cloud-init.conf setting PasswordAuthentication yes, so your file must be named to sort first (00-…) or you edit that file directly. Always check with sshd -T.
  • Keep the current SSH session open and try a login from another terminal before you close it, so you do not lock yourself out.

3. Firewall and brute-force protection​

UFW​

Deny all incoming connections by default. Open only what you need.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # open SSH BEFORE you enable the firewall
sudo ufw enable

sudo ufw allow 'Nginx Full' # 80 + 443, after you install Nginx
sudo ufw status verbose
Docker bypasses UFW

Ports published by Docker (-p 5432:5432, ports: in Compose) are opened by Docker directly in iptables and do not pass through UFW rules — a database can be exposed on the internet even when ufw status does not allow it. Bind to localhost (127.0.0.1:5432:5432) or do not publish ports for internal services.

Fail2ban​

Watches logs and bans IPs that look suspicious, for example many failed SSH logins. On Ubuntu, the sshd jail is enabled after you install it.

sudo apt install fail2ban -y
sudo fail2ban-client status sshd

Customize it in /etc/fail2ban/jail.local (do not edit jail.conf; that file is overwritten on upgrade):

/etc/fail2ban/jail.local
[sshd]
maxretry = 5
findtime = 10m
bantime = 1h

4. Database​

Always create a separate user for the app instead of using an admin account, and keep the database listening only on localhost unless you really need remote access.

sudo apt install mysql-server -y
sudo mysql_secure_installation # remove anonymous users, the test database, and block remote root login
sudo mysql # on Ubuntu, root logs in via auth_socket, no password
CREATE DATABASE my_app_db;
CREATE USER 'my_app_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT ALL PRIVILEGES ON my_app_db.* TO 'my_app_user'@'localhost';

FLUSH PRIVILEGES is not needed when you use CREATE USER / GRANT. See also: MySQL.

5. Nginx and SSL​

Nginx takes traffic on ports 80/443, handles SSL, then forwards to the app on an internal port. One server can then run several apps, and the app does not need permission to open a low port.

sudo apt install nginx certbot python3-certbot-nginx -y

Create the config file in /etc/nginx/sites-available/your_domain.com:

server {
listen 80;
server_name your_domain.com www.your_domain.com;

location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # for WebSocket
proxy_set_header Connection "upgrade";
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, test the config, then let Certbot obtain the certificate and add the HTTPS redirect itself:

sudo ln -s /etc/nginx/sites-available/your_domain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d your_domain.com -d www.your_domain.com
sudo certbot renew --dry-run # test automatic renewal

6. Process management​

The app needs to start again on its own after a crash and after a server reboot.

Already on Ubuntu. It can run any kind of app. Nothing extra to install.

/etc/systemd/system/my-app.service
[Unit]
Description=My app
After=network.target

[Service]
User=deploy
WorkingDirectory=/var/www/my-app
ExecStart=/usr/bin/java -jar app.jar
Restart=on-failure
EnvironmentFile=/var/www/my-app/.env

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now my-app
journalctl -u my-app -f # view the log

Summary: defense layers​

LayerMeasureAgainst
AccessSSH key, password login and root login offPassword guessing, leaked passwords
NetworkUFW opens only 22, 80, 443Access to internal services
DetectionFail2banProlonged brute-force
Operating systemunattended-upgradesVulnerabilities that already have a patch
WebNginx + HTTPS (Certbot), block hidden filesEavesdropping, leaked .env/.git
DataA separate app user, listen on localhost only, authentication onUnauthorized database access