Education › DevOps › Stage 1: Foundations

Linux & the command line

Filesystems, processes, permissions, systemd, and living comfortably in a shell.

Beginner ~30 min read Module 1 of 17

Almost everything you will deploy runs on Linux: containers, Kubernetes nodes, CI runners, and most cloud servers. When a deployment fails at 2 a.m. there is no GUI, only a shell. This module gives you the working model of a Linux system that every later module assumes: where things live, who is allowed to touch them, what is running, and how to find out why it broke.

After this module you can
  • Navigate the filesystem and know what belongs in /etc, /var, /usr, /home and /tmp
  • Read and change file permissions and ownership, in both symbolic and octal form
  • Inspect, signal, and background processes, and manage long-running services with systemctl
  • Combine small tools with pipes and redirection to answer real questions about a system
  • Find the cause of a failing service from its logs using journalctl

The filesystem is a single tree

Linux has no drive letters. Everything hangs off one root directory, /, and extra disks, network shares, and even kernel information are mounted somewhere inside that tree. The layout is a convention called the Filesystem Hierarchy Standard, and learning it means you can sit down at any server and know where to look.

PathWhat lives there
/etcSystem-wide configuration, as plain text. The first place to look when behaviour is wrong.
/varData that changes while the system runs: logs in /var/log, package caches, databases.
/usrInstalled programs and libraries (/usr/bin, /usr/lib). Read-only in normal operation.
/homeOne directory per human user. root's home is /root.
/tmpScratch space, usually wiped on reboot. Never store anything you need here.
/proc, /sysNot real files: a live view into the kernel and hardware.

Paths that start with / are absolute; anything else is relative to your current directory. Scripts and cron jobs should use absolute paths, because you cannot be sure which directory they start in.

