All writing Notes & observations

A Practical Linux Tutorial: From the Basics to Everyday Use and Service Management

Start with Ubuntu setup and learn terminal commands, file permissions, log analysis, development environments, processes, networking, SSH, systemd, and Shell automation through complete examples of service management, backup, recovery, and troubleshooting.

Linux is widely used on servers, in development environments, and on many other computing devices. Everyday Linux work revolves around terminals, files, permissions, processes, and networking: using commands to manage files and software, run programs, inspect logs, and investigate problems step by step.

This tutorial starts from the basics and combines concepts with practical operations. It uses Ubuntu 24.04 LTS, Bash, and a small HTTP service to introduce file operations, permissions, log analysis, development environments, processes, and networking before moving on to service management and Shell automation. All example code is included in the article.

You do not need to learn Python first or rent a cloud server. Windows users can use WSL 2. An existing installation of a recent Ubuntu release also works, although software versions and some command output may differ.

1. Reading Path and Example Conventions

If you are new to Linux, work through the sections in order and run the examples. Once you know the basics, use individual sections as a reference. Here is how the topics fit together:

Topic Main operations Sections
Environment and files Set up Linux; create, edit, find, and manage files 2–3
Permissions and logs Understand access permissions; combine commands to analyze text and logs 4–5
Development and processes Configure dependencies, run programs, and inspect processes and system resources 6–7
Networking and SSH Check connections, log in remotely, and transfer files 8
Services and automation Manage a service with systemd, write scripts, and back up and restore files 9–10
Troubleshooting Use logs, permissions, configuration, and port information to locate problems 11

Linux learning path from setup to service management and troubleshooting

The examples follow these conventions. Check where a command belongs before copying it:

Notation Where it belongs
powershell block Windows PowerShell; administrator access is called out when required
bash block An Ubuntu terminal as a regular user; commands needing elevated privileges include sudo
text, ini, or python block Example output or file contents; do not execute the entire block as Shell commands
“Local machine” and “server” The machine running the SSH client and the target reached through SSH; keep the roles distinct even when they are the same computer

Commands omit the $ prompt so they are easy to copy. Lines beginning with # are comments. Replace placeholders such as YOUR_SERVER_IP with real values. Do not paste angle-bracket placeholders directly into the Shell, because < and > have special meanings.

File exercises use ~/linux-lab, and backups go in ~/linux-lab-backups. The service management section explicitly creates /opt/linux-lab and its systemd configuration; run those steps on your own practice machine.

2. Setting Up a Usable Linux Environment

Start by separating three concepts. The Linux kernel handles processes, memory, devices, and other low-level operations. A distribution such as Ubuntu combines the kernel with software packages and system tools. Bash is a Shell that interprets the commands you type. A terminal is the window that presents input and output. Opening a dark command window does not necessarily mean you are running Linux.

On Windows, open PowerShell as administrator, list the available distributions, and install the chosen version:

1
2
wsl --list --online
wsl --install -d Ubuntu-24.04

Use the distribution name shown in the list. Restart if prompted, open Ubuntu, and create a Linux username and password. Password entry usually displays no characters or asterisks; that is normal. This password is separate from your Windows sign-in password.

If Ubuntu is already installed, use that environment. Inspect the installations and enter the appropriate one:

1
2
wsl --list --verbose
wsl -d Ubuntu-24.04

An existing distribution might simply be named Ubuntu; in that case, use wsl -d Ubuntu. Its VERSION column should show 2. Only if you need to convert an older installation, use wsl --set-version Ubuntu 2, substituting the actual distribution name. See the Microsoft WSL installation guide for requirements and installation troubleshooting.

On macOS, you can set up an Ubuntu virtual machine; choose an ARM64 image on Apple Silicon. If you already have a Linux computer or Ubuntu VM, continue there. The system tools available through the macOS terminal differ from GNU/Linux tools, so the native macOS environment is not a substitute for the Linux environment used here.

Once inside Ubuntu, run:

1
2
3
4
5
whoami
pwd
cat /etc/os-release
uname -r
ps -p 1 -o comm=

These commands identify your current user, working directory, distribution, kernel version, and process 1. The service sections require the last command to report systemd.

If systemd is not enabled in WSL, check the WSL version with wsl --version in PowerShell and run wsl --update if needed. Then run sudo nano /etc/wsl.conf in Ubuntu. Preserve any existing configuration and set the following option in the existing [boot] section, or add that section if it is absent:

1
2
[boot]
systemd=true

Save your work, then run wsl --shutdown in PowerShell. This stops all running WSL distributions. Reopen Ubuntu and check PID 1 again. systemd support requires WSL 0.67.6 or later; see the Microsoft systemd documentation. A practice service inside WSL is not an always-online server when WSL has stopped or Windows is shut down or asleep.

Install the tools used in the examples:

1
2
3
4
5
6
sudo apt update
sudo apt install -y man-db nano vim-tiny curl wget git unzip zip \
  tree file less procps iproute2 iputils-ping dnsutils lsof \
  openssh-client rsync tmux python3 python3-venv shellcheck
mkdir -p ~/linux-lab/{notes,data,logs,app,scripts}
cd ~/linux-lab

apt update refreshes the package index; it does not upgrade every installed package. apt install installs packages, and -y accepts the installation confirmation. A \ before a newline continues the command onto the next line; do not put spaces after it. These commands use the distribution’s package repositories, so you do not need to send an installation script from the web straight into sudo bash.

In WSL, /mnt/c and /mnt/d correspond to Windows drives. This tutorial uses the Linux filesystem under ~ to avoid the different permission behavior and performance characteristics of Windows-mounted paths. Run explorer.exe . inside Ubuntu to view the current directory in Windows File Explorer. With VS Code and its WSL extension installed, you can run code . from Ubuntu; check that the editor is connected to WSL. See Microsoft’s filesystem guidance.

Hands-on exercise: Close and reopen the terminal, enter Ubuntu, identify the current user and distribution, and return to ~/linux-lab without following the commands above.

3. Files, Paths, and Editors

Linux filenames are case-sensitive: App.py and app.py are different names. Paths use /. The path / is the filesystem root, ~ is the current user’s home directory, . is the current directory, and .. is its parent. /root is the root user’s home directory.

Directory Usual purpose Typical contents
/home/username Regular users’ files Source code, practice data, and personal configuration
/etc System configuration SSH, service, and other software configuration
/var/log Logs System and application activity records
/tmp Temporary files Temporary experiments; do not rely on long-term retention
/usr/bin Executables installed by the system Often the location of commands such as python3 and git
/opt Additional deployed software The service files used later in this tutorial
/proc Process and system information exposed by the kernel A virtual interface, not an ordinary directory of disk files

Work through a small sequence of file operations:

