Home
DevOps & Cloud Engineering / Lesson 2 — Linux Fundamentals for DevOps

Linux Fundamentals for DevOps

The terminal, processes, permissions, and signals — every cloud server you'll ever touch runs on this.


Why Linux Matters Even More in the Cloud Era

Almost every cloud server you'll ever touch runs Linux. Your Docker containers? Linux. Your Kubernetes nodes? Linux. AWS EC2 default? Amazon Linux. Most managed databases? Linux underneath.

Even if you live in a managed-services world (Lambda, Cloud Run, Heroku), debugging eventually drags you into Linux: tailing logs, exec'ing into a container, looking at process trees, checking disk usage, reading systemd unit files.

The DevOps engineer who's fluent on the Linux command line ships and debugs at 3-5x the speed of one who isn't. This isn't elitism — it's the difference between stepping through a problem and waiting for things to load in a web console.

This lesson is the foundation for almost everything that follows. Read it. Practice the commands. The investment pays back forever.


The Filesystem — Everything Is a File

Linux's mental model is "everything is a file": regular files, directories, devices, sockets, even running processes (in /proc). Once you internalize this, a lot of Linux's design clicks.

The standard directory layout you'll see on every server:

Text
/                Root of the filesystem (no drive letters in Linux)
├── bin/         Essential user binaries (ls, cp, cat — symlinked to /usr/bin)
├── boot/        Kernel and bootloader files
├── dev/         Device files (disks, terminals — even /dev/null)
├── etc/         System-wide configuration files (think "et cetera")
├── home/        User home directories — /home/alice, /home/bob
├── lib/         Shared libraries
├── opt/         Optional / third-party software
├── proc/        Live kernel and process info (not real files on disk)
├── root/        Root user's home directory (NOT the same as /)
├── run/         Runtime data — PIDs, sockets — cleared on boot
├── sbin/        System binaries (ifconfig, fdisk — usually root-only)
├── srv/         Service data
├── sys/         Kernel and hardware info (similar to /proc)
├── tmp/         Temporary files (often cleared on boot)
├── usr/         User programs and libraries (the bulk of the OS)
│   ├── bin/     Most commands you run
│   ├── lib/
│   └── local/   Locally-installed software (often /usr/local/bin)
└── var/         Variable data — logs (/var/log), spool files, caches

Memorize the high-level tree, especially: /etc for config, /var/log for logs, /home for users, /tmp for scratch.

Navigation commands you'll use constantly:

Bash
pwd                    # print working directory
cd /var/log            # change directory
cd ~                   # go to your home directory (~ = home)
cd -                   # go to the previous directory
ls                     # list files (current directory)
ls -la                 # list all (including hidden), long format
ls -lh                 # list with human-readable sizes (1.2M not 1234567)
ls -lt                 # list sorted by modification time
tree                   # visual directory tree (install separately on most distros)

Hidden files start with . (a dot). This is a convention, not a special filesystem flag. .bashrc, .gitconfig, .ssh/ — all hidden in normal listings. Pass -a to ls to see them.


Files — Reading, Writing, Editing

The commands every DevOps engineer types dozens of times a day:

Reading files:

Bash
cat file.txt           # print entire file
less file.txt          # scrollable pager (q to quit, / to search)
head file.txt          # first 10 lines
head -n 50 file.txt    # first 50 lines
tail file.txt          # last 10 lines
tail -f /var/log/app   # follow — show new lines as they appear (great for logs)
tail -n 100 -f file    # last 100 lines, then follow
wc -l file.txt         # count lines

Searching files:

Bash
grep "ERROR" app.log              # lines containing "ERROR"
grep -i "error" app.log           # case-insensitive
grep -r "TODO" .                  # recursive search in current dir
grep -v "DEBUG" app.log           # invert: lines NOT containing "DEBUG"
grep -n "ERROR" app.log           # show line numbers
grep -A 2 -B 2 "error" log        # 2 lines after and before each match
grep -c "ERROR" app.log           # count matches
grep -E "(ERROR|FATAL)" app.log   # extended regex (multiple patterns)

Searching for files:

Bash
find . -name "*.log"              # all .log files in current dir + subdirs
find /var/log -mtime -1           # modified in last 1 day
find /tmp -size +100M             # bigger than 100 MB
find . -type d -name "node_modules"  # find directories named node_modules
find . -name "*.pyc" -delete      # delete all .pyc files (be careful!)

Editing files (you need ONE of these — pick one and learn it):