bash
pwd                       # where am I?
ls -lah /var/log          # long listing, all files, human-readable sizes
cd /etc && ls | head      # move, then peek at the first ten entries
df -h                     # free space per mounted filesystem
du -sh /var/log/* | sort -h | tail -5   # the five biggest things in /var/log
Tip

A full disk is one of the most common causes of mysterious outages. df -h to find the full filesystem, then du -sh * | sort -h to walk down to the culprit, is a routine worth memorising.

Users, permissions, and ownership

Every file has an owning user, an owning group, and three sets of permission bits: for the user, the group, and everyone else. Each set can grant read, write, and execute. ls -l shows them as a ten-character string such as -rwxr-x---: a type flag, then three triplets.

The octal form is the same information as numbers: read is 4, write is 2, execute is 1, and you add them up per triplet. So rwx is 7, r-x is 5, r-- is 4, and -rwxr-x--- is 750. On a directory, execute means permission to enter it and read means permission to list it, which is why a directory with r-- alone is nearly useless.

bash
ls -l deploy.sh                 # -rw-r--r-- 1 alice dev 412 Mar  3 10:12 deploy.sh
chmod u+x deploy.sh             # symbolic: add execute for the owning user
chmod 640 secrets.env           # octal: owner rw, group r, others nothing
chown alice:dev secrets.env     # change owner and group (needs root)
chmod -R g+rX /srv/app          # capital X: execute only on directories
id                              # which user am I, and which groups am I in?

The root user bypasses all of these checks, which is exactly why you should not work as root. Use sudo to run one command with elevated rights: it is logged, it can be limited per user, and it forces a moment of thought.

Watch out

chmod 777 makes a permission error disappear by letting every user on the machine modify the file. It is almost never the right fix. Work out which user the process runs as (ps -o user= -p PID) and grant that user, or its group, the minimum it needs.

Processes and signals

A running program is a process with a numeric ID (PID), a parent, an owning user, and an exit code when it finishes. An exit code of 0 means success and anything else means failure. CI pipelines, shell && chains, and Kubernetes probes all depend on that one convention.

bash
ps aux | head                   # every process, with owner, CPU and memory
ps -ef --forest | less          # the same, drawn as a parent/child tree
pgrep -a nginx                  # PIDs and command lines matching a name
top                             # live view; press q to quit (htop is friendlier)
ss -tulpn                       # which process is listening on which port
echo $?                         # exit code of the last command

You do not kill a process, you send it a signal. SIGTERM (15) is a polite request to shut down, and a well-written program catches it, finishes in-flight work, and exits. SIGKILL (9) cannot be caught: the kernel removes the process immediately, with no chance to flush data or release locks. Always try SIGTERM first.

bash
kill 4242            # SIGTERM by default
kill -9 4242         # SIGKILL: last resort only
kill -HUP 4242       # many daemons reload their config on SIGHUP
sleep 600 &          # trailing & runs a job in the background
jobs                 # list background jobs of this shell
fg %1                # bring job 1 back to the foreground
Note

This matters directly for containers. When Kubernetes stops a pod it sends SIGTERM, waits for the grace period (30 seconds by default), then sends SIGKILL. An app that ignores SIGTERM drops requests on every deploy.

Services with systemd

On nearly every modern distribution, long-running services are supervised by systemd. It starts them at boot in dependency order, restarts them when they crash, and captures their output. You talk to it with systemctl, and each service is described by a unit file ending in .service.

bash
systemctl status nginx          # running? since when? last log lines
sudo systemctl restart nginx    # stop, then start
sudo systemctl reload nginx     # re-read config without dropping connections
sudo systemctl enable --now nginx   # start at boot AND start right now
systemctl list-units --type=service --state=failed
systemctl cat nginx             # show the unit file that is actually in effect

start and enable are different things. start affects this moment; enable affects the next boot. A service that was started but never enabled works perfectly until the server reboots, a classic cause of an outage after routine patching.

A minimal unit file: /etc/systemd/system/myapp.service
ini
[Unit]
Description=My web app
After=network-online.target

[Service]
User=myapp
WorkingDirectory=/srv/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/srv/myapp/bin/server --port 8080
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

After creating or editing a unit file, run sudo systemctl daemon-reload so systemd re-reads it. Note the User= line: the service runs as an unprivileged account, not as root.

Pipes, redirection, and the text toolkit

The Unix design idea is many small programs that each do one thing, connected by pipes. Every process has three standard streams: stdin (0), stdout (1), and stderr (2). The | operator connects one program's stdout to the next one's stdin; > and >> send a stream to a file.

<| stdout to stdin|> or >>errorsaccess.logstdin (0)awk'{print $1}'sortuniq -cTerminal or filestdout (1)Terminalstderr (2), unless 2>
Every process has three streams, and a pipeline connects one process's stdout to the next one's stdin, while stderr from each still reaches the terminal unless it is redirected.
bash
grep -ri "timeout" /etc/nginx/          # recursive, case-insensitive search
grep -c " 500 " access.log               # count matching lines
tail -f /var/log/syslog                  # follow a file as it grows

# top 5 client IPs in a web access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5

./backup.sh > backup.log 2>&1            # stdout AND stderr into one file
./backup.sh 2> /dev/null                 # discard errors only
find /var/log -name "*.gz" -mtime +30    # files older than 30 days

Read that log pipeline left to right: take column one, sort so identical lines sit together, collapse them with a count, sort numerically in reverse, keep five. Nearly every investigation you do on a server is some variation of filter, extract, count, sort.

Tip

Order matters in > file 2>&1. It means: point stdout at the file, then point stderr at wherever stdout currently goes. Written the other way round, 2>&1 > file, stderr still goes to the terminal.

Finding out why it broke

systemd collects the output of every service in the journal. journalctl queries it, and its filters are what turn a wall of text into an answer.

bash
journalctl -u nginx --since "10 min ago"   # one service, recent window
journalctl -u nginx -f                      # follow live
journalctl -p err -b                        # errors and worse, since this boot
journalctl -k | tail -50                    # kernel messages (OOM kills show here)
dmesg -T | grep -i "out of memory"          # did the kernel kill something?

A reliable order of attack for "the service is down":

  1. systemctl status NAME: is it running, failed, or restarting in a loop? Note the exit code.
  2. journalctl -u NAME --since "15 min ago": read the last lines before it stopped. The real error is usually the first one, not the last.
  3. Check resources: df -h for disk, free -h for memory, journalctl -k for OOM kills.
  4. Check the network: is it listening (ss -tulpn), and can you reach it locally (curl -v localhost:8080)?
  5. Only then change something, one thing at a time, and watch the log while you do it.
Hands-on practice

Run and break your own service

  1. Get a Linux shell you can safely damage: a cloud free-tier VM, or locally with docker run -it --rm ubuntu bash (for the systemd steps use a VM or Multipass).
  2. Create a user myapp with sudo useradd --system --create-home myapp, and a directory /srv/myapp owned by it with mode 750.
  3. Write a unit file that runs python3 -m http.server 8080 as myapp from /srv/myapp. Run daemon-reload, then enable --now, and prove it works with curl localhost:8080.
  4. Find its PID with pgrep, send it SIGKILL, and watch systemd restart it. Find the restart in journalctl -u myapp.
  5. Break it on purpose: change User= to a user that does not exist, restart, and diagnose it using only systemctl status and journalctl.
  6. Write a one-line pipeline that lists the five largest files under /var, and another that counts how many processes each user is running.
Cheat sheet

Linux & the command line — at a glance

Main things to focus on

  • The filesystem map: config in /etc, changing data and logs in /var, programs in /usr.
  • Permission triplets and octal: r=4, w=2, x=1, so 750 is rwxr-x---. On directories, x means "may enter".
  • Exit code 0 is success, anything else is failure. All automation is built on this.
  • SIGTERM asks, SIGKILL forces. Always try SIGTERM first.
  • systemctl start is for now, systemctl enable is for the next boot. You usually want both.
  • Debugging order: status, then logs, then disk and memory, then network, then change one thing.

Navigate and inspect

ls -lah PATHLong listing including hidden files, human-readable sizes
df -hFree space per filesystem
du -sh * | sort -hSize of each item here, smallest to largest
find PATH -name "*.log" -mtime +7Files by name pattern, modified more than 7 days ago
less FILEPage through a file; /text searches, q quits
tail -f FILEFollow a file as new lines arrive

Permissions and users

chmod u+x FILEAdd execute for the owner (symbolic form)
chmod 640 FILEOwner rw, group r, others none (octal form)
chown USER:GROUP FILEChange owner and group; add -R for a whole tree
idCurrent user, UID, and group memberships
sudo -u USER CMDRun one command as another user

Processes

ps auxEvery process with owner, CPU, memory
pgrep -a NAMEPIDs and command lines matching a name
kill PIDSend SIGTERM (graceful)
kill -9 PIDSend SIGKILL (cannot be caught; last resort)
ss -tulpnListening TCP/UDP ports and the owning process
echo $?Exit code of the previous command

Services and logs

systemctl status NAMEState, uptime, and last log lines
systemctl enable --now NAMEStart now and at every boot
systemctl daemon-reloadRe-read unit files after editing them
journalctl -u NAME -fFollow one service's logs
journalctl -u NAME --since "1 hour ago"Logs for a time window
journalctl -p err -bErrors and worse since the last boot

Pipes and redirection

CMD1 | CMD2stdout of CMD1 becomes stdin of CMD2
CMD > FILEWrite stdout to FILE, replacing it (>> appends)
CMD > FILE 2>&1stdout and stderr both into FILE
CMD 2> /dev/nullThrow away error output
sort | uniq -c | sort -rnCount duplicates, most frequent first
grep -ri TEXT PATHRecursive, case-insensitive search

Common pitfalls

  • Fixing a permission error with chmod 777 instead of finding out which user the process runs as.
  • Starting a service but never enabling it, so it silently stays down after the next reboot.
  • Reaching for kill -9 first, which skips cleanup and can corrupt data or leave stale lock files.
  • Editing a unit file and forgetting systemctl daemon-reload, then wondering why nothing changed.
  • Using relative paths in scripts and cron jobs, which run from a directory you did not expect.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →