Ubuntu Server Initial Setup: 9 Steps to Start on a Fresh Ubuntu Server

Test environmentDebian familyUbuntu 24.04.5 LTSroot · regular user account
Ubuntu server first setup tasks verified

When you get a new server, you run through an Ubuntu server initial setup once before putting any services on it. On an Ubuntu 24.04 server created at DigitalOcean, we connected as root and set things up in order, from package updates to a sudo user, key login, the firewall, the time zone, automatic security updates and swap, then confirmed that the settings stayed in place after a reboot.

The result boxes below are the output of running the commands in this order. The server IP 203.0.113.10 is an example value, and the new regular user is named deploy.

🎯 Key points
Package updates and reboot-required check
sudo group membership for the regular user deploy
deploy key login verified, then root login blocked
ufw allows only SSH, 80 and 443
Time zone change and automatic security update check
2GB swap file and settings kept after reboot
Prerequisites
Connectionroot-login SSH key and an Ubuntu 24.04 server

1️⃣ Ubuntu package update

Even a newly created server has updates waiting to be installed. After downloading a fresh package list, check what will change before actually installing anything.

🔲 apt-get update

export DEBIAN_FRONTEND=noninteractive

The apt-get commands below were run with this value set first. With it, no configuration prompts appear during package installation and the default answers are applied.

apt-get update
Hit:1 http://mirrors.digitalocean.com/ubuntu noble InRelease
Hit:2 http://mirrors.digitalocean.com/ubuntu noble-updates InRelease
Hit:3 http://mirrors.digitalocean.com/ubuntu noble-backports InRelease
Hit:4 https://repos-droplet.digitalocean.com/apt/droplet-agent main InRelease
Hit:5 http://security.ubuntu.com/ubuntu noble-security InRelease
Reading package lists...

🔲 apt-get -s upgrade

apt-get -s upgrade
Calculating upgrade...
The following upgrades have been deferred due to phasing:
  libsqlite3-0
The following packages will be upgraded:
  apt apt-transport-https apt-utils bind9-dnsutils bind9-host bind9-libs
  cloud-init distro-info-data gir1.2-glib-2.0 git git-man libapt-pkg6.0t64
  libbrotli1 libglib2.0-0t64 libglib2.0-bin libglib2.0-data liblzma5
  libmspack0t64 libpcre2-8-0 python3-jwt python3-twisted tmux
  ubuntu-pro-client ubuntu-pro-client-l10n xz-utils zlib1g
26 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.

The -s option is a simulated run that only calculates the result without installing anything. On this server, 26 packages were to be upgraded, and libsqlite3-0 was left out of this upgrade as [deferred due to phasing]. The number of packages differs from server to server, depending on when the server was created and the image used.

🔲 apt-get -y upgrade

apt-get -y upgrade
26 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
Need to get 15.4 MB of archives.
After this operation, 398 kB of additional disk space will be used.

After the installation finishes, a list of restarted services follows.

Running kernel seems to be up-to-date.

Restarting services...
 /etc/needrestart/restart.d/systemd-manager
 systemctl restart cron.service fwupd.service multipathd.service packagekit.service polkit.service ssh.service systemd-journald.service systemd-networkd.service systemd-resolved.service systemd-timesyncd.service systemd-udevd.service udisks2.service

Service restarts being deferred:
 systemctl restart ModemManager.service
 /etc/needrestart/restart.d/dbus.service
 systemctl restart systemd-logind.service
 systemctl restart unattended-upgrades.service

When an update that needs a reboot is installed, the /var/run/reboot-required file is created. This time [Running kernel seems to be up-to-date.] appeared and the file was not created, so we moved on to the next step without rebooting.

2️⃣ Ubuntu add user and sudo privileges

Do everyday work as a regular user and run only administrative commands with sudo. sudo raises privileges with your own password instead of the root password, so there is a record of who ran which command.

🔲 adduser deploy

adduser deploy
info: Adding user `deploy' ...
info: Selecting UID/GID from range 1000 to 59999 ...
info: Adding new group `deploy' (1000) ...
info: Adding new user `deploy' (1000) with group `deploy (1000)' ...
info: Creating home directory `/home/deploy' ...
info: Copying files from `/etc/skel' ...
New password: 
Retype new password: 
passwd: password updated successfully
Changing the user information for deploy
Enter the new value, or press ENTER for the default
	Full Name []: 
	Room Number []: 
	Work Phone []: 
	Home Phone []: 
	Other []: 