1
2
3
4
5
6
7
8
9
cd ~/linux-lab
pwd
ls -lah
printf 'first Linux note\n' > notes/day1.txt
cp -i notes/day1.txt notes/day1.copy.txt
mv -i notes/day1.copy.txt notes/archive.txt
mkdir -p 'data/raw files'
cp -i notes/day1.txt 'data/raw files/sample.txt'
tree -a -L 3 .

-l shows details, -a includes hidden files whose names begin with a dot, and -h displays sizes in readable units. cp copies files; mv moves or renames them. The -i option asks before overwriting an existing file. Quote paths containing spaces.

Next, try these navigation commands:

1
2
3
4
5
6
cd notes
pwd
cd ..
cd /etc
cd -
cd ~

cd - returns to the previous working directory, and cd ~ returns home. Relative paths are interpreted from the current directory; absolute paths start at /. If a command says a file does not exist, checking pwd is often more useful than repeatedly changing the command.

Reading and editing. Use cat for small files and less for larger ones. Inside less, type /keyword to search, n for the next match, and q to quit. Use tail -n 50 filename to see the last 50 lines, or tail -f filename to watch new log entries; press Ctrl+C to stop watching. If log rotation renames the file, tail -F can continue following its name.

1
nano ~/linux-lab/notes/day1.txt

In Nano, type directly, press Ctrl+O and Enter to save, and press Ctrl+X to exit. Also learn enough Vim to handle an unfamiliar machine: vim filename opens a file, i enters Insert mode, Esc returns to Normal mode, :wq saves and exits, :q! discards changes and exits, and /keyword searches. If typing does not behave as expected, first check the editor’s current mode.

Finding files and searching their contents are different operations.

1
2
3
find ~/linux-lab -type f -name '*.txt'
grep -RIn --include='*.txt' 'Linux' ~/linux-lab/notes
file ~/linux-lab/notes/day1.txt

find searches by file attributes. grep searches text: -R is recursive, -I skips binary files, and -n displays line numbers. Quoting '*.txt' lets find interpret the pattern instead of having the current Shell expand it first.

A symbolic link stores a path pointing to another location:

1
2
3
4
ln -s notes/day1.txt ~/linux-lab/latest-note
ls -l ~/linux-lab/latest-note
readlink ~/linux-lab/latest-note
cat ~/linux-lab/latest-note

The relative target notes/day1.txt is interpreted from the link’s containing directory, ~/linux-lab. If the target is moved or deleted, the link may stop working. Deleting the link itself does not automatically delete its target. When a program path is a link, readlink -f path resolves it to the actual target.

Check the target before deleting. The following commands delete only the copy created earlier:

1
2
3
4
ls -l ~/linux-lab/notes/archive.txt
rm -i ~/linux-lab/notes/archive.txt
mkdir ~/linux-lab/empty-demo
rmdir ~/linux-lab/empty-demo

rm generally does not move files to a recycle bin. rmdir removes only empty directories. Check pwd and the target path before recursive operations; knowing about rm -rf does not make it the right default for everyday deletion. -- ends option parsing: for example, rm -i -- ./-demo.txt correctly handles a filename beginning with -.

Look up options when you need them.

1
2
3
4
ls --help
man chmod
type cd
command -v python3

Inside man, search with /keyword and quit with q. cd is a Shell builtin, whereas python3 is usually an external program. You do not need to memorize every command, but you should know how to look up its options. Use Tab completion, the arrow keys for command history, and Ctrl+R to search history. Ctrl+L clears the screen. If you accidentally press Ctrl+S and the display stops updating, Ctrl+Q can resume it.

Hands-on exercise: Create a directory with a space in its name, edit a file inside it, find the file by searching its contents, copy it to another directory, and delete only that copy.

4. Users and Permissions: Understanding Permission Denied

Run id to see your UID, primary group, and supplementary groups, then inspect a file:

1
ls -l ~/linux-lab/notes/day1.txt

The output may look like this:

1
-rw-r--r-- 1 alice alice 17 Sep 14 10:00 day1.txt

The first character is the file type: - means a regular file, d a directory, and l a symbolic link. The next three groups of characters describe permissions for the owner, group, and everyone else. The two occurrences of alice are the owner and group names, respectively.

Permission For a regular file For a directory
r, value 4 Read its contents List the names inside it
w, value 2 Modify its contents Add, remove, or rename entries, usually together with x
x, value 1 Allow direct execution Traverse the directory to access paths inside it

Linux path and permission model: path components, rwx groups, and common repair commands

Thus, 644 gives the owner read and write access and everyone else read access. 755 gives the owner read, write, and execute access and everyone else read and execute access. 600 gives only the owner read and write access. Each digit adds up the permissions within one group; it is not an arbitrary code to memorize.

1
2
3
chmod 600 ~/linux-lab/notes/day1.txt
ls -l ~/linux-lab/notes/day1.txt
chmod u=rw,go=r ~/linux-lab/notes/day1.txt

The x permission on directories is especially important. Even if a file has mode 644, someone who cannot traverse its parent directory still cannot read it. Deleting a file mainly depends on the parent directory’s permissions and can also be restricted by the sticky bit, ACLs, and other controls. Looking only for w on the file is not enough.

sudo runs a command as another identity, commonly root, according to its configuration. Installing system software requires elevated privileges; editing your own code usually does not. Running an editor or build tool with sudo can leave root-owned files in your project.

Practice correcting a file with the wrong owner:

1
2
3
4
sudo touch ~/linux-lab/notes/root-owned.txt
ls -l ~/linux-lab/notes/root-owned.txt
sudo chown "$(id -un):$(id -gn)" ~/linux-lab/notes/root-owned.txt
printf 'ownership fixed\n' >> ~/linux-lab/notes/root-owned.txt

$(...) inserts a command’s output into an argument; here it supplies the current username and primary group. Change only the named file. Do not recursively make yourself the owner of all of /usr, /etc, or a shared project.

Another common trap is sudo echo content > /etc/some-file. The current Shell handles >, so elevating echo does not grant the outer Shell permission to write the file. For system configuration, use sudoedit filename, or explicitly use printf 'content\n' | sudo tee filename. The latter overwrites by default, so check the target first.

Troubleshooting order: Use id to check your identity → ls -ld on the target and its parent directories to inspect permissions → namei -l full-path to examine each component → confirm whether you should be modifying the file. Do not jump straight to chmod -R 777; it gives more users write and execute access and can conceal the real ownership problem.

Hands-on exercise: Explain -rwxr-x--- without looking it up, describe one reason a readable file might still be inaccessible, and fix the root-owned file created above.

5. Shell, Pipelines, and Logs: Combining Commands

The Shell processes variables, quotes, wildcards, pipelines, and redirections before a program runs. Many apparently strange command failures happen because the arguments have already changed before the program receives them.

1
2
3
4
lab_dir="$HOME/linux-lab"
printf '%s\n' "$lab_dir"
printf '%s\n' '$lab_dir'
ls "$lab_dir/notes"