For DevOps work, you'll often pop into vim on a remote server when you only have SSH. Knowing the basic vim escape sequences (:wq, :q!, Esc) is genuinely a survival skill.

Writing files quickly:

Bash
echo "hello" > file.txt           # write (overwrite)
echo "more" >> file.txt           # append
cat > file.txt << EOF             # heredoc — type/paste, then "EOF" alone on a line
line one
line two
EOF

Permissions — Read, Write, Execute

Linux file permissions look intimidating but become second nature.

Every file has an owner (user), a group, and three permission triplets — for owner, group, and everyone else. Each triplet has three bits: read (r), write (w), execute (x).

Text
$ ls -l
-rw-r--r--  1 alice  staff  1234  May  5 12:30  config.txt
↑ │  │  │
│ │  │  └─ permissions for "others" (everyone else)
│ │  └──── permissions for the group ("staff")
│ └─────── permissions for the owner ("alice")
└───────── file type:  -=file  d=directory  l=symlink

Reading permissions: -rw-r--r-- means:
• Owner: read + write
• Group: read only
• Others: read only

Numeric (octal) form is common in scripts:
• r = 4
• w = 2
• x = 1

Add them up per triplet:

Text
755 → rwxr-xr-x   (owner: rwx, group: r-x, others: r-x)
644 → rw-r--r--   (owner: rw-, group: r--, others: r--)
600 → rw-------   (owner: rw-, group/others: nothing)
700 → rwx------   (owner: rwx, group/others: nothing)
777 → rwxrwxrwx   (everyone everything — almost always wrong)

Changing permissions:

Bash
chmod 755 script.sh        # numeric
chmod +x script.sh         # add execute for everyone
chmod u+x script.sh        # add execute for owner (u=user)
chmod g-w file.txt         # remove write for group
chmod -R 755 directory/    # recursive (entire tree)

Changing owner / group:

Bash
chown alice file.txt              # change owner
chown alice:staff file.txt        # change owner AND group
chown -R alice:staff directory/   # recursive

Common patterns:
• 755 for directories — anyone can list and enter, only owner can modify
• 644 for normal files — owner writes, others read
• 600 for secrets — only owner can read or write
• 700 for ~/.ssh — SSH refuses to use a directory with looser permissions
chmod +x for scripts you want to execute directly

The "execute" bit on a directory means something different from a file: it means "you can enter this directory and access files within it." A directory needs x to be useful.