Is the information correct? [Y/n] Y
info: Adding new user `deploy' to supplemental / extra groups `users' ...
info: Adding user `deploy' to group `users' ...

adduser wraps useradd; it creates the home folder and group, then asks for a password and user information in turn. The password does not appear on screen as you type, and details such as name and phone number can be left blank with Enter.

🔲 usermod -aG sudo deploy

usermod -aG sudo deploy
groups deploy
deploy : deploy sudo users

🔲 su

Users in the sudo group can run administrative commands with sudo. Switch to the deploy account with su and check its privileges.

su - deploy
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
whoami
deploy
sudo whoami
[sudo] password for deploy: 
root
exit

With sudo, you are asked for deploy's password, and if it is correct the command runs with root privileges.

3️⃣ Register an SSH key for the new user

At first, an SSH key is registered only for root, so key login does not work for deploy. Copy root's authorized_keys into deploy's home folder.

🔲 install -o deploy -g deploy

install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
ls -la /home/deploy/.ssh
total 12
drwx------ 2 deploy deploy 4096 Nov  5 11:48 .
drwxr-x--- 3 deploy deploy 4096 Nov  5 11:48 ..
-rw------- 1 deploy deploy  100 Nov  5 11:48 authorized_keys

install creates the folder, copies the file and sets the owner and permissions in one go. sshd checks the permissions and owner of the user's files before accepting a login (StrictModes), so set the .ssh folder to 700 and authorized_keys to 600.

🔲 ssh deploy@203.0.113.10

Do not close the root window that is open now; first check in a new terminal window that you can connect as deploy.

ssh deploy@203.0.113.10
whoami
deploy
🧭 A setup order that won't lock you out
Explainer diagram
🧭 Steps
🔑① Copy keyroot's key to deploy
➜
🪟② Check in new windowKeep the root window open
➜
⛔③ Block root loginPermitRootLogin no
➜
🪟④ Check in new windowdeploy login still works
✅⑤ Allow SSHufw allow OpenSSH
➜
🧱⑥ Turn on ufwOnly allowed ports open
➜
🪟⑦ Check in new windowLogin still works
Copy root's authorized_keys into deploy's home folder.
Do not close the root window that is open now; check in a new terminal window that key login works as deploy.
Block root login only after the deploy login is confirmed. If you cannot connect in the new window, revert the change in the window you kept open.
After reloading the SSH settings, check in a new window that the deploy login still works.
Add the SSH allow rule before turning on the firewall.
The moment it is turned on, any connection that is not allowed is blocked, so the SSH allow rule has to be in place first.
After turning on the firewall, check in a new window that you can connect as deploy again.

4️⃣ Block SSH root login

In Ubuntu 24.04, the Include line at the top of sshd_config loads the files in the /etc/ssh/sshd_config.d/ folder first, and for the same setting the value read first applies. So before changing anything, check the value that applies now and the file that contains it.