The first printf prints the path; the second prints the literal text $lab_dir. Variable assignments have no spaces around =. Double quotes allow variable expansion while keeping spaces in a path from splitting it into separate arguments. Single quotes preserve literal text. The ~ in "~/linux-lab" does not expand; use "$HOME/linux-lab" instead. See the Bash quoting documentation.

Input, output, and errors. An ordinary command has standard input 0, standard output 1, and standard error 2:

Syntax Effect
command > out.log Write standard output to a file, replacing its previous contents
command >> out.log Append standard output to a file
command 2> err.log Write standard error to a separate file
command > all.log 2>&1 Redirect standard output first, then make standard error follow it
a | b Pass a’s standard output to b’s standard input
a | tee out.log Display the output and write it to a file

> all.log 2>&1 and 2>&1 > all.log are different: redirections are processed from left to right. An ordinary | does not automatically pass standard error along. Do not use the same file as both input and redirected output in grep ... file > file; the Shell truncates the file first. See the Bash redirection documentation.

Create a log with known results. The cat <<'EOF' construct below is a here-document: it writes the content between the delimiters into a file. The closing EOF must be on a line of its own. Quoting the opening delimiter prevents variable expansion within the content.

1
2
3
4
5
6
7
8
cat > ~/linux-lab/logs/access.log <<'EOF'
2026-09-14 10:00:01 level=INFO method=GET path=/health status=200 duration_ms=2
2026-09-14 10:00:02 level=INFO method=GET path=/api/orders status=500 duration_ms=120
2026-09-14 10:00:03 level=INFO method=GET path=/api/users status=500 duration_ms=80
2026-09-14 10:00:04 level=INFO method=GET path=/missing status=404 duration_ms=3
2026-09-14 10:00:05 level=INFO method=GET path=/api/orders status=500 duration_ms=150
2026-09-14 10:00:06 level=INFO method=GET path=/ status=200 duration_ms=4
EOF

Inspect the data one step at a time instead of memorizing a long pipeline:

1
2
3
4
5
6
cd ~/linux-lab
wc -l logs/access.log
grep -n 'status=500' logs/access.log
grep -c 'status=500' logs/access.log
awk '$6 == "status=500" {print $5}' logs/access.log
awk '$6 == "status=500" {print $5}' logs/access.log | sort | uniq -c | sort -nr

You should find 6 lines and 3 responses with status 500. The final command produces:

1
2
      2 path=/api/orders
      1 path=/api/users

By default, awk separates fields on whitespace: $5 is the fifth field and $6 the sixth. The first sort places identical paths next to one another. uniq -c counts only adjacent identical lines. The final sort -nr sorts numerically in descending order. This pipeline depends on the log format used here; it cannot be applied unchanged to arbitrary JSON or fields containing spaces.

Next, practice viewing context and editing text:

1
2
3
grep -n -C 1 'path=/api/users' logs/access.log
sed -n '2,4p' logs/access.log
sed 's/level=INFO/level=DEMO/g' logs/access.log > logs/access.demo.log

grep -C 1 shows context around each matching line, and sed -n '2,4p' displays lines 2 through 4. The substitution writes to a new file so you can compare the results. With real service logs, look at timestamps, request IDs, error types, and surrounding context instead of simply counting the word ERROR.

Exit status matters for reliable automation.

1
2
3
grep -q 'status=503' logs/access.log
echo $?
mkdir -p notes/check && printf 'directory ready\n'

$? is the previous command’s exit status, so read it immediately. Usually, 0 means success and a nonzero value indicates another outcome. For grep, 1 means no match and 2 means an error; they are not interchangeable. a && b runs b if a succeeds, and a || b runs b if a fails.

A pipeline normally returns the exit status of its last command. In a script, set -o pipefail makes failures in earlier stages easier to detect, but it does not make the entire script’s logic automatically correct. See the Bash pipeline documentation.

Hands-on exercise: Count the 404 responses in the sample log. Save all 500-response lines to a new file while also displaying them in the terminal. Explain why sorting is needed before uniq -c.

6. Development Environments: Running the Right Program

When setting up a project on a new machine, start with its README and version requirements, then inspect the system and tools. A missing program is not a reason to copy unrelated installation commands at random.

1
2
3
4
5
6
command -v git python3
git --version
python3 --version
apt show python3-venv
dpkg -l python3-venv
printf '%s\n' "$PATH"

PATH is the colon-separated list of directories the Shell searches for commands. Typing python3 searches those directories; ./script.sh explicitly selects a script in the current directory. The current directory is usually not in PATH, so seeing a file in the directory does not mean you can run it by typing its name alone.

Ubuntu and Debian commonly use apt, while Fedora and RHEL-family systems commonly use dnf. The system package manager handles system-level dependencies; tools such as pip and npm handle dependencies for their respective languages. Understand that distinction before looking up commands for your distribution and project.

Practice dependency isolation with a Python virtual environment. The application in this tutorial has no third-party dependencies, but creating an environment makes the startup workflow resemble a real project:

1
2
3
4
5
6
7
cd ~/linux-lab/app
python3 -m venv .venv
source .venv/bin/activate
command -v python
python -m pip --version
python -c 'import sys; print(sys.executable)'
deactivate

After activation, the interpreter path should be inside ~/linux-lab/app/.venv/bin/. If a real project provides requirements.txt, run python -m pip install -r requirements.txt after activating its environment. This example does not require that step. Do not modify the system Python with sudo pip install or bypass system-environment protection by casually adding --break-system-packages.

source filename executes a file in the current Shell and can therefore change the current environment. bash filename starts a child Shell whose variables do not automatically propagate back to its parent. Only source files you wrote or have inspected and trust.

Environment variables also have a scope:

1
2
3
4
5
export APP_MESSAGE='Hello from my shell'
python3 -c 'import os; print(os.getenv("APP_MESSAGE"))'
APP_MESSAGE='Only this command' python3 -c 'import os; print(os.getenv("APP_MESSAGE"))'
printf '%s\n' "$APP_MESSAGE"
unset APP_MESSAGE

export makes a variable available to child processes started afterward. An assignment immediately before a command affects only that invocation. A new terminal does not automatically inherit another terminal’s temporary settings, and an already-running service does not change with them. .bashrc usually contains personal settings for interactive Bash sessions. systemd services do not obtain their configuration by reading your .bashrc.

Basic Git operations on Linux. Make a local commit in a separate practice directory without pushing anything:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mkdir -p ~/linux-lab/git-demo
cd ~/linux-lab/git-demo
git init
git config user.name 'Linux Learner'
git config user.email 'learner@example.com'
printf '# Linux practice\n' > README.md
printf '.venv/\n.env\n__pycache__/\n*.log\n' > .gitignore
git status
git add README.md .gitignore
git diff --cached
git commit -m 'docs: add Linux practice notes'
git log --oneline -3