Special bits (you'll encounter these less often but they exist): setuid (s instead of x), setgid, sticky bit (t on /tmp).


Processes & Signals

Every running program is a process. Linux exposes processes as files in /proc/<pid>/, but you'll usually interact with them via commands.

Listing processes:

Bash
ps                           # processes from your current shell
ps aux                       # all processes, all users, full info (BSD style)
ps -ef                       # all processes (System V style — equivalent)
ps aux | grep nginx          # find nginx processes
top                          # interactive — sorted by CPU usage, refreshes
htop                         # better top (install if not present)
pgrep nginx                  # just the PIDs of nginx processes

Each process has:
• A PID (process ID) — unique number
• A PPID (parent process ID) — who started it
• A user — whose permissions it runs with
• An executable
• Memory and CPU stats

Killing processes:

Bash
kill 1234                    # send SIGTERM (polite "please exit")
kill -9 1234                 # send SIGKILL (forced — last resort)
kill -SIGTERM 1234           # explicit signal name
killall nginx                # kill ALL processes named nginx
pkill -f "python myapp"      # kill processes matching command line

Signals — the OS communicates with processes via signals:

Text
SIGTERM (15)   "Please shut down cleanly"      ← always try first
SIGKILL (9)    "Die now" (uncatchable)         ← last resort
SIGINT  (2)    Sent by Ctrl-C
SIGHUP  (1)    Hangup — historically when terminal closed; many daemons
               reload config on SIGHUP
SIGSTOP/SIGCONT  Pause and resume a process

Why this matters for DevOps: graceful shutdown depends on apps handling SIGTERM properly (covered in Backend Module 17). Kubernetes sends SIGTERM, waits 30 seconds, then sends SIGKILL. If your app ignores SIGTERM, you lose data on every deploy.

Backgrounding and foregrounding:

Bash
./long_script.sh &           # run in background
jobs                         # list background jobs
fg                           # bring most recent to foreground
fg %1                        # bring job 1 to foreground
bg                           # resume a stopped job in background
nohup ./script.sh &          # run in background, immune to terminal hangup
disown                       # detach a job from the current shell

For long-running processes that should survive a logout, use a terminal multiplexer like tmux or screen — these let you start a session, detach from it, log out, and reattach later from another machine.


Pipes & Redirection — Where Linux Becomes Powerful

The Unix philosophy is "do one thing well, and let users compose tools together." Pipes (|) are how you compose them. This is where the command line goes from useful to genuinely powerful.

Standard streams:
• stdin (file descriptor 0) — input
• stdout (1) — normal output
• stderr (2) — error messages

Redirection:

Bash
command > file.txt           # send stdout to file (overwrite)
command >> file.txt          # send stdout to file (append)
command 2> errors.txt        # send stderr to file
command > out.txt 2>&1       # send both stdout AND stderr to out.txt
command &> all.txt           # shorter form of the above (bash)
command < input.txt          # read stdin from file
command 2>/dev/null          # discard errors completely
command > /dev/null 2>&1     # discard everything (silent)

/dev/null is the void — write to it and it disappears. Read from it and you get EOF.

Pipes — connecting commands:

Bash
ls -la | grep "^d"                          # only directories
cat /var/log/syslog | grep ERROR | wc -l    # count error lines
ps aux | sort -k 4 -nr | head -10           # top 10 by memory
du -sh * | sort -h                          # disk usage, sorted
history | grep ssh                          # shell commands containing ssh

A few essential composition tools:

Bash
# sort and uniq
sort file.txt | uniq -c | sort -nr        # frequency-sorted unique lines

# cut — extract columns
cat /etc/passwd | cut -d: -f1              # get usernames
ps aux | awk '{print $1}' | sort | uniq    # unique users running processes

# tr — translate/delete characters
echo "hello world" | tr 'a-z' 'A-Z'        # → HELLO WORLD
cat file | tr -d '\r'                      # remove Windows carriage returns

# xargs — turn input lines into command arguments
find . -name "*.tmp" | xargs rm -f         # delete every found file
echo "a b c" | xargs -n1 echo              # one argument per call

# tee — split output to file AND stdout
command | tee output.log                   # see it AND save it
command 2>&1 | tee -a app.log              # append both streams

The awk and sed tools are entire mini-languages used in pipelines. You don't need to master them — knowing they exist and being able to look up syntax when needed is enough for most DevOps work.


SSH — Your Connection to Every Server

SSH (Secure Shell) is how you connect to remote servers. Every cloud VM, every Linux container you exec into, every Git push — all SSH under the hood.

Basic connection:

Bash
ssh user@example.com                  # connect to example.com as user
ssh -p 2222 user@host                 # custom port
ssh -i ~/.ssh/id_special user@host    # specific key

SSH keys — the right way:

Passwords for SSH are insecure (brute-forceable, phishable). Public-key authentication is the standard. You generate a keypair once and put your public key on every server you want to access.

Bash
ssh-keygen -t ed25519 -C "your_email@example.com"   # generate keypair
# saves to ~/.ssh/id_ed25519 (private — KEEP SECRET)
# and    ~/.ssh/id_ed25519.pub (public — share freely)

ssh-copy-id user@host                 # install your public key on the server
# now: ssh user@host  (no password)

Once your key is installed:
• The server stores your public key in ~/.ssh/authorized_keys
• When you connect, your local agent proves you have the matching private key
• No password ever leaves your machine

The SSH config file — the productivity multiplier:

In ~/.ssh/config (create if missing), define shortcuts:

Text
Host prod
    HostName prod-1.internal.example.com
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_prod

Host bastion
    HostName bastion.example.com
    User alice
    IdentityFile ~/.ssh/id_corp

# Jump through bastion to internal hosts
Host internal-*
    User alice
    ProxyJump bastion

After this:

Bash
ssh prod                              # connects to prod-1.internal as deploy
ssh internal-db1                      # tunnels through bastion to internal-db1

Common SSH operations:

Bash
# Run a command remotely without staying in a shell
ssh user@host 'systemctl status nginx'

# Copy files (scp = secure copy)
scp file.txt user@host:/tmp/                    # local → remote
scp user@host:/var/log/app.log .                # remote → local
scp -r directory/ user@host:/tmp/               # recursive

# Better: rsync — faster for large transfers, only sends differences
rsync -avz local_dir/ user@host:/remote_dir/    # sync directories
rsync -avz --delete src/ host:/dst/             # mirror (delete extras on dst)

# Forward a port — local 8080 → remote port 80
ssh -L 8080:localhost:80 user@host

# Run commands as you go — useful for one-off troubleshooting
ssh user@host 'tail -f /var/log/app.log'

SSH security essentials:
• Disable password authentication on production servers (PasswordAuthentication no in /etc/ssh/sshd_config)
• Use ed25519 keys (modern, fast, secure) — older RSA still works but ed25519 is preferred
• Never share private keys. Each engineer has their own.
• Use a bastion host for production: production servers don't accept connections from the internet, only from a single hardened bastion.


Systemd — The Init System

When a Linux machine boots, something has to start the services (sshd, nginx, your app). Modern Linux uses systemd for this.

systemd manages services as "units" defined by .service files. The key commands:

Bash
systemctl status nginx           # is it running? recent log lines?
systemctl start nginx            # start now
systemctl stop nginx             # stop now
systemctl restart nginx          # stop, start
systemctl reload nginx           # send SIGHUP — config reload without restart
systemctl enable nginx           # start on boot
systemctl disable nginx          # don't start on boot
systemctl daemon-reload          # reload systemd's view of unit files
                                 # (run after editing a .service file)

Service unit files live in /etc/systemd/system/ (your custom services) or /lib/systemd/system/ (package-installed). Here's a minimal one for a Node.js app:

Ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js App
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
StandardOutput=append:/var/log/myapp/stdout.log
StandardError=append:/var/log/myapp/stderr.log

[Install]
WantedBy=multi-user.target

After creating it: systemctl daemon-reload && systemctl enable --now myapp.

Logs from systemd services go to the journal:

Bash
journalctl -u nginx              # all logs from nginx
journalctl -u nginx -f           # follow (like tail -f)
journalctl -u nginx --since "10 minutes ago"
journalctl -u nginx -n 100       # last 100 lines
journalctl -p err                # only error-priority messages
journalctl --disk-usage          # how much disk the journal is using

Even when running containerized apps in 2026, you'll often have a systemd unit running Docker or the kubelet. Knowing systemd is foundational.


The Tools You'll Use Daily

A grab-bag of commands that come up constantly in real DevOps work:

Disk and filesystem:

Bash
df -h                            # disk free, human-readable
df -hT                           # also shows filesystem type
du -sh /var/log                  # size of /var/log
du -sh * | sort -h               # size of each child, sorted
ncdu /var/log                    # interactive disk usage explorer (recommended)
lsblk                            # block devices (disks, partitions)
mount                            # what's mounted where

Network:

Bash
ip addr                          # network interfaces (replaces ifconfig)
ip route                         # routing table
ss -tlnp                         # listening TCP ports + processes (replaces netstat)
ss -tnp                          # all TCP connections
curl -v https://example.com      # HTTP request with headers
curl -X POST -d '{"k":"v"}' \
     -H 'Content-Type: application/json' url    # POST JSON
wget -O - https://example.com    # alternative
ping example.com                 # is it reachable
traceroute example.com           # path hops to destination
mtr example.com                  # combined ping+traceroute (recommended)
dig example.com                  # DNS lookup, full detail
host example.com                 # simpler DNS lookup
nslookup example.com             # alternative DNS lookup

System info:

Bash
uname -a                         # kernel and OS info
uptime                           # how long running, load average
free -h                          # memory usage
nproc                            # number of CPU cores
lscpu                            # detailed CPU info
hostnamectl                      # hostname info
date                             # current date/time + timezone
date -u                          # in UTC

Process resource usage:

Bash
top                              # interactive process view
htop                             # better top
iotop                            # disk I/O by process
nethogs                          # network I/O by process

Package management (varies by distro):

Bash
# Debian / Ubuntu
apt update && apt upgrade
apt install nginx
apt remove nginx
apt search nginx

# RHEL / CentOS / Amazon Linux
yum install nginx                # older
dnf install nginx                # newer (DNF replaces YUM)

# Alpine (lightweight, common in Docker)
apk add nginx

Tar and compression:

Bash
tar -czf backup.tar.gz dir/      # create gzipped tarball
tar -xzf backup.tar.gz           # extract
tar -tzf backup.tar.gz           # list contents without extracting
gzip file.txt                    # compress file → file.txt.gz
gunzip file.txt.gz               # decompress
zip -r archive.zip dir/          # create zip
unzip archive.zip                # extract zip

Memorize these incrementally. Use man <command> or <command> --help whenever you need details. The Linux man pages are dense but authoritative.

This was a long lesson — and we just scratched the surface. The shell is a deep tool. Spend a few weeks living in it and your productivity will permanently shift upward.


⁂ Back to all modules