/etc/ssh└─ sshd_config  at the top: Include /etc/ssh/sshd_config.d/*.conf   ├─ ① sshd_config.d/50-cloud-init.conf         PasswordAuthentication no   ├─ ② sshd_config.d/60-cloudimg-settings.conf  PasswordAuthentication no   └─ ③ sshd_config line 42                      PermitRootLogin yes → no

🔲 sshd -T

sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication) '
permitrootlogin yes
passwordauthentication no
sudo grep -rn 'PasswordAuthentication' /etc/ssh/sshd_config.d/
/etc/ssh/sshd_config.d/60-cloudimg-settings.conf:1:PasswordAuthentication no
/etc/ssh/sshd_config.d/50-cloud-init.conf:1:PasswordAuthentication no

Password login is already turned off in two files inside sshd_config.d, so the only change is the root login line in the main file.

🔲 sed -i PermitRootLogin

sudo grep -n '^PermitRootLogin' /etc/ssh/sshd_config
42:PermitRootLogin yes
sudo sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sudo grep -n '^PermitRootLogin' /etc/ssh/sshd_config
42:PermitRootLogin no

🔲 systemctl reload ssh

sudo sshd -t
sudo systemctl reload ssh
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication) '
permitrootlogin no
passwordauthentication no

sshd -t checks the configuration syntax, and if nothing is printed there are no errors. After reloading, confirm in a new window that the deploy login still works, then try connecting as root and see that it is refused.

ssh root@203.0.113.10
root@203.0.113.10: Permission denied (publickey).

Even with the right key you cannot log in as root; administrative work is done by connecting as deploy and using sudo.

5️⃣ Ubuntu firewall ufw

ufw is Ubuntu's default firewall tool, but it is off at first. The moment it is turned on, any connection that is not allowed is blocked, so add the SSH allow rule first and turn it on last.

sudo ufw status verbose
Status: inactive

🔲 ufw allow OpenSSH

sudo ufw app list
Available applications:
  OpenSSH
sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)
sudo ufw allow 80/tcp
Rules updated
Rules updated (v6)
sudo ufw allow 443/tcp
Rules updated
Rules updated (v6)
sudo ufw show added
Added user rules (see 'ufw status' for running firewall):
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp

OpenSSH is a profile that opens port 22, and 80 and 443 are the ports used by a web server. The rules are added for both IPv4 and IPv6.

🔲 ufw enable

sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere                  
80/tcp                     ALLOW IN    Anywhere                  
443/tcp                    ALLOW IN    Anywhere                  
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)             
80/tcp (v6)                ALLOW IN    Anywhere (v6)             
443/tcp (v6)               ALLOW IN    Anywhere (v6)             

Incoming connections are blocked by default, and with only the three allowed ports open, the firewall also starts at boot. After turning it on, connect again as deploy in a new window and check the rule numbers.

sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere                  
[ 2] 80/tcp                     ALLOW IN    Anywhere                  
[ 3] 443/tcp                    ALLOW IN    Anywhere                  
[ 4] OpenSSH (v6)               ALLOW IN    Anywhere (v6)             
[ 5] 80/tcp (v6)                ALLOW IN    Anywhere (v6)             
[ 6] 443/tcp (v6)               ALLOW IN    Anywhere (v6)             

6️⃣ Ubuntu time zone setup

The initial time zone is Etc/UTC. To match log and scheduled job times to the server's region, this server, created in the Singapore region, was set to Asia/Singapore.

timedatectl
               Local time: Thu 2026-11-05 12:00:31 UTC
           Universal time: Thu 2026-11-05 12:00:31 UTC
                 RTC time: Thu 2026-11-05 12:00:31
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

🔲 timedatectl list-timezones

timedatectl list-timezones | grep Singapore
Asia/Singapore

🔲 timedatectl set-timezone

sudo timedatectl set-timezone Asia/Singapore
timedatectl
               Local time: Thu 2026-11-05 20:00:32 +08
           Universal time: Thu 2026-11-05 12:00:32 UTC
                 RTC time: Thu 2026-11-05 12:00:32
                Time zone: Asia/Singapore (+08, +0800)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Only the Local time and Time zone lines change; clock synchronization (NTP) and UTC time stay the same.

7️⃣ unattended upgrades automatic security updates

Ubuntu installs security updates automatically with the unattended-upgrades package, which is installed and enabled from the start. So in this step, check that it is enabled and what it installs automatically.

🔲 dpkg-query -W

dpkg-query -W unattended-upgrades
unattended-upgrades	2.9.1+nmu4ubuntu1
systemctl is-enabled unattended-upgrades apt-daily.timer apt-daily-upgrade.timer
enabled
enabled
enabled

🔲 cat 20auto-upgrades

cat /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

The 1 in both values means once a day, and changing them to 0 turns automatic updates off. What gets installed is decided by the 50unattended-upgrades file.

grep -vE '^[[:space:]]*(//|$)' /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
	"${distro_id}:${distro_codename}";
	"${distro_id}:${distro_codename}-security";
	"${distro_id}ESMApps:${distro_codename}-apps-security";
	"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::DevRelease "auto";

The allowed origins include -security but not -updates, so regular updates are not installed automatically and only security updates are. To receive regular updates automatically as well, uncomment the -updates line in this file.

🔲 unattended-upgrade

sudo unattended-upgrade --dry-run --debug
Starting unattended upgrades script
Allowed origins are: o=Ubuntu,a=noble, o=Ubuntu,a=noble-security, o=UbuntuESMApps,a=noble-apps-security, o=UbuntuESM,a=noble-infra-security

These are the last two lines of the same run.

No packages found that can be upgraded unattended and no pending auto-removals
The list of kept packages can't be calculated in dry-run mode.

--dry-run calculates only what the automatic update would do, without installing anything. Since the package updates had already been done, it reported no packages to install automatically. Records of actual runs are kept in the /var/log/unattended-upgrades/ folder.

sudo ls -l /var/log/unattended-upgrades/
total 32
-rw-r--r-- 1 root root     0 Nov  5 20:00 unattended-upgrades-dpkg.log
-rw-r--r-- 1 root root     0 Nov  5 17:34 unattended-upgrades-shutdown.log
-rw-r--r-- 1 root root 29093 Nov  5 20:04 unattended-upgrades.log

8️⃣ Ubuntu swap memory setup

This server has 1GB of memory and no swap at first. Before putting any apps on it, create a 2GB swap file and register it in /etc/fstab so that it is turned on after a reboot as well.

free -h
               total        used        free      shared  buff/cache   available
Mem:           961Mi       328Mi       161Mi       4.0Mi       631Mi       633Mi
Swap:             0B          0B          0B
df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        24G  2.0G   22G   9% /

🔲 fallocate -l 2G /swapfile

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
ls -lh /swapfile
-rw------- 1 root root 2.0G Nov  5 20:04 /swapfile

🔲 mkswap /swapfile

sudo mkswap /swapfile
Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
no label, UUID=0b43ccd8-5b4a-4048-9b84-eee12667b387

🔲 swapon /swapfile

sudo swapon /swapfile
swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   2G   0B   -2
free -h
               total        used        free      shared  buff/cache   available
Mem:           961Mi       328Mi       160Mi       4.0Mi       632Mi       632Mi
Swap:          2.0Gi          0B       2.0Gi

🔲 tee -a /etc/fstab

swapon only turns it on for now; to have it turned on after a reboot as well, add one line to fstab.

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

9️⃣ Ubuntu server initial setup check

Once you confirm that the settings survive a reboot, the Ubuntu initial setup is complete.

🔲 systemctl reboot

sudo systemctl reboot

🔲 uptime -p

After the reboot, connect again as deploy.

uptime -p
up 2 minutes
swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   2G   0B   -2
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication) '
permitrootlogin no
passwordauthentication no
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere                  
80/tcp                     ALLOW IN    Anywhere                  
443/tcp                    ALLOW IN    Anywhere                  
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)             
80/tcp (v6)                ALLOW IN    Anywhere (v6)             
443/tcp (v6)               ALLOW IN    Anywhere (v6)             
timedatectl | head -n 4
               Local time: Thu 2026-11-05 20:06:35 +08
           Universal time: Thu 2026-11-05 12:06:35 UTC
                 RTC time: Thu 2026-11-05 12:06:35
                Time zone: Asia/Singapore (+08, +0800)
systemctl is-active unattended-upgrades ssh.socket
active
active

Swap, SSH settings, the firewall, the time zone and automatic updates are all the same as before the reboot. Next, we will deploy a Node.js app on this server with PM2 and Nginx.

🔢 FAQ & Recommended Content

You can leave it as it is. A package left out as [deferred due to phasing] is an update in a phased rollout. Ubuntu releases some updates to only part of the servers at first and, if there are no problems, widens the rollout to all servers. Security updates are not phased, and the package that was left out is installed with the next upgrade once the rollout widens.

The automatic update job runs once a day, and the two timers apt-daily.timer and apt-daily-upgrade.timer run it with a random delay after a set time. On this server, the next run of apt-daily-upgrade.timer was scheduled for 06:27:59 (+08) the next day. If the server is off and misses that time, the job can run right at the next boot, so apt tasks may wait for a moment just after booting.

Not with the default settings. The default value of Automatic-Reboot in 50unattended-upgrades is false, and that line is not enabled in this server's settings either. When an update that needs a reboot is installed, the /var/run/reboot-required file is created, so reboot manually when that file exists.

ℹ️ Affiliate Disclosure
This site's content contains affiliate links. When a visitor buys a product or service through one of them, the site receives a commission from the seller. The amount the buyer pays(it goes down during event discounts ↓)does not go up. Posted prices, discounts, and stock reflect the time of writing and may differ, so confirm with the seller before buying. Products are chosen and reviewed by our own standards, and commissions do not affect the order or content of reviews.

Leave a Reply

Your email address will not be published. Required fields are marked *

Prove your humanity: 7   +   9   =