The Git identity settings here apply only to this practice repository, not to other projects. With a team’s actual repository, the usual starting point is to run git clone repository-url, enter the directory, read the README, switch to a working branch, install dependencies, and run tests. Inspect git status and git diff before making changes. Do not clear unfamiliar modifications with git reset --hard. Private keys, tokens, and real .env files should not be committed.

Hands-on exercise: Demonstrate that a program is using the virtual environment’s Python, explain the difference between export and a one-command environment assignment, and make a local commit after inspecting its diff.

7. Processes and Resources: What Is Running and Why It Is Slow

A process is a running program with a PID, user, state, working directory, and environment. A program can create multiple processes, and a process can contain multiple threads.

Use sleep to practice background jobs without modifying any data:

1
2
3
4
5
sleep 300 &
sleep_pid=$!
jobs -l
ps -p "$sleep_pid" -o pid,ppid,user,stat,etime,args
kill -TERM "$sleep_pid"

& runs the command in the background. $! is the PID of the most recently started background job. jobs lists only jobs managed by the current Shell, whereas ps inspects system processes. Prefer the program’s normal shutdown mechanism or TERM when stopping it. kill -9 sends KILL, which the process cannot catch to perform cleanup; use it only after confirming the target and finding that normal shutdown does not work.

Start sleep 300 again, press Ctrl+Z to suspend it, and run jobs -l to inspect its stopped state. Use bg to resume it in the background, fg to bring it to the foreground, and finally Ctrl+C to end it. Suspending a process is not the same as exiting; the process still exists.

Need Suitable tools Limitations to understand
Run temporary work concurrently in the current terminal &, jobs, fg, bg A background job is not automatically a reliable persistent service
Resume interactive work after SSH disconnects tmux You can reattach to the session, but it does not survive a machine shutdown
Run a temporary task that ignores hangup signals nohup command > run.log 2>&1 & Does not provide automatic restarts, health checks, or startup at boot
Run a long-lived application systemd Manages the lifecycle, logging, and restart policy

To get started with tmux, run tmux new -s linux-lab and work inside it. Press Ctrl+B, release the keys, and then press D to detach. List sessions with tmux ls and reattach with tmux attach -t linux-lab. To finish, stop your tasks inside the session and type exit.

Inspect the system as a whole instead of focusing on one number.

1
2
3
uptime
nproc
top

In top, press P to sort by CPU usage, M to sort by memory usage, and q to quit. The load averages reported by uptime cover 1, 5, and 15 minutes. They include runnable tasks and tasks in uninterruptible sleep; they are not CPU percentages. Interpret them alongside CPU count and I/O activity.

1
2
3
4
5
free -h
df -h
df -i
du -h --max-depth=1 ~/linux-lab
ps aux --sort=-%mem | head -n 8
Symptom First checks Interpretation
Very little memory appears to be “free” The available column in free -h Linux uses memory for caching, so free alone is insufficient
Disk writes fail df -h, df -i Storage space or inodes may be exhausted
A project directory becomes unexpectedly large Use du to narrow the search one directory level at a time Identify the specific logs, caches, or data before taking action
A program is suddenly killed journalctl -k -b, with sudo if needed Look for OOM or other evidence; the word Killed alone does not prove an OOM event
Deleting a log does not free disk space sudo lsof +L1 A process may still have the file open; after confirming, release it through normal service log rotation or a restart

df reports filesystem-level usage, while du measures the files under a path. Their numbers do not have to match. lsof lists files opened by processes; on Linux it can also inspect objects such as network sockets.

Hands-on exercise: Find and stop a process you started, explain the differences between background execution, suspension, and exit, and locate a directory that uses disk space without deleting arbitrary system files.

8. Networking and SSH: From Local Services to Remote Machines

Start with the addresses. 127.0.0.1 is the IPv4 loopback address of the current network environment itself. As a listening address, 0.0.0.0 means all IPv4 interfaces; it is not the server address to enter in a browser. Ports distinguish services on the same machine. WSL, containers, and the host have their own network boundaries, so do not assume localhost means the same thing on both sides.

Linux networking and SSH mental model: client, server, listening port, keys, and port forwarding

Begin with a small network experiment you can immediately troubleshoot. In terminal A, run:

1
2
3
cd ~/linux-lab/data
printf 'hello over HTTP\n' > index.html
python3 -m http.server 8000 --bind 127.0.0.1

This serves static files from the current data directory. Leave terminal A running and use a second Ubuntu terminal, B, for these checks:

1
2
3
ss -lntp 'sport = :8000'
curl -i --connect-timeout 2 --max-time 5 http://127.0.0.1:8000/
curl -i --max-time 5 http://127.0.0.1:8000/not-exist

The first command should show LISTEN and 127.0.0.1:8000. You can usually see process information when running as the same user; use sudo ss if necessary. The second command returns 200 and hello over HTTP; the third returns 404. A 404 means you reached an HTTP service, and the problem is at the route or resource level.

Return to A and stop the server with Ctrl+C. Request the same address from B again; it will usually report a refused connection. This is a different layer of failure from the earlier 404. Leave port 8000 free after this experiment, since the application in the next section will use it.

Symptom Next step What you cannot conclude from the symptom alone
Could not resolve host getent hosts domain, dig domain Does not establish that the application is down
Connection refused Check the target address, port, and ss -lntp Usually means no listener or an active rejection, not an HTTP 500
Connection timeout Check routes, firewalls, security groups, and target reachability Does not identify the failing layer by itself
TLS certificate verification fails Check the hostname, system time, and certificate chain curl -k should not become a permanent fix
HTTP 401 / 403 Check authentication and access permissions Does not necessarily indicate a Linux file-permission error
HTTP 404 / 500 Check paths, request parameters, and application logs Changing DNS cannot resolve every application error

Useful commands for checking each layer include:

1
2
3
4
5
6
ip -br addr
ip route
getent hosts example.com
dig example.com
ping -c 3 example.com
curl -v --connect-timeout 3 --max-time 10 https://example.com/

ping uses ICMP, which the destination may block. A failed ping does not prove HTTP is unreachable, and a successful ping does not prove the application is healthy. getent uses system name resolution, which may include /etc/hosts. dig primarily examines DNS, so their answers can differ.

With curl, -i displays response headers, -v displays connection details, -L follows redirects, and -o filename saves a response. A normal curl request does not necessarily return a nonzero exit status for HTTP 404 or 500. In scripts, you can use curl --fail-with-body --silent --show-error --max-time 5 URL. --connect-timeout limits the connection-establishment phase, while --max-time limits the entire transfer. See the official curl manual.

If access works inside Linux but fails in a Windows browser, identify the direction of access and WSL networking mode before checking Windows firewall or proxy settings. NAT and mirrored networking behave differently; see the Microsoft WSL networking documentation. The checks in this tutorial use curl inside Ubuntu as their baseline.

You can practice SSH without renting a server. Install an SSH server on your own Ubuntu practice machine and connect to it from the same environment. The following installation normally enables an inbound SSH service, so use it only in your own practice environment. There is no need to reinstall SSH on a company server that already provides it:

1
2
3
4
5
sudo apt install -y openssh-server
sudo systemctl start ssh
sudo ss -lntp 'sport = :22'
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
ssh -o HostKeyAlgorithms=ssh-ed25519 "$(id -un)@127.0.0.1"

When the first connection displays a fingerprint, compare its SHA256 value with the preceding command’s output before accepting it and entering your Linux user’s password. For a real remote machine, obtain the host fingerprint independently from an administrator or a trusted console. The host fingerprint identifies the server, while a user’s key identifies the person logging in; these are separate roles.

After logging in, run whoami, hostname, and pwd, then type exit to return to the client. When connecting to the same machine, the username and hostname may be unchanged. Checking them is still a useful habit.

Next, create a separate practice key on the SSH client—the Ubuntu terminal after leaving the remote session. If a file with the same name already exists, reuse it or choose another name rather than overwriting an existing key:

1
2
3
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_linux_lab -C 'linux-lab'
ssh-copy-id -i ~/.ssh/id_ed25519_linux_lab.pub "$(id -un)@127.0.0.1"
ssh -i ~/.ssh/id_ed25519_linux_lab "$(id -un)@127.0.0.1"

You can protect the key with a passphrase. A later prompt for that passphrase is different from a server-password prompt. Keep the private key, id_ed25519_linux_lab, on the client; the .pub public key can be installed in the server’s ~/.ssh/authorized_keys. Recommended permissions are 700 for .ssh and 600 for private keys and authorized_keys; also check ownership and parent-directory permissions. The client records server host keys in known_hosts.

Some cloud servers disable password authentication by default. You may need to connect with an existing key or install a public key through the cloud console. Do not assume ssh-copy-id can always use a password for the first login. Connection, key, and forwarding options are documented in the OpenSSH manual.

Type exit again to return to the client, then transfer a file and verify it:

1
2
3
4
5
scp -i ~/.ssh/id_ed25519_linux_lab ~/linux-lab/notes/day1.txt \
  "$(id -un)@127.0.0.1:linux-lab/notes/received.txt"
sha256sum ~/linux-lab/notes/day1.txt
ssh -i ~/.ssh/id_ed25519_linux_lab "$(id -un)@127.0.0.1" \
  'sha256sum ~/linux-lab/notes/received.txt'

The hashes should match. The relative path after the colon starts from the remote user’s home directory, and its parent directory must already exist. For a real server, use ssh -p PORT USER@SERVER_ADDRESS; scp uses an uppercase -P for its port option.

For repeated connections, use nano ~/.ssh/config to add a Host alias. Replace YOUR_LINUX_USER below with the output of id -un. You can then connect with ssh linux-lab:

1
2
3
4
5
6
Host linux-lab
    HostName 127.0.0.1
    User YOUR_LINUX_USER
    Port 22
    IdentityFile ~/.ssh/id_ed25519_linux_lab
    IdentitiesOnly yes

Preview directory synchronization first, then remove n to perform it. The trailing / means to copy the directory’s contents:

1
2
rsync -avn ~/linux-lab/notes/ linux-lab:linux-lab/notes-sync/
rsync -av ~/linux-lab/notes/ linux-lab:linux-lab/notes-sync/

To access port 8000 on a server that listens only on loopback, open a tunnel from the client:

1
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18000:127.0.0.1:8000 linux-lab

The left-hand 127.0.0.1:18000 is the client’s listening address. The right-hand 127.0.0.1:8000 is reached from the SSH server’s perspective. Leave this terminal running and visit http://127.0.0.1:18000/health from another client terminal to reach the application deployed in the next section. The application is not running yet, so verify the tunnel after starting it there. Close the tunnel with Ctrl+C.

If SSH fails, use ssh -v linux-lab to inspect connection and authentication details. Review logs before sharing them publicly. Before applying a server configuration change, check its syntax with sudo sshd -t, keep the original connection open, and verify a new connection in another window. If a host key changes, first check whether the server was rebuilt or replaced rather than disabling host-key checks. See the sshd configuration-check and permission guidance.

Hands-on exercise: Explain the difference between HTTP 404 and a refused connection, log in with an SSH key, and verify a transferred file’s hash. If you have access to a real remote machine, repeat the operations across machines.

9. Putting It Together: Deploying and Managing an HTTP Service

This section combines the earlier skills. The application has just three behaviors: /health returns a health status, / returns a message from an environment variable, and other paths return 404. You do not need to study Python syntax here. Focus on where the program lives, which user runs it, how configuration reaches it, and where its logs appear.

Managing a Linux service with systemd: unit file, environment configuration, app process, port, and logs

First, get it working in the foreground. Run nano ~/linux-lab/app/app.py, paste the complete contents below, then save and exit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3
"""A small HTTP service for practicing Linux operations; no dependencies."""

import json
import os
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/health":
            status, payload = 200, {"status": "ok", "service": "linux-lab"}
        elif path == "/":
            status, payload = 200, {"message": os.getenv("APP_MESSAGE", "Hello Linux")}
        else:
            status, payload = 404, {"error": "not found"}

        body = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_request(self, code="-", size="-"):
        # repr escapes control characters in the request target.
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        print(
            f"{now} level=INFO method={self.command} "
            f"path={self.path!r} status={code}",
            flush=True,
        )


if __name__ == "__main__":
    host = os.getenv("APP_HOST", "127.0.0.1")
    port = int(os.getenv("APP_PORT", "8000"))
    with ThreadingHTTPServer((host, port), Handler) as server:
        print(f"Listening on http://{host}:{server.server_port}", flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            print("Stopping linux-lab", flush=True)

In terminal A, run:

1
2
cd ~/linux-lab/app
APP_MESSAGE='My first Linux service' .venv/bin/python app.py

Verify it in terminal B:

1
2
3
4
curl -i --max-time 5 http://127.0.0.1:8000/health
curl -i --max-time 5 http://127.0.0.1:8000/
curl -i --max-time 5 http://127.0.0.1:8000/missing
ss -lntp 'sport = :8000'

The health endpoint should return {"status": "ok", "service": "linux-lab"}. The root path returns the message My first Linux service, and an unknown path returns 404. Watch terminal A: each request produces a log entry containing path= and status=, with timestamps in UTC. These live logs are separate from the fixed sample log in section 5; do not expect their counts to match.

If you see Address already in use, first check whether the static server from section 8 is still running. Once foreground verification is complete, press Ctrl+C in A to stop the application and release the port.

This local application uses Python’s standard-library http.server to teach operational tasks. It is not a production web framework, and the official documentation advises against using that module directly in production. For a real project, follow the framework’s requirements for the application server, traffic entry points, and access controls. See the Python http.server documentation.

Next, make the runtime environment explicit. Run the following on your own Ubuntu practice machine. If the user, /opt/linux-lab directory, or service already exists, confirm that it belongs to this exercise before reusing it.

1
2
3
4
5
id -u linux-lab >/dev/null 2>&1 || sudo useradd --system --user-group \
  --home-dir /opt/linux-lab --no-create-home --shell /usr/sbin/nologin linux-lab
sudo install -d -o root -g root -m 755 /opt/linux-lab
sudo install -o root -g root -m 644 ~/linux-lab/app/app.py /opt/linux-lab/app.py
sudo /usr/bin/python3 -m venv /opt/linux-lab/.venv

These commands create a dedicated system user without interactive login. Root owns the application code; the service user needs only read and execute access. Create a fresh virtual environment for deployment instead of moving the one from your home directory, because virtual environments can contain absolute paths.

Write the configuration:

1
2
3
4
5
6
sudo tee /etc/linux-lab.env > /dev/null <<'EOF'
APP_HOST=127.0.0.1
APP_PORT=8000
APP_MESSAGE="Managed by systemd"
EOF
sudo chmod 600 /etc/linux-lab.env

Then create the service unit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
sudo tee /etc/systemd/system/linux-lab.service > /dev/null <<'EOF'
[Unit]
Description=Linux practice HTTP service
After=network.target

[Service]
Type=simple
User=linux-lab
Group=linux-lab
WorkingDirectory=/opt/linux-lab
EnvironmentFile=/etc/linux-lab.env
ExecStart=/opt/linux-lab/.venv/bin/python -u /opt/linux-lab/app.py
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
Setting Purpose in this example
User, Group Explicitly run as a regular service user
WorkingDirectory Set the working directory independently of where you start the service
EnvironmentFile Supply configuration explicitly; the system service manager reads it
ExecStart Use absolute paths for the interpreter and application
-u Disable Python output buffering so logs appear promptly
Restart=on-failure Attempt a restart after an abnormal failure; a manual stop does not trigger it
RestartSec=3 Wait 3 seconds between restart attempts rather than looping tightly
WantedBy Establish the startup relationship with a target when the unit is enabled

EnvironmentFile is not a general-purpose Shell script: do not put export, command substitution, or arbitrary script logic in it. ExecStart also does not automatically interpret >, |, and ~ as Bash would. After=network.target specifies ordering; it does not guarantee that the internet or a database is already reachable. See the systemd service documentation source and execution-environment documentation source.

Check the configuration, start the service, and verify the responses:

1
2
3
4
5
6
7
sudo systemd-analyze verify /etc/systemd/system/linux-lab.service
sudo systemctl daemon-reload
sudo systemctl enable --now linux-lab
systemctl status linux-lab --no-pager -l
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/health
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/
sudo journalctl -u linux-lab -n 30 --no-pager

Expect active (running), an ok response from the health endpoint, and Managed by systemd at the root path. There can be a brief interval between process startup and the port becoming ready. If an immediate request fails, inspect the logs and check again. An active systemd state describes the process state; you still need an HTTP request to verify application readiness.

Keep these operations distinct:

Operation Effect
sudo systemctl start linux-lab Start the service now
sudo systemctl stop linux-lab Stop it now
sudo systemctl restart linux-lab Stop it and start it again
sudo systemctl enable linux-lab Configure future startup at the appropriate system stage; does not start it immediately
sudo systemctl disable linux-lab Remove future automatic startup; does not stop it immediately
sudo systemctl daemon-reload Reread unit configuration after changes; does not restart the application
sudo journalctl -u linux-lab -f Follow service logs; press Ctrl+C to stop watching

After changing a .service file, normally run daemon-reload and then restart the service. If you change only the referenced env file, restarting the application is enough to reread it. Not every service supports systemctl reload; this example does not configure that operation.

Edit APP_MESSAGE with sudo nano /etc/linux-lab.env, save, run sudo systemctl restart linux-lab, and request the root path to confirm the change. Try setting only export APP_MESSAGE=... in an interactive terminal and observe that it does not alter the running service.

Inspect the service identity and restart count:

1
2
3
systemctl show linux-lab -p MainPID -p User -p NRestarts
sudo ss -lntp 'sport = :8000'
sudo journalctl -u linux-lab --since '10 minutes ago' --no-pager

To verify automatic recovery after a failure, first confirm that the target is this practice service, note its MainPID, and run the following command once. This deliberately simulates a forcibly killed process. Continue using systemctl stop for normal shutdown:

1
sudo systemctl kill --kill-whom=main --signal=SIGKILL linux-lab

After a few seconds, repeat systemctl show and the health check. You should see a new PID, an increased restart count, and a working endpoint. In contrast, systemctl stop should leave the service stopped until you explicitly start it.

Finally, verify startup recovery. In a regular VM, save your work and reboot. In WSL, save all WSL work, run wsl --terminate Ubuntu-24.04 in PowerShell using the actual distribution name, and enter the distribution again. Check systemctl is-enabled linux-lab, systemctl is-active linux-lab, and the health endpoint. This verifies service recovery after the Linux environment starts again.

When you are finished practicing, sudo systemctl disable --now linux-lab stops the service and disables automatic startup. You can keep the configuration, code, and notes for later review.

Hands-on exercise: Run the service as linux-lab without keeping a terminal open to sustain it. Change its configuration, inspect logs, restart it, access it through an SSH tunnel, and verify recovery after the environment restarts.

10. Shell Automation: A Backup Must Be Restorable

A useful script needs clear inputs, meaningful failure reporting, correct handling of filenames with spaces, and predictable behavior when run repeatedly. A backup script provides a practical way to work through those requirements.

Backup and restore loop: validate inputs, create an archive, restore into a temporary directory, compare results, and repeat verification

Start with two common combinations. A loop repeats an operation for a group of files:

1
2
3
4
5
for note in "$HOME/linux-lab/notes/"*.txt; do
    [[ -f "$note" ]] || continue
    printf '%s: ' "$note"
    wc -l < "$note"
done

This prints each note’s line count, with note holding one path on each iteration. The variable portion is quoted, while the *.txt outside the quotes can still expand. The file check skips the unexpanded pattern when no files match. Do not use for file in $(ls) to process filenames: spaces and newlines can split them incorrectly.

Functions and conditional branches turn repeated checks into a reusable operation. Make sure the service from the previous section is running, then execute:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
check_health() {
    curl --fail-with-body --silent --show-error --max-time 5 \
        http://127.0.0.1:8000/health
}

if check_health; then
    printf 'HTTP health check passed\n'
else
    printf 'HTTP health check failed; inspect the service logs\n' >&2
fi

Here, if uses the exit status of the function’s last command. This check establishes that the HTTP request succeeded; a stricter application check should also parse the response’s status field. Add a Bash shebang if you save this as a script. To report failure to a caller, explicitly use exit 1 in the failure branch: a successful final printf does not mean the service is healthy.

Next, learn the basic archive operations. Make sure data contains the files from the earlier exercises:

1
2
3
4
5
6
mkdir -p ~/linux-lab-backups
tar -czf ~/linux-lab-backups/data-demo.tar.gz -C ~/linux-lab data
tar -tzf ~/linux-lab-backups/data-demo.tar.gz
restore_dir=$(mktemp -d "$HOME/linux-lab-restore.XXXXXX")
tar -xzf ~/linux-lab-backups/data-demo.tar.gz -C "$restore_dir"
diff -r ~/linux-lab/data "$restore_dir/data"

With tar, c creates an archive, x extracts it, t lists its contents, z uses gzip, f is followed by the archive filename, and -C sets the working directory. Repeating this introductory example overwrites data-demo.tar.gz; the script below will use a unique name for each backup instead. No output from diff -r, together with exit status 0, means the contents match. Each restore goes into a new temporary directory without overwriting the original data.

Extract only trusted archives this way, and inspect the listing first when an archive is unfamiliar. Copying a directory or using tar cannot guarantee a consistent backup of a database that is actively being written. Use the database’s own backup tools for that; this example handles static practice files.

Run nano ~/linux-lab/scripts/backup.sh and enter the complete script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 2 ]]; then
    printf 'Usage: bash %s SOURCE_DIR BACKUP_DIR\n' "$0" >&2
    exit 64
