To work confidently on a Linux server, a developer needs five skills: logging in with an SSH key, working as a regular user with sudo, understanding file owners and permissions, finding processes and busy ports, and running the app as a systemd service with logs in journalctl. Below is the whole initial setup of an Ubuntu 24.04 LTS VPS with real command output and a final checklist.
After this you will be able to
- create an ed25519 SSH key, verify the server fingerprint and log in through an alias in
~/.ssh/config - set up a user with
sudo, fix~/.sshpermissions and find the cause of an access denial withjournalctlandnamei -l - find a process and a busy port with
pgrep,psandss -tlnpand check the app's response withcurl - describe the app as a systemd service with restarts, read its journal and disable password login after checking with
sshd -t
Know beforehand
Verified with
Ubuntu 24.04.5 LTSOpenSSH 9.6p1systemd 255bash 5.2ufw 0.36.2Python 3.12 (http.server)Node.js 24.18
Checked against docs: October 11, 2026
Contents
The server from the inside: where things live
A VPS is a remote Linux machine without a graphical interface: everything is done with commands over SSH. This lesson continues “Terminal and command line”, which already covers navigation, redirection and chmod basics. Here we cover what only shows up on a server: other users, services, networking and security. The examples were checked on Ubuntu 24.04.5 LTS. Ubuntu LTS releases get five years of standard security maintenance; for 24.04 that runs until May 2029.
We tested on two Docker containers: a “server” with systemd and OpenSSH and a “laptop” with only the SSH client. In the examples the server address is replaced with the documentation address 203.0.113.10 — use your VPS IP. The # prompt means a command run as root, $ — as a regular user.
/etc
- What lives there
- Configuration of this machine
- Example
/etc/ssh/sshd_config,/etc/systemd/system
/home, /root
- What lives there
- Home directories of users and root
- Example
/home/deploy/.ssh/authorized_keys
/srv
- What lives there
- Data the server serves to others
- Example
/srv/app— the project in this lesson
/opt
- What lives there
- Third-party software, each in its own folder
- Example
- A vendor's monitoring agent
/var/log, /var/lib
- What lives there
- Log files and changing program state
- Example
auth.log,dpkg.log; database data
/tmp
- What lives there
- Temporary files, may be deleted without notice
- Example
- Nothing valuable
/usr/bin, /usr/local/bin
- What lives there
- Programs;
localholds manually installed ones - Example
/usr/local/bin/node
# ls -C /
bin etc lib64 proc sbin.usr-is-merged usr
bin.usr-is-merged home media root srv var
boot lib mnt run sys
dev lib.usr-is-merged opt sbin tmp
# df -h /
Filesystem Size Used Avail Use% Mounted on
overlay 1007G 156G 800G 17% /
# du -sh /var/log /usr
26M /var/log
280M /usr
# ls -C /var/log
README auth.log dmesg journal private
alternatives.log bootstrap.log dpkg.log kern.log syslog
apt btmp faillog lastlog wtmpoverlay filesystem and its size come from Docker; on a VPS you will see a disk like /dev/vda1. The *.usr-is-merged directories are service markers of the /bin-into-/usr/bin merge in Ubuntu 24.04.SSH: a key instead of a password
SSH is an encrypted channel to the server. A key has two parts: the private one stays on your laptop, the public one goes to the server into ~/.ssh/authorized_keys. A password can be brute-forced; a key, in practice, cannot. The Ubuntu documentation recommends the ed25519 type — it is shorter and faster than RSA; if you need RSA somewhere, use a 4096-bit key.
$ ssh-keygen -t ed25519 -C "dev@laptop"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/dev/.ssh/id_ed25519):
Created directory '/home/dev/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/dev/.ssh/id_ed25519
Your public key has been saved in /home/dev/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:rQv/ITuF0G35TpciyF1zw3hCADi0ObFTTv7dfQ/g2J0 dev@laptop
$ ls -l ~/.ssh
total 16
-rw------- 1 dev dev 444 Oct 12 01:35 id_ed25519
-rw-r--r-- 1 dev dev 92 Oct 12 01:35 id_ed25519.pub
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICJoFk+NOCBCDZTysLYYWbhJNTTZN/nijzyyyRBFj42/ dev@laptop- The passphrase encrypts the private key on disk: if the laptop is stolen, the key is useless without it
- The
.pubfile can be shown to anyone — that is what goes to the server; the privateid_ed25519is never shared, not even with colleagues ssh-keygensets the private key permissions to600itself — owner only- One key per device: lose a laptop and you delete one line from
authorized_keyswhile other access keeps working
How the public key gets to the server. Most VPS providers have an “SSH key” field when you create a server — paste the contents of id_ed25519.pub there and it ends up in /root/.ssh/authorized_keys. If the provider gave you only a login and password, use ssh-copy-id: it logs in with the password one last time and appends the key. By default OpenSSH does not let root log in with a password (PermitRootLogin prohibit-password), so a root password only works if the provider changed that setting.
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub ops@203.0.113.10
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/dev/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
ops@203.0.113.10's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'ops@203.0.113.10'"
and check to make sure that only the key(s) you wanted were added.ops account that had a password (password login was temporarily allowed for that account only and removed after the test).On the first connection the client shows the server's key fingerprint and asks whether to trust it. After yes the fingerprint is stored in ~/.ssh/known_hosts, and if it ever changes, SSH refuses to connect with a loud warning — protection against a spoofed server.
$ ssh root@203.0.113.10 'hostname; whoami'
The authenticity of host '203.0.113.10 (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:IaTy+ntmy5vPYW0mWxRtuKucgJ/6xml8wEPCiK2pPoA.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '203.0.113.10' (ED25519) to the list of known hosts.
Enter passphrase for key '/home/dev/.ssh/id_ed25519':
vps
rootvps.# ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:IaTy+ntmy5vPYW0mWxRtuKucgJ/6xml8wEPCiK2pPoA root@vps (ED25519)Users, groups and sudo
Working as root is dangerous: any typo runs with full privileges. On Ubuntu the root password is locked by default, and admin rights are granted through sudo to members of the sudo group. So the first step on a new server is to create a regular user, for example deploy, and give it sudo.
# 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' ...
# usermod -aG sudo deploy
# id deploy
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),100(users)
# ls -ld /home/deploy
drwxr-x--- 2 deploy deploy 4096 Oct 12 01:35 /home/deploydeploy password will be needed for sudo; SSH login will use the key.adduseris Ubuntu's friendly wrapper: it creates a group with the same name and a home directory and asks for a password. The low-leveluseraddwithout flags does not even create a home directoryuid=1000(deploy) gid=1000(deploy)are numeric IDs: the kernel checks permissions by numbers, names are for humans- The
-aflag inusermod -aGis mandatory: without itusermod -G users deployreplaces the group list and the user drops out ofsudo(verified). The Ubuntu documentation variant isadduser deploy sudo - On Ubuntu 21.10 and newer, home directories are created with
750permissions: other users cannot look inside
Now the key: copy root's authorized_keys into deploy's home. A typical mistake is copying as root and forgetting about the owner.
# install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
# cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
# ls -l /home/deploy/.ssh
total 4
-rw------- 1 root root 92 Oct 12 01:35 authorized_keysinstall -d creates the folder with permissions and owner at once; but cp run as root left root as the file owner.$ ssh -o BatchMode=yes deploy@203.0.113.10 whoami
deploy@203.0.113.10: Permission denied (publickey,password).
# journalctl -u ssh -n 2 --no-pager
Oct 12 01:36:04 vps sshd[278]: Could not open user 'deploy' authorized keys '/home/deploy/.ssh/authorized_keys': Permission denied
Oct 12 01:36:04 vps sshd[278]: Connection closed by authenticating user deploy 198.51.100.23 port 52416 [preauth]BatchMode=yes stops the client from asking for a password, so we see that the key was rejected.The client error explains nothing, while the server log names the cause directly: sshd reads keys as user deploy, but the file belongs to root and is locked with 600. Fix the owner:
# chown deploy:deploy /home/deploy/.ssh/authorized_keys
# chmod 600 /home/deploy/.ssh/authorized_keys
# ls -la /home/deploy/.ssh
total 12
drwx------ 2 deploy deploy 4096 Oct 12 01:35 .
drwxr-x--- 3 deploy deploy 4096 Oct 12 01:35 ..
-rw------- 1 deploy deploy 92 Oct 12 01:35 authorized_keyschown key login works.To avoid typing the IP and user name every time, create ~/.ssh/config on the laptop with an alias, and let ssh-agent hold the key passphrase — it remembers it until the end of the session.
Host myvps
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesHost blocks go above general rules. The file must not be writable by others — set 600.$ eval "$(ssh-agent -s)"
Agent pid 363
$ ssh-add
Enter passphrase for /home/dev/.ssh/id_ed25519:
Identity added: /home/dev/.ssh/id_ed25519 (dev@laptop)
$ chmod 600 ~/.ssh/config
$ ssh myvps 'whoami; id'
deploy
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),100(users)
$ ssh -t myvps 'sudo whoami; sudo -l'
[sudo] password for deploy:
root
Matching Defaults entries for deploy on vps:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User deploy may run the following commands on vps:
(ALL : ALL) ALL
Connection to 203.0.113.10 closed.sudo asks for deploy's own password, not root's, and remembers it for a while.Owners and permissions on a server: chown, umask, namei
How to read rwxr-x--- and what chmod 644 and chmod 600 mean is covered in the terminal lesson. On a server the question “who” is added: every file has an owner and a group, and every process runs as some user. Access is checked against the triple that matches the process: owner, otherwise group, otherwise everyone else. Let's prepare the project folder: create it with sudo and hand it to deploy.
$ sudo mkdir -p /srv/app/public
$ ls -ld /srv/app
drwxr-xr-x 3 root root 4096 Oct 12 01:37 /srv/app
$ sudo chown -R deploy:deploy /srv/app
$ ls -ld /srv/app /srv/app/public
drwxr-xr-x 3 deploy deploy 4096 Oct 12 01:37 /srv/app
drwxr-xr-x 2 deploy deploy 4096 Oct 12 01:37 /srv/app/public
$ umask
0002
$ echo '<h1>Hello from /srv/app</h1>' > /srv/app/public/index.html
$ printf 'PORT=8080\nAPP_SECRET=change-me\n' > /srv/app/.env
$ ls -la /srv/app /srv/app/public
/srv/app:
total 16
drwxr-xr-x 3 deploy deploy 4096 Oct 12 01:37 .
drwxr-xr-x 1 root root 4096 Oct 12 01:37 ..
-rw-rw-r-- 1 deploy deploy 31 Oct 12 01:37 .env
drwxr-xr-x 2 deploy deploy 4096 Oct 12 01:37 public
/srv/app/public:
total 12
drwxr-xr-x 2 deploy deploy 4096 Oct 12 01:37 .
drwxr-xr-x 3 deploy deploy 4096 Oct 12 01:37 ..
-rw-rw-r-- 1 deploy deploy 29 Oct 12 01:37 index.html
$ chmod 600 /srv/app/.env
$ chmod 750 /srv/app
$ stat -c '%A %a %U:%G %n' /srv/app /srv/app/.env /srv/app/public/index.html
drwxr-x--- 750 deploy:deploy /srv/app
-rw------- 600 deploy:deploy /srv/app/.env
-rw-rw-r-- 664 deploy:deploy /srv/app/public/index.html
$ sudo adduser --disabled-password --comment "" intern > /dev/null
$ sudo -u intern cat /srv/app/public/index.html
cat: /srv/app/public/index.html: Permission denied
$ sudo -u intern ls /srv/app
ls: cannot open directory '/srv/app': Permission denied
$ namei -l /srv/app/public/index.html
f: /srv/app/public/index.html
drwxr-xr-x root root /
drwxr-xr-x root root srv
drwxr-x--- deploy deploy app
drwxr-xr-x deploy deploy public
-rw-rw-r-- deploy deploy index.htmldeploy session over SSH, real output. The intern user exists only to test access.sudo mkdircreated the folder as root —deploycould not write into it.chown -R deploy:deployhanded over owner and group recursivelyumask 0002is the permission mask for new files:002is removed from666, giving664(rw-rw-r--). That is why.envwas world-readable after creation and had to be locked explicitly withchmod 600- The
xpermission on a folder means “may enter”. Outsiders cannot enter/srv/appwith750, so theindex.htmlinside with664is out of their reach too namei -lshows the permissions at every step of the path — you immediately see which directory blocks access
~/.ssh
- Permissions and owner
700, owned by the user- Why
sshdrejects keys if others can modify the folder
~/.ssh/authorized_keys
- Permissions and owner
600, owned by the user- Why
- Read as the user; only the user writes to it
/srv/app
- Permissions and owner
750,deploy:deploy- Why
- Full access for the app, read for the group, nothing for others
/srv/app/.env
- Permissions and owner
600, owned by the service user- Why
- Only the app sees passwords and keys
/etc/systemd/system/*.service
- Permissions and owner
644,root:root- Why
- Only the administrator edits the service definition
Processes, ports and curl
A process is a running program with a PID and an owning user. Let's start a simple Python web server from the project folder in the background and look at it with the main tools: pgrep finds processes by name, ps gives a snapshot of the list, top a live load table (quit with q).
$ nohup python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/app/public > /tmp/http.log 2>&1 &
$ sleep 1; pgrep -a python3
2241 python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/app/public
$ ps -eo pid,user,%cpu,%mem,cmd --sort=-%mem | head -5
PID USER %CPU %MEM CMD
2241 deploy 0.9 0.0 python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/app/public
24 root 0.0 0.0 /usr/lib/systemd/systemd-journald
1 root 0.0 0.0 /sbin/init
2225 root 0.0 0.0 sshd: deploy [priv]
$ top -b -n 1 | head -8
top - 01:47:17 up 3 days, 11:53, 0 user, load average: 2.15, 3.11, 3.34
Tasks: 20 total, 1 running, 19 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 97.9 id, 0.0 wa, 0.0 hi, 1.7 si, 0.0 st
MiB Mem : 31936.7 total, 4438.6 free, 4555.4 used, 23685.6 buff/cache
MiB Swap: 8192.0 total, 8190.3 free, 1.7 used. 27381.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 21336 12752 9684 S 0.0 0.0 0:00.63 systemd
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=2066,fd=3),("systemd",pid=1,fd=93))
LISTEN 0 4096 127.0.0.11:41039 0.0.0.0:*
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=2241,fd=3))
LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=2066,fd=4),("systemd",pid=1,fd=94))
$ curl -sS http://127.0.0.1:8080/
<h1>Hello from /srv/app</h1>
$ curl -sI http://127.0.0.1:8080/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.3
Date: Mon, 12 Oct 2026 01:47:17 GMT
Content-type: text/html
Content-Length: 29
Last-Modified: Mon, 12 Oct 2026 01:37:06 GMT
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/missing
404
$ curl -sS http://127.0.0.1:8081/
curl: (7) Failed to connect to 127.0.0.1 port 8081 after 0 ms: Couldn't connect to server
$ kill $(pgrep -f 'http[.]server 8080')
$ sleep 1; pgrep -a python3 || echo "no python3 processes"
no python3 processes
$ cat /tmp/http.log
127.0.0.1 - - [12/Oct/2026 01:47:17] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [12/Oct/2026 01:47:17] "HEAD / HTTP/1.1" 200 -
127.0.0.1 - - [12/Oct/2026 01:47:17] code 404, message File not found
127.0.0.1 - - [12/Oct/2026 01:47:17] "GET /missing HTTP/1.1" 404 -deploy session, real output. The 127.0.0.11:41039 line is Docker's internal DNS and will not appear on a VPS; load and memory in top belong to the test host.ss -tlnp is the main answer to “who is listening on this port”: t — TCP, l — listening sockets only, n — numbers instead of names, p — the process. Without sudo, other users' processes are not shown. Note the address: 127.0.0.1:8080 is reachable only from the server itself, 0.0.0.0:22 — from all interfaces. On Ubuntu 24.04 port 22 is held by systemd itself (the ssh.socket unit), which starts sshd on connection — you can see it in the process column.
curl -sS URL
- What it shows
- The response body;
-shides the progress meter,-Skeeps error messages
curl -sI URL
- What it shows
- Response headers only: status, type, length
curl -s -o /dev/null -w '%{http_code}\n' URL
- What it shows
- Only the status code — handy in check scripts
curl: (7) Failed to connect
- What it shows
- Nobody listens on the port: the process is not running or listens on another address
SIGTERM (15)
- How to send
kill PID— the default- What happens
- A request to exit: the program can close connections and save data
SIGINT (2)
- How to send
- Ctrl+C in a terminal
- What happens
- Interrupt from the terminal, usually behaves like
SIGTERM
SIGHUP (1)
- How to send
kill -HUP PID- What happens
- Many daemons reload their configuration on it
SIGKILL (9)
- How to send
kill -9 PID- What happens
- Immediate kill by the kernel, cannot be caught — a last resort
A process started with nohup … & survives logging out of SSH, but not a reboot, and if it crashes nobody brings it back. An app on a server needs a service manager — systemd.
systemd: the app as a service, and logs
systemd is the first process of the system (PID 1): it starts services at boot, restarts crashed ones and collects their output into the journal. A service is described by a unit file. Your own units go to /etc/systemd/system. Let's describe our web server — it takes the port from .env.
[Unit]
Description=Hello web: static files from /srv/app/public
After=network.target
[Service]
Type=exec
User=deploy
Group=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/usr/bin/python3 -m http.server ${PORT} --bind 127.0.0.1 --directory /srv/app/public
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.targetExecStart=/usr/local/bin/node server.mjs — such a unit for Node.js is built in the exercise below. Create the file with sudo nano.User=, Group=
- Why
- The process runs as
deploy, not root
EnvironmentFile=
- Why
- Variables from
.env; systemd reads the file, so the600permissions owned bydeploydo not get in its way
ExecStart=
- Why
- The start command with an absolute path;
${PORT}is taken from the environment
Restart=on-failure, RestartSec=2
- Why
- Restart after 2 seconds if the process crashed
WantedBy=multi-user.target
- Why
- What
systemctl enablehooks into: start on a normal boot
$ sudo cp /tmp/hello-web.service /etc/systemd/system/hello-web.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now hello-web
Created symlink /etc/systemd/system/multi-user.target.wants/hello-web.service → /etc/systemd/system/hello-web.service.
$ systemctl status hello-web --no-pager
● hello-web.service - Hello web: static files from /srv/app/public
Loaded: loaded (/etc/systemd/system/hello-web.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-10-12 01:47:42 UTC; 5ms ago
Main PID: 2384 (python3)
Tasks: 1 (limit: 38311)
Memory: 2.4M (peak: 2.4M)
CPU: 3ms
CGroup: /system.slice/hello-web.service
└─2384 /usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 --dir…
$ curl -sS http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server
$ sleep 1; curl -sS http://127.0.0.1:8080/
<h1>Hello from /srv/app</h1>
$ journalctl -u hello-web -n 5 --no-pager
Hint: You are currently not seeing messages from other users and the system.
Users in groups 'adm', 'systemd-journal' can see all messages.
Pass -q to turn off this notice.
Oct 12 01:47:43 vps python3[2384]: 127.0.0.1 - - [12/Oct/2026 01:47:43] "GET / HTTP/1.1" 200 -
$ sudo journalctl -u hello-web -n 5 --no-pager
Oct 12 01:47:42 vps systemd[1]: Starting hello-web.service - Hello web: static files from /srv/app/public...
Oct 12 01:47:42 vps systemd[1]: Started hello-web.service - Hello web: static files from /srv/app/public.
Oct 12 01:47:43 vps python3[2384]: 127.0.0.1 - - [12/Oct/2026 01:47:43] "GET / HTTP/1.1" 200 -
$ sudo kill -9 $(systemctl show -p MainPID --value hello-web)
$ sleep 3; systemctl status hello-web --no-pager | head -5
● hello-web.service - Hello web: static files from /srv/app/public
Loaded: loaded (/etc/systemd/system/hello-web.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-10-12 01:47:46 UTC; 815ms ago
Main PID: 2401 (python3)
Tasks: 1 (limit: 38311)
$ sudo journalctl -u hello-web -n 6 --no-pager
Oct 12 01:47:43 vps python3[2384]: 127.0.0.1 - - [12/Oct/2026 01:47:43] "GET / HTTP/1.1" 200 -
Oct 12 01:47:43 vps systemd[1]: hello-web.service: Main process exited, code=killed, status=9/KILL
Oct 12 01:47:43 vps systemd[1]: hello-web.service: Failed with result 'signal'.
Oct 12 01:47:46 vps systemd[1]: hello-web.service: Scheduled restart job, restart counter is at 1.
Oct 12 01:47:46 vps systemd[1]: Starting hello-web.service - Hello web: static files from /srv/app/public...
Oct 12 01:47:46 vps systemd[1]: Started hello-web.service - Hello web: static files from /srv/app/public.
$ sudo kill $(systemctl show -p MainPID --value hello-web)
$ sleep 3; systemctl is-active hello-web; systemctl is-enabled hello-web
inactive
enabled
$ sudo systemctl start hello-web
$ systemctl is-active hello-web
active
$ sudo journalctl -u hello-web --since "10 min ago" -p warning --no-pager
Oct 12 01:47:43 vps systemd[1]: hello-web.service: Main process exited, code=killed, status=9/KILL
Oct 12 01:47:43 vps systemd[1]: hello-web.service: Failed with result 'signal'.
$ systemctl list-units --type=service --state=failed --no-pager
UNIT LOAD ACTIVE SUB DESCRIPTION
0 loaded units listed.deploy session, systemd 255, real output (colours removed).Let's go through what happened. The first curl right after start got no answer: with Type=exec systemd considers the service started as soon as the program is executed, not when it opens the port. Post-deploy checks therefore retry the request a few times or wait for a health endpoint. The service survived kill -9: Restart=on-failure restarts a process killed by a signal. A plain kill (SIGTERM), however, counts as a clean exit — according to the systemd.service documentation, SIGHUP, SIGINT, SIGTERM and SIGPIPE are not failures, so the service stayed stopped. If you need it back no matter what, there is Restart=always.
-u hello-web
- What it does
- Only entries of this unit
-f
- What it does
- Follow new entries as they appear, like
tail -f
-n 50
- What it does
- The last 50 lines (10 without a number)
--since "10 min ago", --since today
- What it does
- Entries for a period
-p warning
- What it does
- Only warnings and above
-b
- What it does
- Only since the last boot
Packages, the firewall and key-only login
Software on Ubuntu comes from repositories via apt. apt update only refreshes the package lists, apt upgrade installs new versions — so the first, then the second. apt is meant for humans; for scripts the Ubuntu documentation recommends apt-get. Package sources on 24.04 are described in /etc/apt/sources.list.d/ubuntu.sources, and by default Ubuntu Server applies security updates automatically.
$ sudo apt update
Hit:1 http://security.ubuntu.com/ubuntu noble-security InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Hit:4 http://archive.ubuntu.com/ubuntu noble-backports InRelease
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
2 packages can be upgraded. Run 'apt list --upgradable' to see them.
$ apt list --upgradable
Listing... Done
libaudit-common/noble-updates 1:3.1.2-2.1ubuntu0.1 all [upgradable from: 1:3.1.2-2.1build1.1]
libaudit1/noble-updates 1:3.1.2-2.1ubuntu0.1 amd64 [upgradable from: 1:3.1.2-2.1build1.1]
$ sudo apt install -y tree ufw
…
The following NEW packages will be installed:
iptables libip4tc2 libip6tc2 libjansson4 libnetfilter-conntrack3
libnfnetlink0 libnftables1 libnftnl11 nftables tree ufw
0 upgraded, 11 newly installed, 0 to remove and 2 not upgraded.
…
Setting up tree (2.1.1-2ubuntu3.24.04.2) ...
Setting up ufw (0.36.2-6) ...
…
$ tree -a /srv/app
/srv/app
├── .env
└── public
└── index.html
2 directories, 2 files
$ apt policy tree
tree:
Installed: 2.1.1-2ubuntu3.24.04.2
Candidate: 2.1.1-2ubuntu3.24.04.2
Version table:
*** 2.1.1-2ubuntu3.24.04.2 500
500 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 Packages
100 /var/lib/dpkg/status
2.1.1-2ubuntu3 500
500 http://archive.ubuntu.com/ubuntu noble/universe amd64 Packages
$ sudo apt remove -y tree
…
The following packages will be REMOVED:
tree
…
Removing tree (2.1.1-2ubuntu3.24.04.2) ...
$ grep -E " (install|remove) tree" /var/log/dpkg.log
2026-10-12 01:40:02 install tree:amd64 <none> 2.1.1-2ubuntu3.24.04.2
2026-10-12 01:40:04 remove tree:amd64 2.1.1-2ubuntu3.24.04.2 <none>…). Every install and removal is logged to /var/log/dpkg.log.The ufw firewall is disabled on Ubuntu initially. Order matters: allow SSH first, then enable — otherwise new connections to the server become impossible. The OpenSSH profile opens port 22/tcp. All other incoming connections are refused once it is enabled: in our check a test server on 0.0.0.0:8090 stopped answering from outside (curl timed out).
$ sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)
$ 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), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6) The last step is to forbid password and root logins. SSH server settings are read from /etc/ssh/sshd_config, which on Ubuntu first includes the files from /etc/ssh/sshd_config.d/. For sshd the first value found wins, so give your file a low number — it will be read before files the provider or cloud-init may have added.
# Key-only login; root cannot log in over SSH
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no644, owned by root.$ sudo nano /etc/ssh/sshd_config.d/01-hardening.conf
$ echo 'PasswordAuthentification no' | sudo tee /etc/ssh/sshd_config.d/02-typo.conf
PasswordAuthentification no
$ sudo sshd -t
/etc/ssh/sshd_config.d/02-typo.conf: line 1: Bad configuration option: PasswordAuthentification
/etc/ssh/sshd_config.d/02-typo.conf: terminating, 1 bad configuration options
$ sudo rm /etc/ssh/sshd_config.d/02-typo.conf
$ sudo sshd -t && echo "config OK"
config OK
$ sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|kbdinteractiveauthentication|pubkeyauthentication) '
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
$ sudo systemctl restart ssh.service
$ systemctl is-active ssh.socket ssh.service
active
activedeploy session, real output. The typo is added on purpose to show how sshd -t catches an error before the restart.$ ssh -o PubkeyAuthentication=no myvps
deploy@203.0.113.10: Permission denied (publickey).
$ ssh root@203.0.113.10
root@203.0.113.10: Permission denied (publickey).
$ ssh myvps hostname
vpsdeploy key login works.sshd -T prints the effective settings — that is how you confirm the file was applied. On Ubuntu 24.04 port 22 is held by ssh.socket, so look up port changes in the documentation for this exact version: with socket activation the steps differ from guides for older releases.
Initial setup checklist and exercises
- An ed25519 key with a passphrase; the server fingerprint is checked against the provider console
- A user with
sudois created; key login as that user is tested in a new window - In
sshd_config.d:PasswordAuthentication no,PermitRootLogin no;sudo sshd -treports no errors ufw:OpenSSHallowed, then enabled;sudo ufw status verboseshows the rulessudo apt updateandsudo apt upgradeare done- The project lives in
/srv/app, owned by the service user,.envwith600 - The app runs as a systemd service with
Restart=on-failure, enabled viaenable, listening on127.0.0.1 - You know where to look:
journalctl -u <service>,/var/log/auth.log,ss -tlnp
Next step — Docker: the same server, but the app is packaged into an image with all its dependencies and starts with one command. The skills from this lesson — users, permissions, ports and logs — are needed inside containers too.
How this was verified14 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.