fi

if [[ ! -d "$1" ]]; then
    printf 'Source directory does not exist: %s\n' "$1" >&2
    exit 1
fi

# realpath resolves symlinks; -m also accepts a destination not yet created.
source_dir=$(realpath -e -- "$1")
backup_dir=$(realpath -m -- "$2")
if [[ "$source_dir" == / || "$backup_dir" == "$source_dir" ||
      "$backup_dir" == "$source_dir/"* ]]; then
    printf 'Backup directory must be outside the source directory.\n' >&2
    exit 1
fi

mkdir -p -- "$backup_dir"
# mktemp creates a unique file, even for concurrent runs in the same second.
archive=$(mktemp "$backup_dir/backup-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX.tar.gz")
# A failed tar command must not leave an incomplete archive behind.
trap 'rm -f -- "$archive"' EXIT
tar -czf "$archive" -C "$source_dir" .
trap - EXIT

printf 'Backup created: %s\n' "$archive"

Understand the important lines before running it:

Syntax Purpose
#!/usr/bin/env bash Select Bash as the interpreter when executing the script directly
$#, $1, $2 The argument count, first argument, and second argument
[[ ... ]], if Bash conditions, such as directory existence and argument-count checks
>&2, exit 1 Write an error to standard error and return a nonzero status
set -u Expose references to unset variables
set -e, pipefail Stop early for many command failures; understand exceptions involving conditions and pipelines
realpath Resolve actual paths, including symbolic links, to avoid writing backups inside the source directory
mktemp Create a unique file so repeated runs do not overwrite one another
trap ... EXIT Remove this run’s incomplete archive if the script fails

The script accepts ordinary directory backups, rejects /, and requires the backup directory to be outside the source directory. Otherwise it could archive the backup it is currently creating. It is intended for practice directories you control, not complicated cases such as multiple users changing directory paths concurrently.

Run these commands in Ubuntu:

1
2
3
4
bash -n ~/linux-lab/scripts/backup.sh
shellcheck ~/linux-lab/scripts/backup.sh
chmod u+x ~/linux-lab/scripts/backup.sh
~/linux-lab/scripts/backup.sh "$HOME/linux-lab/data" "$HOME/linux-lab-backups"

bash -n checks syntax, and ShellCheck checks common Shell mistakes. bash script does not require an executable permission bit on the script; ./script requires execute permission and a valid interpreter. If editing on Windows produces an error such as /usr/bin/env: 'bash\r': No such file or directory, inspect line endings with file script, switch the editor to LF and UTF-8, and save again.

The script prints Backup created: full-path. Replace the first line below with the archive path it printed, then verify restoration:

1
2
3
4
5
6
archive_path='/replace/with/the/full/archive/path/printed/above'
tar -tzf "$archive_path"
restore_dir=$(mktemp -d "$HOME/linux-lab-restore.XXXXXX")
tar -xzf "$archive_path" -C "$restore_dir"
diff -r "$HOME/linux-lab/data" "$restore_dir"
echo $?

There is no outer data/ directory here because the script uses -C source-directory .. Compare against the restore directory itself. Expect no diff output and exit status 0. The existence of an archive alone does not prove the backup is usable.

Deliberately trigger failure cases as well:

1
2
3
4
bash ~/linux-lab/scripts/backup.sh
echo $?
bash ~/linux-lab/scripts/backup.sh "$HOME/linux-lab/data" "$HOME/linux-lab/data/backups"
echo $?

The first pair should display usage instructions and return 64. The second should reject a destination inside the source directory and return 1. Try a source directory whose name contains spaces to verify the value of quoting. Do not debug a script containing real passwords with bash -x; it prints expanded commands.

Scheduled execution is an optional extension. Install and start cron on your practice machine:

1
2
3
sudo apt install -y cron
sudo systemctl enable --now cron
crontab -e

Assuming your Linux username is alice, add the following file contents in the editor. Replace every /home/alice with the actual home directory shown by echo "$HOME". The log directory must already exist:

1
2
3
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
0 19 * * * /bin/bash /home/alice/linux-lab/scripts/backup.sh /home/alice/linux-lab/data /home/alice/linux-lab-backups >> /home/alice/linux-lab/logs/backup-cron.log 2>&1

The first five fields are minute, hour, day of month, month, and day of week. This example runs every day at 19:00 in the machine’s timezone. Check the time with date and timedatectl, and list saved jobs with crontab -l. Cron’s environment differs from an interactive terminal, so use absolute paths and an explicit PATH. Run the same command manually first, confirm it succeeds, then wait for a scheduled run and inspect the log and new archive.

Ordinary cron does not catch up on jobs missed while the machine was shut down. You also cannot rely on it to back up files while WSL is not running. After practicing, use crontab -e to remove the line you added while preserving other jobs. For more robust server-side scheduling, systemd timers are a useful next topic, but they are beyond the scope of this section.

Hands-on exercise: Generate a restorable backup on a successful run, return a nonzero status for invalid arguments, handle directories containing spaces, and keep two distinct archives after two consecutive runs.

11. Diagnosing and Recovering from Common Failures

Port conflicts, insufficient permissions, invalid configuration values, and misspelled paths are common service problems. Use the example service to reproduce these four cases and observe their symptoms and solutions. Reproduce one problem at a time, restore the service, and verify its health before continuing. Perform these operations only in your own practice environment.

Linux service troubleshooting flow from symptom, scope, and evidence to root cause, repair, and verification

Before starting, confirm that the service from section 9 is healthy and save its working configuration. If backup files already exist, decide whether to preserve them; do not overwrite your only working baseline:

1
2
3
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/health
sudo cp -i /etc/linux-lab.env /etc/linux-lab.env.before-drill
sudo cp -i /etc/systemd/system/linux-lab.service /etc/systemd/system/linux-lab.service.before-drill

Organize your investigation in this order:

1
2
3
4
5
6
Symptom: What did the user observe, and when did it start?
Scope: This machine, this service, this endpoint, or all requests?
Evidence: Which commands ran, and which output supports the diagnosis?
Root cause: Which configuration, file, process, or environment condition is wrong?
Repair: What changed, and why does that address the cause?
Verification: What do the state, port, HTTP response, and logs show now?

Failure A: Another program owns the port. Stop the practice service and start a temporary server in terminal A:

1
2
sudo systemctl stop linux-lab
python3 -m http.server 8000 --bind 127.0.0.1 --directory "$HOME/linux-lab/data"

Leave A running and execute sudo systemctl start linux-lab in B. Request /health; it may return 404 because the port is currently served by the static file server. Checking only that “port 8000 is open” would give you the wrong conclusion.

Task: Identify the process actually listening on the port and explain why the systemd service failed to start. Restore the correct service and verify the JSON returned by /health.

Failure B: The program exists, but the service user cannot read it. After stopping the temporary server in A and restoring the practice service, run:

1
2
sudo chmod 000 /opt/linux-lab/app.py
sudo systemctl restart linux-lab

Task: Use the logs and permissions to find the cause. Do not fix it by running the service as root, setting the whole directory to 777, or reinstalling Python.

Failure C: An environment variable contains an invalid value. After fixing B, run sudo nano /etc/linux-lab.env, change APP_PORT=8000 to APP_PORT=oops, save, and run sudo systemctl restart linux-lab.

Task: Confirm that Python fails while reading the port configuration. Explain why export APP_PORT=8000 in the current terminal does not fix the service. Correct the file the service actually reads and verify it again.

Failure D: The interpreter path is misspelled. After fixing C, run sudo nano /etc/systemd/system/linux-lab.service. In ExecStart, change only .venv/bin/python to .venv/bin/python-missing, then run:

1
2
sudo systemctl daemon-reload
sudo systemctl restart linux-lab

Task: Use the systemd state and logs to distinguish “Python started and reported an error” from “the interpreter could not be executed.” After repairing the path, explain why this change requires daemon-reload.

Reference: diagnostic evidence and recovery steps

Start with these read-only checks. Gather evidence before changing configuration:

1
2
3
4
5
6
systemctl status linux-lab --no-pager -l
sudo journalctl -u linux-lab -n 50 --no-pager
sudo ss -lntp 'sport = :8000'
ls -l /opt/linux-lab/app.py
namei -l /opt/linux-lab/.venv/bin/python
systemctl cat linux-lab
Failure Typical evidence Recovery
A Logs contain Address already in use; the PID in ss belongs to the temporary static server Press Ctrl+C in terminal A, where you started that server, then start linux-lab
B Python reports Permission denied when opening app.py; the file mode is ---------- Run sudo chmod 644 /opt/linux-lab/app.py and keep the regular service user
C Python logs contain ValueError, and the port value is oops Set the port in /etc/linux-lab.env back to 8000
D The state commonly shows 203/EXEC, and the interpreter path does not exist Restore .venv/bin/python, then run daemon-reload

In B, the interpreter starts successfully, so failure to read the application file usually appears as a Python exit status. In D, executing the interpreter itself fails. Exact log messages vary by version; use the actual output as your evidence.

If repeated failures trigger the service’s start-rate limit, fix the cause, run sudo systemctl reset-failed linux-lab, and then start it. Do not repeatedly reset the failed state without repairing the underlying problem.

After all four cases, you can restore the original configuration from the baseline you checked earlier:

1
2
3
4
5
6
sudo cp /etc/linux-lab.env.before-drill /etc/linux-lab.env
sudo cp /etc/systemd/system/linux-lab.service.before-drill /etc/systemd/system/linux-lab.service
sudo chmod 644 /opt/linux-lab/app.py
sudo systemctl daemon-reload
sudo systemctl reset-failed linux-lab
sudo systemctl restart linux-lab

Confirm that the temporary static server has stopped, then check the service state, listening process, /health contents, and latest logs. Verify each failure’s repair individually, not just once after all four cases.

For problems outside this tutorial, use the same sequence to narrow the search:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Which machine and user am I working as?
        ↓
Is the target process running? What do its state and latest logs show?
        ↓
Are the paths, permissions, working directory, and environment correct?
        ↓
Are the listening address and port correct? Which layer is curl failing at?
        ↓
If local requests work but remote ones fail, inspect network boundaries and access controls.
        ↓
Fix one cause → Repeat the same checks → Record the evidence

Hands-on exercise: Observe and compare the state and logs for all four failures, and check the endpoint response after every repair.

12. Command Reference and Further Reading

The following table brings together concepts that are easy to confuse:

Common question Key points
What should I check first when a service is unreachable? Confirm the machine and address, inspect the process and logs, then check the listener and the layer indicated by curl’s error
Why is the endpoint wrong even though the port is open? The wrong process may own the port; correlate its PID with the actual response contents
Why does a program work in my terminal but fail under systemd? Check differences in user, directory, interpreter path, permissions, and environment variables
Why is chmod 777 not a general fix? It can conceal ownership or parent-directory problems while broadening access
How do nohup and systemd differ? The former mainly handles hangups; the latter manages lifecycle, logging, and restarts
Does low free memory mean the machine is out of memory? Consider available memory, caches, processes, and OOM evidence
How do I prove a backup succeeded? Restore it to a new directory and compare the contents; an archive’s existence is insufficient
What makes a Shell script reliable? Argument validation, quoting, exit status, failure handling, and behavior across repeated runs

When you need to perform an operation, start with these commands:

Purpose Useful starting points
Identify who and where you are whoami, id, hostname, pwd
Find files and inspect contents find, grep -n, less, tail -F
Inspect permissions ls -ld, namei -l
Inspect processes and resources ps, top, free -h, df -h, df -i, du
Inspect connections ss -lntp, curl -v, getent hosts
Inspect services systemctl status, systemctl cat, journalctl -u
Log in and transfer files ssh, scp, rsync
Look up help command --help, man command

Once you are comfortable with these basics, follow your actual needs into Docker, reverse proxies, database administration, CI, or deeper system monitoring. Kernel compilation, driver development, and advanced kernel tracing are more specialized topics to explore when relevant problems arise.

Official references: Bash manual, GNU Coreutils manual, Ubuntu Server documentation, OpenSSH manual, and systemd documentation. If an option differs from this article, start with the practice machine’s local man pages, which usually match the installed version more closely.