Skip to content
All guides

beginner 3 hr 20 min read Part of the Linux roadmap Updated 2026-06-27

Linux for DevOps Engineers

A comprehensive guide to Linux fundamentals for DevOps: filesystem, permissions, process management, systemd, networking, SSH, bash scripting, and more.


On this page · 39 sections

Linux is the operating system under every Docker container, every Kubernetes node, and the majority of cloud VMs. Before you can deploy, debug, or automate anything in a modern infrastructure stack, you need to be comfortable at the shell. This guide takes you from the absolute fundamentals — how Linux thinks about files and processes — through the tools DevOps engineers reach for daily: permissions, grep/awk/sed, systemd, SSH, package managers, network commands, and production-grade bash scripting. Follow the Linux roadmap to see how these topics connect into a full learning path.

Introduction to Linux

Section 1 of 39 · ~6 min

Imagine you bought a brand new car. You see the dashboard, the steering wheel, the leather seats, the touchscreen infotainment system. But what actually moves the car? The engine. You rarely open the hood, you never see it during your daily drive, yet without it, nothing works. Linux is exactly that engine for the entire modern internet. Every time you scroll Instagram, order from Uber Eats, watch Netflix, or tap your phone to pay — somewhere in a datacenter, a Linux server is doing the heavy lifting. You never see it. You never think about it. But it’s powering 96% of the world’s digital infrastructure in 2026.

As an experienced frontend developer, you’ve been writing code that runs on top of this engine. You’ve been the person designing the dashboard. Now you’re about to learn what’s under the hood — and that’s exactly what the highest-paid DevOps engineers know cold. The transition from frontend to DevOps is one of the most valuable career moves in tech today, because you already understand how applications behave; now you’ll learn how they live, breathe, scale, and recover at the infrastructure level.

Real world: Think of Linux like the global air-traffic network. You don’t see the radar, the control towers, or the routing systems when you fly — but they’re the reason tens of thousands of flights move millions of passengers safely every single day. Linux is the air-traffic network of the internet. AWS, Azure, GCP, Netflix, Google, Facebook, WhatsApp, Spotify, Amazon — they all run on Linux.

Why Linux Dominates DevOps in 2026

If you walked into any tech company in a major tech hub today and asked their SRE team what operating system runs production, the answer is almost universally Linux. Here’s why this domination is now absolute:

  • ~96% of public cloud servers run Linux — AWS EC2, Azure VMs, Google Compute Engine. Even Microsoft, which makes Windows, runs the majority of Azure on Linux.
  • 100% of containers use the Linux kernel — Docker, Podman, containerd all rely on Linux namespaces and cgroups. Even “Windows containers” running on Docker Desktop spin up a hidden Linux VM underneath.
  • Every single Kubernetes node is Linux — kubelet, kube-proxy, CNI plugins, the entire control plane is designed for Linux first.
  • All major CI/CD runners are Linux — GitHub Actions, GitLab Runners, Jenkins agents, ArgoCD, CircleCI default to Linux.
  • Every observability stack runs on Linux — Prometheus, Grafana, Loki, Elastic, Datadog agents, OpenTelemetry collectors.
  • Edge computing and IoT — Raspberry Pi, AWS Greengrass, Azure IoT Edge, all Linux.

For a React developer transitioning to DevOps, this means one undeniable truth: you cannot avoid Linux. It’s not optional anymore. The good news? You already have Ubuntu 24.04 installed, which is the gold standard for DevOps learning in 2026.

Linux vs Windows vs macOS: The Honest Comparison

FeatureLinuxWindowsmacOS
CostFree (most distros)~$140 per licenseBundled with Apple hardware (premium)
CustomizationUnlimited — change anythingLimited to UI/registryRestricted — Apple’s way only
CLI PowerNative, world-class (bash, zsh)Catching up (PowerShell, WSL2)Strong (Unix-based, zsh default)
Server Use~96% of internet servers~3% (legacy enterprise AD)Negligible (not a server OS)
SecurityExcellent (permissions, SELinux)Improving but still targetedStrong (sandboxed by default)
Package Mgmtapt, dnf, pacman, snap, flatpakwinget, choco, MSI installersbrew (community), App Store
Source Code100% open source (GPL)Proprietary (closed)XNU kernel partially open, OS closed
DevOps FitThe de-facto standardNiche (Windows containers)Good for local dev only

The Linux Distro Universe

One of the most confusing things for beginners is the sheer number of Linux distributions (“distros”). They’re all Linux at the core (same kernel), but they package the system differently — different installers, different package managers, different defaults, different philosophies. Here’s the map you need:

DistroBasePackage MgrBest ForDevOps Use
UbuntuDebianaptDevelopment, learning, desktopsDev laptops, CI runners, EC2 default
RHELFedoradnf/yumEnterprise productionBanks, telco, govt, regulated industries
CentOS StreamRHEL upstreamdnfTesting RHEL features earlyRolling preview of RHEL
DebianIndependentaptStable servers, rock-solid uptimeLong-running production servers
AlpineIndependent (musl)apkContainers — only 5MB base!99% of Docker base images
Rocky/AlmaLinuxRHEL clonesdnfFree RHEL alternativePost-CentOS production replacement
Arch LinuxIndependentpacmanAdvanced users, customizationPersonal workstations (rarely prod)
Amazon Linux 2023Fedora-baseddnfAWS-optimized workloadsDefault EC2/ECS/Lambda runtime

Note: Which distro do real DevOps engineers actually use?

  • Dev laptop: Ubuntu 24.04 LTS or macOS
  • Production servers: RHEL, Amazon Linux 2023, Rocky Linux, or Ubuntu Server LTS
  • Docker base images: Alpine (tiny, fast), or distroless/Debian-slim
  • Kubernetes nodes: Ubuntu, Bottlerocket (AWS), Flatcar, or Talos Linux

Master Ubuntu first because apt-based skills transfer 80% to Debian and Kali. Then learn dnf for RHEL/Rocky/Amazon Linux. That covers ~95% of real-world DevOps scenarios.

Kernel vs Operating System

This is the single most common interview blunder. Junior candidates confuse “kernel” and “OS” all the time.

The Linux Kernel is the core program — roughly 30+ million lines of C code — written originally by Linus Torvalds in 1991 when he was a 21-year-old student in Helsinki, Finland. The kernel is just the engine — it talks to the CPU, RAM, disks, network cards, and so on. By itself, the kernel cannot give you a usable computer.

An Operating System is the kernel plus all the surrounding software needed to make it usable: the GNU utilities (ls, cat, grep, sed, awk), the shell (bash, zsh), system libraries (glibc), package managers (apt, dnf), and applications. So when you say “Linux,” what you usually mean is “GNU/Linux.”

$ uname -r
6.8.0-31-generic
$ cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
$ uname -a
Linux dev-laptop 6.8.0-31-generic #31-Ubuntu SMP x86_64 GNU/Linux

Interview tip: If an interviewer asks “What is Linux?” — never just say “an operating system.” Say: “Linux is technically the kernel — a monolithic Unix-like kernel created by Linus Torvalds in 1991. When people say ‘Linux,’ they usually mean GNU/Linux, which is the kernel plus the GNU userland utilities, a shell, system libraries like glibc, and applications, all packaged by a distribution like Ubuntu or RHEL.”

Open Source and the GPL

Linux is released under the GNU General Public License version 2 (GPLv2). The GPL guarantees four fundamental freedoms (numbered 0 through 3, because programmers count from zero):

  • Freedom 0: Run the program for any purpose, without restriction.
  • Freedom 1: Study how the program works and modify it. Source code access is required.
  • Freedom 2: Redistribute copies to help others.
  • Freedom 3: Distribute your modified versions so the community benefits.

Why do massive corporations like Google, Meta, Amazon, and Microsoft pour billions into a “free” project? Because open-source Linux gives them zero licensing cost at scale, total customizability, security through transparency, vendor independence, and faster innovation.

The DevOps Salary Reality (2026)

LevelExperienceUS (USD)Europe (EUR)India (INR)Typical Skills
Junior DevOps0-2 years$65k–100k€45k–70k₹6–12 LPALinux basics, Git, Docker, basic CI/CD, one cloud
Mid DevOps / SRE2-5 years$100k–150k€70k–105k₹15–25 LPAKubernetes, Terraform, Prometheus, scripting, multi-cloud
Senior DevOps / SRE5-8 years$150k–200k€105k–145k₹30–50 LPAArchitecture, security, cost optimization, on-call leadership
Lead / Staff / Principal8-12+ years$190k–270k€140k–190k₹50–80+ LPAPlatform engineering, multi-team coordination, strategy
DevOps Manager / Director10+ years$200k–350k+€150k–230k₹70 LPA–1.5 Cr+People management, business alignment, P&L

Figures are rough 2026 market signals, not guarantees. They swing widely with company size, city, cost of living, and equity — treat them as the relative gaps between levels, not absolute targets.

Linux Architecture

Section 2 of 39 · ~5 min

Now that you know what Linux is, let’s open the hood and look at how it actually works. Linux follows a layered architecture — each layer has a specific job, and they communicate through well-defined interfaces. If you understand this architecture clearly, you’ll debug production issues 10x faster than engineers who only know commands by rote.

The Kernel: The Brain of the System

The Linux kernel is the lowest software layer above the hardware. It has six core responsibilities that you must internalize:

  1. Process Management: Creating, scheduling, pausing, killing processes. The kernel decides which process gets CPU time. Tools like ps, top, htop, kill all interact with this subsystem.
  2. Memory Management: Allocating RAM to processes, managing virtual memory, paging, swapping. The OOM (Out of Memory) killer lives here.
  3. Device Drivers: Translating generic OS calls into hardware-specific signals — GPU, NIC, SSD, USB, Bluetooth all speak through drivers loaded as kernel modules.
  4. Filesystem Management: Reading, writing, organizing data on disk through filesystems like ext4, xfs, btrfs, zfs, overlayfs (used by Docker).
  5. Networking: The entire TCP/IP stack, sockets, packet routing, iptables/nftables — all kernel code.
  6. Security & Permissions: User IDs, group IDs, file permissions, capabilities, SELinux/AppArmor, seccomp filters (used by Docker for sandboxing).

Shells: Your Window to the Kernel

You don’t talk to the kernel directly — you talk to a shell, and the shell talks to the kernel. A shell is just a program that reads commands, interprets them, and asks the kernel to do work via system calls.

ShellDefault OnStrengthsWeaknessesDevOps Verdict
BashUbuntu, RHEL, Debian, Alpine (ash)Universal, scripting, POSIX compliancePlain UX, no autosuggestionsMust master — every server has it
ZshmacOS Catalina+Themes (oh-my-zsh), autosuggestions, pluginsSlightly heavier, not on serversGreat for daily dev laptop use
FishNone defaultFriendly UX, autosuggest out of the boxNon-POSIX — scripts don’t transferPersonal use only, never scripts
DashUbuntu’s /bin/shTiny, fast, POSIX-strictMinimal featuresUsed by Ubuntu boot scripts
PowerShellWindowsObject pipeline, cross-platform nowHeavy, alien to Unix folkSkip unless Windows-shop

Tip: Use Zsh + oh-my-zsh on your Ubuntu dev laptop for daily comfort, but always write your shell scripts in Bash (with #!/usr/bin/env bash) so they run on every server you’ll ever touch. Production servers don’t have your fancy zsh setup.

System Libraries and Utilities

Between user programs and the kernel sit two crucial layers. System Libraries — the most important is glibc (GNU C Library). When your Node.js process needs to open a file, it calls a C function like fopen() in glibc, which translates to the kernel’s open() system call. Alpine Linux uses musl libc instead, which is why Alpine images are smaller but sometimes have subtle compatibility quirks.

System Utilities — the GNU coreutils package gives you ls, cp, mv, rm, cat, echo, mkdir, chmod, chown, and ~100 more. Then there’s util-linux, findutils, grep, sed, awk, tar, gzip — collectively known as “the Unix toolbox.”

User Space vs Kernel Space

Modern CPUs have two privilege modes: kernel mode (ring 0 on x86) where code can do anything, and user mode (ring 3) where code is sandboxed and must ask the kernel (via system calls) to do anything dangerous. Your React app, Node.js, Python scripts, even databases like PostgreSQL — all run in user space. Only the kernel and its drivers run in kernel space. You can literally see system calls with strace:

$ strace -c ls /tmp
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 24.31    0.000312          26        12           openat
 18.13    0.000233          19        12           read
 11.45    0.000147          12        12           close

The Linux Boot Process

When you press the power button on a Linux machine (or click “Start instance” on an EC2 console), a tightly choreographed sequence brings the system to life. Understanding this is critical because most production outages happen during boot.

  1. BIOS / UEFI: The first code that runs lives in firmware on the motherboard. It initializes the CPU and looks for a bootable device. On EC2, this is the Nitro hypervisor’s firmware.
  2. POST (Power-On Self-Test): Hardware checks — RAM, keyboard, basic devices.
  3. MBR / GPT Boot Sector: Firmware reads the first sector of the boot disk to find the bootloader.
  4. GRUB (GRand Unified Bootloader): Displays the boot menu, lets you pick a kernel, and loads the kernel image and initramfs into memory. Config lives in /boot/grub/grub.cfg.
  5. Kernel + initramfs: The kernel unpacks itself, the initramfs provides essential drivers to mount the real root filesystem. Kernel mounts / and starts PID 1.
  6. systemd (PID 1): Modern init system. Reads /etc/systemd/system/, starts services in dependency order, mounts filesystems from /etc/fstab, brings up the network.
  7. multi-user.target / graphical.target: systemd’s “runlevel” — on servers it stops at multi-user.target (text login).
  8. Login prompt: getty on TTYs, sshd for remote, GDM for graphical.

Interview tip: “Walk me through what happens when you press the power button on a Linux server until you get a login prompt.” This is asked in literally every senior DevOps interview. Memorize the 8-step sequence above. Bonus points if you mention systemd targets, initramfs, and the role of /etc/fstab.

DevOps Tie-Ins: Why Boot Knowledge Pays

  • EC2 boot failures: When an EC2 instance won’t start, AWS shows you the serial console output — you see exactly which boot step failed.
  • GRUB recovery: Kernel update broke boot? You boot from a rescue ISO, chroot into the broken system, run grub-install and update-grub.
  • systemd unit debugging: A service won’t start at boot? systemctl status, journalctl -xeu service-name, check dependencies with systemctl list-dependencies.
  • Custom AMI building: Packer images, custom EKS node AMIs, Bottlerocket — all require understanding what boots and when.
  • Container startup: Containers skip BIOS/GRUB/kernel — they start at “PID 1 in a namespace.” tini exists exactly because containers need a proper PID 1.

Linux Filesystem Hierarchy

Section 3 of 39 · ~2 min

Linux organises everything into a single tree rooted at /. Unlike Windows with drive letters, there is no C:\ — everything is a file or a directory beneath /. Understanding where things live stops you from wasting time searching for config files.

Key Directories

PathWhat lives here
/bin, /usr/binEssential user binaries (ls, cp, bash)
/sbin, /usr/sbinSystem administration binaries (fdisk, iptables)
/etcSystem-wide configuration files — everything in plain text
/homeUser home directories (/home/pushkar)
/rootRoot user’s home
/varVariable data: logs (/var/log), mail, caches, spool
/tmpTemporary files; cleared on reboot
/procVirtual filesystem exposing kernel and process state
/sysVirtual filesystem for kernel subsystems (devices, drivers)
/devDevice files (/dev/sda, /dev/null, /dev/random)
/bootKernel image, initrd, GRUB config
/optOptional third-party software
/usr/localLocally compiled software (takes precedence over /usr)
/runRuntime data (PIDs, sockets) — tmpfs, cleared on boot
/mnt, /mediaMount points for temporary / removable filesystems
pwd                    # print working directory
ls -la /etc            # long listing including hidden files
ls -lh /var/log        # human-readable sizes
cd /var/log            # change directory
cd -                   # go back to previous directory
cd ~                   # go home
tree -L 2 /etc         # visual tree, depth 2 (install with apt)

Every file is represented by an inode — a data structure storing metadata (owner, permissions, timestamps, block pointers) but not the filename. Directory entries are simply (name → inode number) mappings. This is why:

  • Hard links are just extra directory entries pointing to the same inode. Delete one, the data survives until the last link is removed.
  • Symbolic links (ln -s) store a path string. They can cross filesystem boundaries; hard links cannot.
  • df -i shows inode usage — a filesystem can run out of inodes before running out of blocks if you create millions of tiny files.
ls -li /etc/hostname        # shows inode number in first column
stat /etc/hostname          # full inode metadata dump
ln /etc/hostname /tmp/hn    # hard link (same inode)
ln -s /etc/hostname /tmp/hn_sym   # symbolic link

Tip: /proc and /sys contain no data on disk — they are windows into kernel memory. cat /proc/cpuinfo reads CPU registers; cat /proc/$(pgrep nginx)/status reads a live process’s memory map. Learn to explore them freely.

File Directory Operations

Section 4 of 39 · ~2 min

Confident file manipulation — copying, moving, finding, archiving — is the foundation of every shell workflow.

Essential Commands

# Create
touch notes.txt              # create empty file / update mtime
mkdir -p projects/web/src    # create directory tree in one shot

# Copy, Move, Delete
cp -r /etc/nginx /tmp/nginx-bak    # recursive copy
cp -p file dest/                   # preserve permissions + timestamps
mv old_name new_name               # rename (or move)
rm -rf /tmp/scratch                # recursive force delete — no undo!

# View
cat /etc/hostname           # dump whole file
less /var/log/syslog        # paged viewer (q to quit, / to search)
head -20 /var/log/auth.log  # first 20 lines
tail -50 /var/log/syslog    # last 50 lines
tail -f /var/log/nginx/access.log  # follow (real-time stream)

Finding Files

# find — the workhorse
find /var/log -name "*.log" -mtime -1          # modified in last 24 h
find /home -type f -size +100M                 # regular files over 100 MB
find /etc -name "*.conf" -exec grep -l "port" {} \;   # grep inside results
find /tmp -type f -newer /etc/hostname         # newer than reference file
find . -name "*.pyc" -delete                   # find and delete in one pass

# locate — index-based, faster but stale
sudo updatedb            # rebuild index
locate nginx.conf        # instant search

# which / type — find an executable
which python3
type ls                  # shows if alias, function, or binary

Archives and Compression

# tar — tape archive (most common)
tar -czf backup.tar.gz /etc/nginx      # create gzipped archive
tar -tzf backup.tar.gz                 # list contents without extracting
tar -xzf backup.tar.gz -C /tmp/       # extract to /tmp

# gzip / bzip2 / xz
gzip largefile.log         # compresses in-place → largefile.log.gz
gunzip largefile.log.gz    # decompress
zcat largefile.log.gz      # read without decompressing

# zip (cross-platform)
zip -r archive.zip dir/
unzip archive.zip -d /tmp/out

Tip: Remember tar flags as Create/eXtract zgzip file: czf to create, xzf to extract. For bzip2 use j instead of z. For xz use J.

File Viewing and Editing

Section 5 of 39 · ~7 min

As a DevOps engineer, 80% of your day will be spent reading logs, editing config files, and piecing together what went wrong on a server at 2 AM. Coming from React, you’ve used VS Code — but on a remote production server with no GUI, you have only the terminal. This section makes you fast at viewing files, slicing log data, and editing config files directly on Linux boxes.

Real world: On a production Ubuntu server, when nginx is throwing 502s, you SSH in and immediately run tail -f /var/log/nginx/error.log. Then you edit /etc/nginx/nginx.conf with vim, run nginx -t to validate, and reload. No VS Code, no GUI — just your terminal skills.

cat — Concatenate and View

cat dumps file contents to stdout. Great for small files; avoid on huge logs (use less instead).

$ cat /etc/hostname
$ cat -n /etc/nginx/nginx.conf       # -n prefixes every line with its line number
$ cat -A script.sh                   # -A shows hidden chars: $ for newline, ^I for tab, ^M for CRLF
$ cat file1.txt file2.txt > combined.txt
$ cat ~/.ssh/id_rsa.pub | ssh user@server 'cat >> ~/.ssh/authorized_keys'

Tip: If a bash script fails with weird “command not found” errors and you wrote it on Windows, run cat -A script.sh. If you see ^M$ at end of lines, that’s CRLF. Fix with tr -d '\r' < script.sh > clean.sh or dos2unix script.sh.

tac — Reverse cat

tac prints lines in reverse order (last line first). Useful for logs when you want the newest entries on top.

$ tac /var/log/syslog | head -20     # 20 most recent syslog entries, newest first
$ tac access.log | grep "POST /api/login" | head -5

less and more — The Pagers

less is the modern, more powerful pager. Rule: always use less.

$ less /var/log/nginx/access.log     # file is NOT loaded fully into memory — works on huge logs
$ less +F /var/log/syslog            # open directly in follow mode
$ journalctl -u nginx | less
KeyAction
qQuit
/patternSearch forward
?patternSearch backward
n / NNext / previous match
g / GFirst / last line
Space / bNext / previous page
FFollow mode (like tail -f) — Ctrl+C to exit
&patternShow ONLY lines matching pattern (filter)

head — First N Lines

$ head /etc/passwd                   # first 10 lines (default)
$ head -n 20 access.log
$ head -c 100 /dev/urandom | base64  # grab first 100 BYTES of random data
$ head -n -5 file.txt                # everything EXCEPT last 5 lines

tail — Last N Lines and Follow Mode

If there’s ONE command you must master for DevOps, it’s tail -f.

$ tail /var/log/syslog               # last 10 lines
$ tail -n 50 /var/log/nginx/error.log
$ tail -f /var/log/nginx/access.log  # FOLLOW: stream new lines live. Ctrl+C to stop.
$ tail -F /var/log/app/app.log       # -F = follow + REOPEN on rotation
$ tail -f /var/log/nginx/access.log | grep "POST"
$ tail -f app.log error.log          # follow multiple files

Caution: tail -f vs tail -F: Production logs are rotated daily by logrotate. If you use tail -f and the file gets rotated, you’ll silently stop seeing new logs. Always use tail -F for long-running tails on production. Capital F = forever.

wc — Word/Line/Char Count

$ wc -l /var/log/syslog              # count lines
$ wc -w README.md                    # count words
$ wc -c image.jpg                    # count bytes
$ grep "ERROR" app.log | wc -l       # how many errors today?

sort — Sort Lines

$ sort names.txt                     # alphabetical
$ sort -n sizes.txt                  # NUMERIC sort
$ sort -r names.txt                  # reverse
$ sort -h                            # human-readable: handles 1K, 2M, 3G
$ du -h /var/log/* | sort -h         # find which logs eat disk
$ sort -t: -k3 -n /etc/passwd        # sort users by numeric UID
$ sort -u file.txt                   # sort + dedupe

uniq — Remove/Count Duplicates

Critical: uniq only removes adjacent duplicates. You MUST sort first.

$ sort ips.txt | uniq -c             # -c prefixes each unique line with its count
$ sort ips.txt | uniq -d             # -d only shows lines appearing MORE than once
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# THE classic DevOps one-liner: top 10 IPs hitting your server

Real world: Spot a DDoS in 5 seconds: awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20. If one IP has 50,000 requests and the next has 200, you’ve got a bot. Block it with iptables or ufw.

cut, paste, tr

$ cut -d: -f1 /etc/passwd            # extract usernames (colon delimiter, field 1)
$ cut -d: -f1,3,7 /etc/passwd        # username, UID, shell
$ echo "2026-05-26T14:23:11" | cut -c1-10   # extract just the date

$ paste names.txt ages.txt           # merge line-by-line with TAB separator
$ paste -d, names.txt ages.txt > combined.csv

$ echo "Hello World" | tr A-Z a-z    # lowercase
$ tr -d '\r' < windows_script.sh > linux_script.sh   # fix CRLF line endings
$ echo "a   b     c" | tr -s ' '     # squeeze repeated spaces

tee — Write to File AND stdout

$ ls -la | tee listing.txt           # see output AND save it
$ echo "new line" | tee -a log.txt   # -a append
$ echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts
$ make 2>&1 | tee build.log

Tip: sudo tee: Bash redirection (>) happens in the calling shell BEFORE sudo runs. So sudo echo "x" > /etc/protected fails with “Permission denied.” Use echo "x" | sudo tee /etc/protected — tee runs under sudo and CAN write.

Text Editor: nano (beginner-friendly)

All shortcuts are visible at the bottom of the screen. Good for quick edits to /etc/hosts or a config file.

ShortcutAction
Ctrl+OSave (Write Out)
Ctrl+XExit
Ctrl+WWhere-is — search forward
Ctrl+\Search & replace
Ctrl+K / Ctrl+UCut line / paste
Alt+/ or Ctrl+_Go to line number

Text Editor: vim — Deep Dive

Vim is on every Linux server by default. Mastering it makes you 10x faster than someone fumbling with nano.

The Four Modes: Normal (navigate, delete, copy — keys are COMMANDS), Insert (type text — enter via i/a/o, exit via Esc), Visual (select text via v/V/Ctrl+V), Command (run commands via :).

Interview tip: Most Asked Vim Question: “What are the vim modes and how do you exit vim?” Answer: 4 modes — Normal, Insert, Visual, Command. To exit: press Esc to ensure you’re in Normal mode, then type :q (quit), :wq (write and quit), or :q! (force quit without saving).

CommandAction
:wSave (write)
:qQuit (errors if unsaved)
:q!Force quit, discard changes
:wq or ZZSave and quit
i / aInsert before / append after cursor
o / OOpen new line below / above
h j k lLeft, Down, Up, Right
w / bNext / previous word
0 / $Start / end of line
gg / GTop / bottom of file
dd / yy / pDelete line / yank line / paste
u / Ctrl+rUndo / redo
/pattern / nSearch / next match
:%s/old/new/gReplace ALL “old” with “new” in file
:%s/old/new/gcSame, but confirm each

A solid starter ~/.vimrc for DevOps work:

syntax on
set number
set relativenumber
set expandtab          " convert tabs to spaces (critical for YAML/Python)
set tabstop=4
set shiftwidth=4
set hlsearch
set incsearch
set ignorecase
set smartcase
filetype plugin indent on
" YAML-specific (2-space indent important for k8s/Ansible)
autocmd FileType yaml setlocal ts=2 sts=2 sw=2 expandtab

Tip: Run vimtutor in your terminal right now. It’s a 30-minute interactive tutorial that ships with vim. By the end, you’ll know enough to edit any file confidently.

Quick Reference Cheatsheet

TaskCommand
View small filecat file
View large fileless file
Stream live logtail -F /var/log/app.log
First/last N lineshead -n 20 / tail -n 20
Count lineswc -l file
Sort numericallysort -n
Unique with countssort | uniq -c | sort -rn
Extract column Ncut -d: -f N or awk '{print $N}'
Write to protected file... | sudo tee /etc/file
Power editvim file
Exit vim (most asked)Esc then :wq (save) or :q! (discard)

File Permissions Ownership

Section 6 of 39 · ~2 min

Linux permissions control who can read, write, or execute every file. Getting this wrong is a security vulnerability; getting it right is foundational operations knowledge.

See also: SSH Secure Shell Critical for how permissions protect SSH keys.

The Permission Bits

Every file has three permission triplets: owner (u), group (g), others (o). Each triplet has three bits: r (read=4), w (write=2), x (execute=1).

-rwxr-xr--  1 pushkar devops 4096 Jan 1 12:00 script.sh
│└┬┘└┬┘└┬┘
│ │  │  └── others: r-- = 4 (read only)
│ │  └───── group:  r-x = 5 (read + execute)
│ └──────── owner:  rwx = 7 (full)
└────────── file type: - = regular, d = directory, l = symlink

chmod and chown

# Symbolic mode
chmod u+x script.sh          # add execute for owner
chmod go-w sensitive.conf    # remove write for group and others
chmod a+r public.html        # add read for everyone
chmod u=rwx,g=rx,o= priv/   # set exactly

# Octal mode (faster once memorised)
chmod 755 script.sh    # rwxr-xr-x
chmod 644 config.yml   # rw-r--r--
chmod 600 ~/.ssh/id_rsa       # rw------- (private key requirement)
chmod 700 ~/.ssh/             # rwx------ (SSH dir requirement)
chmod 777 /tmp/shared  # rwxrwxrwx — almost always wrong

# Ownership
chown pushkar:devops file.txt          # change owner and group
chown -R www-data:www-data /var/www/   # recursive
chgrp docker /var/run/docker.sock      # change group only

Special Bits

BitOn filesOn directories
setuid (4xxx)Run as owner’s UID (-rwsr-xr-x)No effect
setgid (2xxx)Run as owner’s GIDNew files inherit directory’s group
sticky (1xxx)No effectOnly owner/root can delete files (/tmp)
chmod u+s /usr/bin/passwd    # setuid — passwd needs root to modify /etc/shadow
chmod g+s /var/www/uploads   # setgid — all new files owned by www-data group
chmod +t /tmp                # sticky — standard on /tmp
ls -la /usr/bin/passwd       # -rwsr-xr-x (note the 's')

umask

The umask is subtracted from the default permissions when new files are created (default 666 for files, 777 for dirs).

umask          # 0022 → files get 644, dirs get 755
umask 027      # more restrictive: files 640, dirs 750

Tip: ACLs (setfacl/getfacl) extend beyond the three-owner model. Use them when you need fine-grained per-user access to a shared directory without changing ownership.

Grep Pattern Searching

Section 7 of 39 · ~1 min

grep filters lines matching a regular expression. It’s your first tool for searching logs, configs, and code at the command line. See also Piping Redirection for how grep composes with other tools. Build and test patterns first with the Regex Log Tester.

Basic Usage

grep "error" /var/log/syslog               # case-sensitive match
grep -i "error" /var/log/syslog            # case-insensitive
grep -n "timeout" app.log                  # show line numbers
grep -c "WARN" app.log                     # count matching lines
grep -v "DEBUG" app.log                    # invert — lines NOT matching
grep -r "SECRET_KEY" /etc/                 # recursive directory search
grep -l "nginx" /etc/logrotate.d/*         # only print filenames
grep -w "root" /etc/passwd                 # whole-word match only

Extended Regex (ERE)

grep -E "error|warn|crit" syslog           # alternation
grep -E "^[0-9]{4}-[0-9]{2}" app.log      # lines starting with ISO date
grep -E "failed (password|publickey)" /var/log/auth.log  # auth failures
grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" access.log  # IPs

Context and Output

grep -A 3 "FATAL" app.log    # 3 lines after each match
grep -B 2 "FATAL" app.log    # 2 lines before
grep -C 5 "FATAL" app.log    # 5 lines before and after
grep --color=auto "error" syslog    # highlight matches
grep -o "[0-9]\+\.[0-9]\+" file     # print only the matching portion

Real-World Patterns

# Find all failed SSH logins
grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn

# Count HTTP 5xx errors in nginx log
grep -E '" 5[0-9]{2} ' /var/log/nginx/access.log | wc -l

# Find config files containing a port number
grep -rn "^Listen\|^Port\|^port" /etc/nginx/ /etc/apache2/ 2>/dev/null

Awk Text Processing Powerhouse

Section 8 of 39 · ~2 min

awk is a field-oriented text processor. Think of it as a per-line program: for each line, split into fields, run your script. It handles 80% of the log-parsing and report-generation tasks you’d otherwise reach for Python for.

Basic Syntax

awk '{print $1}' file.txt          # print first field (whitespace-delimited)
awk '{print $1, $3}' file.txt      # fields 1 and 3
awk '{print NR, $0}' file.txt      # NR = record (line) number, $0 = whole line
awk 'NR==5' file.txt               # print only line 5
awk 'NF > 3' file.txt              # lines with more than 3 fields

Field Separators

awk -F: '{print $1, $3}' /etc/passwd         # colon-delimited
awk -F'\t' '{print $2}' data.tsv             # tab-delimited
awk 'BEGIN{FS=","} {print $1}' data.csv      # set FS in BEGIN block
awk -F'[,;]' '{print $1}' mixed.txt          # regex separator

Built-in Variables

VariableMeaning
$0Entire current line
$1, $2, …Fields 1, 2, …
NRCurrent record (line) number
NFNumber of fields in current record
FSInput field separator (default: whitespace)
OFSOutput field separator (default: space)
RSRecord separator (default: newline)
FILENAMECurrent filename

BEGIN and END Blocks

# Sum a column
awk '{sum += $3} END {print "Total:", sum}' sales.csv

# Count occurrences
awk '{count[$1]++} END {for (k in count) print count[k], k}' access.log

# Print header + filtered rows
awk 'BEGIN{print "Name,UID"} -F: $3 >= 1000 {print $1","$3}' /etc/passwd

Real-World Awk

# Extract top 5 IPs from nginx access log
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5

# Calculate average response time (field 7 = response_time in custom log)
awk '{sum+=$7; n++} END {printf "avg: %.2fms\n", sum/n}' app.log

# Report disk usage by filesystem (parse df output)
df -hP | awk 'NR>1 {printf "%-30s %s used\n", $6, $5}'

# Reformat CSV — swap columns 1 and 2
awk -F, 'BEGIN{OFS=","} {print $2,$1,$3}' data.csv

sed — Stream Editor

Section 9 of 39 · ~3 min

sed is a non-interactive text editor that reads input line-by-line and applies editing commands. While vim/nano are interactive, sed runs in a CI/CD pipeline, in a deployment script, in a Dockerfile — anywhere you need to programmatically modify files without a human pressing keys.

Note: Why DevOps engineers can’t live without sed:

  • CI/CD config injection: swap localhost for prod URL during deploy.
  • Templating: render config files from templates with environment-specific values.
  • Batch find-and-replace: across hundreds of files in a single command.
  • Log scrubbing: remove PII, mask passwords before sharing logs.

Basic Syntax

$ sed 's/old/new/' file           # substitute (first occurrence per line)
$ sed 's/old/new/g' file          # substitute ALL occurrences (global)
$ echo "hello world" | sed 's/world/devops/'
hello devops

By default sed prints to stdout and does NOT modify the file. Use -i to edit in-place.

Key Operations

CommandWhat it does
s/old/new/Substitute first match per line
s/old/new/gSubstitute all matches (global)
s/old/new/giGlobal, case-insensitive
/pattern/dDelete lines matching pattern
3dDelete line 3
1,5dDelete lines 1 through 5
/pattern/pPrint matching line (usually with -n)
/pattern/i\textInsert text BEFORE matching line
/pattern/a\textAppend text AFTER matching line
y/abc/xyz/Transliterate: a→x, b→y, c→z
/start/,/end/dDelete from /start/ through /end/

In-Place Editing

$ sed -i 's/DEBUG=true/DEBUG=false/' .env          # DANGEROUS — no undo!
$ sed -i.bak 's/DEBUG=true/DEBUG=false/' .env       # creates .env.bak first — RECOMMENDED

Caution: Always use -i.bak in production. A bad sed command can corrupt thousands of files instantly. The .bak safety net costs nothing.

Multiple Commands and Regex

$ sed -e 's/foo/bar/g' -e 's/baz/qux/g' file       # chain with -e
$ sed 's/foo/bar/g; s/baz/qux/g' file              # or semicolons
$ sed -E 's/([0-9]+)/NUMBER/' file                  # -E for Extended Regex
$ sed -E 's/(.+)@(.+)/User: \1, Domain: \2/' email.txt   # backreferences

Real DevOps Examples

$ sed 's/localhost/prod-server.com/g' config.yml    # swap hostname (preview)
$ sed -i 's/DEBUG=true/DEBUG=false/' .env           # disable debug in prod
$ sed -n '5,10p' logfile                            # print lines 5-10
$ sed '/^#/d' config.conf                           # remove comment lines
$ sed '/^$/d' file                                  # strip blank lines
$ sed -i '1i\#!/bin/bash' script.sh                 # add shebang at top
$ sed 's/^[[:space:]]*//' file                      # trim leading whitespace
$ sed -i 's|/old/path|/new/path|g' config           # use | delimiter for paths
$ sed -i '/^#PermitRootLogin/s/^#//' /etc/ssh/sshd_config   # uncomment a line
$ sed -i 's/\r$//' file.txt                         # convert CRLF to LF
$ sed -E 's/(password=)[^&]+/\1******/g' app.log    # mask passwords

sed vs awk — When to Use Which

TaskUseWhy
Find and replace textsedBuilt for substitution
Delete lines by patternsedOne-liner with /pattern/d
Column-based processingawkField-aware out of the box
Arithmetic / aggregationawkFull programming language
Edit config files in CI/CDsedQuick, simple substitutions
Parse log columns and countawkNative associative arrays

Real world: Kubernetes ConfigMap templating with sed:

$ sed -e "s/__ENV__/production/g" \
    -e "s/__VERSION__/v2.4.1/g" \
    -e "s|__DB_URL__|${DATABASE_URL}|g" \
    configmap.template.yaml > configmap.yaml
$ kubectl apply -f configmap.yaml

Every modern Helm chart and Kustomize patch fundamentally does this — sed is what every templating engine wraps under the hood.

Piping Redirection

Section 10 of 39 · ~2 min

The Unix philosophy: small tools that do one thing well, composed via pipes. Mastering redirection turns individual commands into data pipelines.

Redirection Operators

# stdout redirection
command > file.txt          # redirect stdout (overwrite)
command >> file.txt         # append stdout
command 2> errors.txt       # redirect stderr
command 2>&1                # merge stderr into stdout
command > file.txt 2>&1     # both stdout and stderr to file
command &> file.txt         # bash shorthand for above

# stdin redirection
command < input.txt         # feed file as stdin
command <<< "string"        # here-string (bash)
command << EOF              # heredoc
line1
line2
EOF

# /dev/null — discard
command > /dev/null 2>&1   # suppress all output
command 2>/dev/null        # suppress errors only

Pipes

# Basic pipe: stdout of left → stdin of right
ls -la | grep ".log"
cat /etc/passwd | awk -F: '{print $1}'     # inefficient, use awk directly
ps aux | grep nginx | grep -v grep

# xargs — build commands from stdin
find /var/log -name "*.log" | xargs wc -l          # count lines in all logs
find /tmp -mtime +7 | xargs rm -f                  # delete old files
cat servers.txt | xargs -I{} ssh {} "uptime"       # run on each host

# tee — split: stdout goes to both pipe and file
command | tee output.txt | next_command             # log and continue

Process Substitution

# <(cmd) — treat command's output as a file (bash)
diff <(sort file1.txt) <(sort file2.txt)
comm -23 <(sort list1.txt) <(sort list2.txt)   # lines only in list1

# >(cmd) — feed output to command as if it were a file
tee >(wc -l > line_count.txt) < big_file.txt

Practical Pipelines

# Top 10 largest files in /var
find /var -type f -printf '%s %p\n' | sort -rn | head -10

# Count unique IPs in nginx log for today
grep "$(date +%d/%b/%Y)" /var/log/nginx/access.log | awk '{print $1}' | sort -u | wc -l

# Show only failed systemd units
systemctl list-units --state=failed --no-legend | awk '{print $1}'

# Monitor a log in real time, highlight errors
tail -f /var/log/syslog | grep --line-buffered -E "error|warn|crit"

Tip: grep --line-buffered forces grep to flush after every matched line when reading from a pipe — essential for real-time monitoring. Without it, grep buffers output and you get it in chunks.

Process Basics

Section 11 of 39 · ~3 min

In Linux, everything is a process. When you run ls, that’s a process. When a Docker container starts, that’s an (isolated) process. When nginx serves a request, that’s a process too. To become a DevOps engineer you must understand processes deeply.

What is a Process?

A process is a running instance of a program. The program (executable file like /usr/bin/node) sits on disk — when you execute it, the kernel loads it into memory, gives it a unique PID (Process ID), assigns CPU time, memory, and file descriptors.

  • PID — Process ID. Unique integer. PID 1 is always init / systemd.
  • PPID — Parent Process ID. Every process (except PID 1) has a parent.
  • UID / GID — User and Group ID the process runs as. Determines permissions.
  • TTY — The terminal it’s attached to (or ? for daemons).
  • NI — Nice value (priority, -20 highest to 19 lowest).

Foreground vs Background

A foreground process owns the terminal and blocks your shell until it exits. A background process detaches (append &) so the prompt returns immediately while it keeps running.

$ sleep 300        # foreground — terminal blocked
$ sleep 300 &      # background — prompt returns immediately
[1] 24817
$ jobs
[1]+  Running                 sleep 300 &

fork() and exec()

Linux creates new processes through two system calls — the famous fork() + exec() pair.

  1. fork() — The parent process duplicates itself. The child gets a new PID; parent gets the child’s PID, child gets 0.
  2. exec() — The child replaces its memory image with a new program. Same PID, different code.
  3. wait() — Parent waits for child to finish and reaps its exit status.

When you type ls in bash: bash forks itself, the child execs /usr/bin/ls, bash waits for it to exit.

Process States

StateCodeMeaningWhen you see it
RunningRCurrently executing or runnableActive workloads
Sleeping (Interruptible)SWaiting for an event (I/O, signal, timer)Most idle daemons
Disk Sleep (Uninterruptible)DWaiting for I/O — cannot be killed even with SIGKILLNFS hang, broken disk
StoppedTSuspended by signal (SIGSTOP / Ctrl+Z)After you hit Ctrl+Z
ZombieZDead but parent hasn’t reaped exit statusBad parent process
IdleIIdle kernel threadKernel worker threads

Caution: D state is dangerous: Disk Sleep processes ignore all signals including SIGKILL. If you see lots of D state processes, your disk / NFS mount is likely broken. kill -9 won’t help.

Exploring /proc/[pid]/

The kernel exposes every process as a directory under /proc/.

$ pgrep -f nginx | head -1
1842
$ cat /proc/1842/cmdline | tr '\0' ' '
nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
$ cat /proc/1842/status | head -6
Name:   nginx
State:  S (sleeping)
Pid:    1842
PPid:   1
$ readlink /proc/1842/exe       # path to binary
/usr/sbin/nginx

Why Processes Matter in DevOps

Containers ARE processes. A Docker container is just a regular Linux process with extra isolation provided by the kernel: namespaces (separate views of PIDs, network, mounts), cgroups (CPU, memory, I/O limits), chroot/pivot_root (restricted filesystem), and seccomp/capabilities (restricted syscalls).

Interview tip: “What’s the difference between a VM and a container?” — A VM virtualizes hardware and runs a full guest kernel. A container is a regular process on the host kernel with namespaces and cgroups for isolation. Container = process. VM = virtual machine.

Process Commands

Section 12 of 39 · ~2 min

Understanding how to inspect and manage processes is essential for debugging hung services, investigating CPU/memory spikes, and safely stopping background work.

See also: systemd Services for managing processes as services.

Viewing Processes

ps aux                        # all processes, BSD syntax (a=all, u=user, x=no-tty)
ps -ef                        # all processes, POSIX syntax (-e=all, -f=full)
ps aux | grep nginx           # filter by name
ps -p 1234 -o pid,ppid,comm,etime,rss   # specific PID, custom columns

# pgrep / pkill — search/signal by name
pgrep -l nginx                # list PIDs matching "nginx"
pgrep -u www-data             # PIDs owned by www-data
pkill -HUP nginx              # send SIGHUP to all nginx processes (graceful reload)

top and htop

top              # interactive process viewer
                 # Keys: k=kill, r=renice, u=filter by user, 1=per-CPU, q=quit
                 # Press 'c' to show full command line

htop             # enhanced viewer (apt install htop)
                 # Mouse clicks work; F5=tree view, F6=sort column

Signals

kill -l                  # list all signals
kill PID                 # send SIGTERM (15) — polite shutdown request
kill -9 PID              # SIGKILL — immediate termination (no cleanup)
kill -HUP PID            # SIGHUP (1) — reload config (nginx, rsyslog)
kill -STOP PID           # pause a process
kill -CONT PID           # resume a paused process

# Sending to all instances
killall nginx            # by name
pkill -f "python worker" # by full command line regex
SignalNumberMeaning
SIGHUP1Hangup / reload config
SIGINT2Keyboard interrupt (Ctrl-C)
SIGQUIT3Quit with core dump
SIGKILL9Immediate kill (uncatchable)
SIGTERM15Graceful shutdown (default for kill)
SIGUSR1/210/12User-defined (app-specific)
SIGSTOP19Pause (uncatchable)
SIGCONT18Resume

Background Jobs and nice

# Background/foreground
command &          # start in background
jobs               # list background jobs in current shell
fg %1              # bring job 1 to foreground
bg %2              # resume stopped job 2 in background
nohup ./script.sh &  # immune to SIGHUP (survives shell logout)
disown %1          # detach job from shell (no SIGHUP on exit)

# nice / renice — CPU scheduling priority (-20=highest, 19=lowest)
nice -n 10 ./heavy_script.sh    # start with lower priority
renice 5 -p 1234                # change running process

lsof and fuser

lsof -i :80                   # what's listening on port 80
lsof -p 1234                  # all files opened by PID
lsof -u www-data              # files opened by a user
lsof +D /var/log              # all processes using files under /var/log

fuser 80/tcp                  # PID using port 80
fuser -k 80/tcp               # kill it

Tip: When a deleted file is still consuming disk space, lsof | grep deleted shows which process still holds the file descriptor open. Restart that process to reclaim the space.

systemd Services

Section 13 of 39 · ~2 min

systemd is the init system on every modern Linux distro. It manages services, mounts, timers, sockets, and devices as a dependency graph. Everything that was once a shell script in /etc/init.d is now a unit file.

Essential systemctl Commands

# Service lifecycle
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl reload nginx            # graceful config reload (if supported)
systemctl status nginx            # status + last 10 log lines

# Enable/disable at boot
systemctl enable nginx            # create symlink in wants/
systemctl disable nginx
systemctl enable --now nginx      # enable + start in one command
systemctl is-enabled nginx        # returns enabled/disabled/static

# Inspection
systemctl list-units --type=service --state=running
systemctl list-units --state=failed
systemctl cat nginx               # print unit file
systemctl show nginx              # all properties
systemctl list-dependencies nginx # tree of dependencies

Unit File Structure

Unit files live in /etc/systemd/system/ (your files) and /lib/systemd/system/ (package defaults). Your files take precedence.

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node Application
Documentation=https://github.com/yourorg/myapp
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
# After creating/editing a unit file
systemctl daemon-reload          # reload all unit files
systemctl enable --now myapp

journalctl — Reading Logs

journalctl -u nginx                    # all logs for nginx unit
journalctl -u nginx -f                 # follow in real time
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --since "2024-01-01" --until "2024-01-02"
journalctl -u nginx -n 50             # last 50 lines
journalctl -p err -u nginx            # only priority error and above
journalctl -b                         # since last boot
journalctl -b -1                      # previous boot
journalctl --disk-usage               # how much disk the journal uses
journalctl --vacuum-size=200M         # trim journal to 200 MB

systemd Timers

Timers replace cron with dependency tracking, logging via journal, and precise time expressions.

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer

[Timer]
OnCalendar=*-*-* 02:00:00   # every day at 02:00
Persistent=true              # run if missed while system was off
RandomizedDelaySec=10min     # spread load across fleet

[Install]
WantedBy=timers.target
systemctl enable --now backup.timer
systemctl list-timers                 # all timers + next trigger time

Tip: Always run systemctl daemon-reload after editing any unit file. Without it, systemd uses the cached version from memory and your changes are silently ignored.

User Management

Section 14 of 39 · ~4 min

User management is the foundation of Linux security. As a DevOps engineer, you’ll create service accounts for CI/CD pipelines, manage developer access, lock down compromised users, and configure sudo policies. Think of this like managing IAM roles in AWS — except it’s text files all the way down, and a single typo in /etc/sudoers can lock you out of your own server.

/etc/passwd and /etc/shadow

Every user account is defined in /etc/passwd. Despite the name, passwords are NOT stored here — they moved to /etc/shadow because /etc/passwd must be world-readable.

/etc/passwd has 7 colon-separated fields: username:x:UID:GID:GECOS:home_dir:login_shell

$ grep deploy /etc/passwd
deploy:x:1001:1001:Deploy User,,,:/home/deploy:/bin/bash
  • deploy — username
  • x — placeholder (the x means “look in /etc/shadow”)
  • 1001 — UID. 0 is root. 1-999 system users. 1000+ regular humans
  • 1001 — primary GID
  • /home/deploy — home directory
  • /bin/bash — login shell (set to /usr/sbin/nologin to disable interactive login)

/etc/shadow (root-only) stores password hashes plus aging policy: username:hash:last_change:min:max:warn:inactive:expire:reserved. The hash prefix $6$ = SHA-512, $y$ = yescrypt (Ubuntu 24.04 default), ! or * = locked.

Creating Users: useradd vs adduser

  • useradd — low-level, scriptable, doesn’t create home dir or set password by default. Everywhere.
  • adduser — Debian/Ubuntu’s friendly wrapper. Interactive, creates home, prompts for password. NOT on RHEL.
$ sudo useradd -m -s /bin/bash -G sudo,docker deploy
$ sudo passwd deploy

The -m flag creates the home directory, -s sets the shell, -G adds supplementary groups. Without -m, the user has no home.

usermod — Modify Existing Users

$ sudo usermod -aG docker $USER        # APPEND to group
$ sudo usermod -L olduser              # lock password
$ sudo usermod -s /usr/sbin/nologin ci-runner   # change shell
$ sudo usermod -e 2026-12-31 contractor-jay     # set expiry

Caution: The -a in -aG is non-negotiable. Running sudo usermod -G docker alice (without -a) REPLACES all of Alice’s supplementary groups with just docker. She loses sudo, adm, dialout — everything. Always: usermod -aG. The -a stands for “append”.

passwd and chage — Password and Aging

$ passwd                    # change YOUR own password
$ sudo passwd alice         # change alice's password as root
$ sudo passwd -l olduser    # lock account
$ sudo passwd -e alice      # force password change on next login

$ sudo chage -l deploy      # list aging policy
$ sudo chage -M 90 -W 7 deploy      # expire in 90 days, warn 7 days before
$ sudo chage -d 0 alice     # force password change at next login

su vs su -

  • su username — switches user, keeps your current environment.
  • su - username — switches AND runs a login shell, loading ~/.bashrc and resetting environment as if you logged in fresh.

Interview tip: “Difference between sudo su and su -?” su - requires the target user’s password and gives a full login shell. sudo su requires YOUR password (sudo authenticates the invoker), then runs su as root without a password prompt — but env vars may leak. Best practice: use sudo -i for a clean interactive root login shell.

sudo and /etc/sudoers

When you type sudo apt update: the setuid-root sudo binary reads /etc/sudoers, verifies your user is allowed, prompts for YOUR password (cached ~15 min), logs the action to /var/log/auth.log, then executes as the target user.

Sudoers rules: user_or_%group host=(runas_user) commands

root    ALL=(ALL:ALL) ALL                        # root can do anything
%sudo   ALL=(ALL:ALL) ALL                         # sudo group (Ubuntu default)
%wheel  ALL=(ALL) ALL                             # wheel group (RHEL)
deploy  ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx

Caution: NEVER edit /etc/sudoers with vim or nano directly. visudo validates syntax BEFORE saving. A single typo renders the file unparseable and breaks sudo for everyone — including root. Recovering requires single-user mode. Use sudo visudo or drop-in files in /etc/sudoers.d/ (chmod 0440).

Real DevOps Examples

# Create a developer account with sudo + docker
$ sudo useradd -m -s /bin/bash -G sudo,docker deploy && sudo passwd deploy

# Lock a former employee's account immediately
$ sudo passwd -l olduser && sudo usermod -L olduser && sudo usermod -s /usr/sbin/nologin olduser

# Service account for CI/CD — no shell, no home login
$ sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/jenkins jenkins

# Audit who has sudo right now
$ getent group sudo

# Run a single command as another user
$ sudo -u postgres psql

Real world: CI/CD Service Account — you NEVER want interactive login:

$ sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/runner \
       --create-home --comment "GitLab CI Runner" gitlab-runner
$ sudo usermod -aG docker gitlab-runner
$ sudo passwd -l gitlab-runner     # lock password — only SSH key auth

--system assigns a UID below 1000. nologin means even if SSH keys are stolen, no shell is granted. The locked password prevents su attacks. Defense-in-depth.

Group Management

Section 15 of 39 · ~2 min

Groups are how Linux scales permissions beyond individual users. Instead of granting access to 20 developers one-by-one, you create a developers group, grant the group access, and add people to the group.

/etc/group Structure

Four colon-separated fields: groupname:x:GID:member1,member2,member3

$ grep -E '^(sudo|docker|developers)' /etc/group
sudo:x:27:pushkar,alice,deploy
docker:x:998:pushkar,deploy,gitlab-runner
developers:x:1100:alice,bob,charlie

System groups: 0–999. User groups: 1000+. The member list shows users whose supplementary group this is — a user’s primary group is set by the GID in /etc/passwd.

Primary vs Supplementary Groups

  • Primary group — exactly ONE per user, defined by GID in /etc/passwd. Files the user creates are owned by this group by default.
  • Supplementary groups — zero or many, listed in /etc/group. Grant additional permissions.
$ id pushkar
uid=1000(pushkar) gid=1000(pushkar) groups=1000(pushkar),27(sudo),998(docker),1100(developers)

Managing Groups

$ sudo groupadd developers
$ sudo groupadd -g 1500 deploy          # specific GID
$ sudo groupmod -n devops developers     # rename
$ sudo groupdel old-team                 # delete (refuses if it's anyone's primary group)

$ sudo gpasswd -a alice developers       # add member
$ sudo gpasswd -d alice developers       # remove member
$ sudo gpasswd -A bob developers         # make bob a group admin
$ sudo usermod -aG docker pushkar        # the standard "add to group" command

Caution: usermod -G without -a is destructive — it REPLACES all supplementary groups. Always usermod -aG. Mnemonic: “a is for ADD, without a it ANNIHILATES.”

newgrp — Switch Active Primary Group

If you were just added to a group, your current shell doesn’t know yet. newgrp spawns a subshell with the named group as primary:

$ sudo usermod -aG docker pushkar
$ docker ps        # permission denied — current session predates the change
$ newgrp docker    # spawn subshell with docker group active
$ docker ps        # works!

Common System Groups

GroupPurpose
sudoDebian/Ubuntu: members can use sudo
wheelRHEL/CentOS equivalent of sudo
dockerRun docker without sudo (effectively root — be careful)
www-dataApache/Nginx run as this user
admRead access to log files in /var/log/
systemd-journalRead journalctl logs without sudo
diskRaw disk access — DANGEROUS

Real world: Shared Deployment Directory with setgid — the setgid bit makes new files inherit the directory’s group:

$ sudo chown -R root:deploy /opt/app
$ sudo chmod -R 2775 /opt/app       # the 2 = setgid bit
$ sudo chmod g+s /opt/app

Now any file created inside /opt/app is owned by the deploy group, not the creator’s primary group — so all team members can read/write each other’s files. This is how /srv/web/, /opt/app/, and shared CI artifact directories should be configured.

Package Managers

Section 16 of 39 · ~1 min

Package managers are your software distribution layer. Each distro family has its own; as a DevOps engineer you’ll touch all of them.

apt (Debian / Ubuntu)

# Update package lists
sudo apt update

# Install / remove
sudo apt install nginx git curl
sudo apt install -y nginx         # non-interactive (scripts/CI)
sudo apt remove nginx             # remove but keep config
sudo apt purge nginx              # remove + delete config files
sudo apt autoremove               # remove orphaned dependencies

# Upgrade
sudo apt upgrade                  # upgrade installed packages
sudo apt full-upgrade             # upgrade + handle dependency changes
sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y   # in scripts

# Search and inspect
apt search "text editor"
apt show nginx
apt list --installed | grep nginx
dpkg -L nginx                     # list files installed by package
dpkg -S /usr/sbin/nginx           # which package owns a file

# Pinning — hold a package at current version
sudo apt-mark hold nginx
sudo apt-mark showhold

dnf / yum (RHEL / Rocky / Fedora)

sudo dnf install nginx
sudo dnf remove nginx
sudo dnf update
sudo dnf search nginx
sudo dnf info nginx
sudo rpm -ql nginx                # list files in installed rpm
sudo rpm -qf /usr/sbin/nginx      # which rpm owns a file

# Repos
sudo dnf repolist
sudo dnf config-manager --enable epel

Alpine apk (containers / musl-based)

apk update
apk add nginx curl bash
apk del nginx
apk search nginx
apk info nginx

# Minimal Dockerfile example
FROM alpine:3.19
RUN apk add --no-cache nginx \
    && rm -rf /var/cache/apk/*

Tip: In Dockerfiles always apt-get update && apt-get install -y pkg in a single RUN layer. Splitting them means the cached update layer can have stale package lists when your install layer is rebuilt days later — causing “package not found” errors.

Disk Management

Section 17 of 39 · ~4 min

Disk management is one of the most frequent on-call problems in DevOps. “Server is down” usually means CPU pegged, OOM killer killing processes, or — most often — disk full. You must be fluent with df, du, partitioning tools, and /etc/fstab.

df — Disk Free

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   25G   23G  53% /
/dev/sda2       100G   89G   11G  90% /var

$ df -hT          # show filesystem TYPE — critical for choosing resize tool
$ df -i           # INODES — the silent killer
Filesystem      Inodes   IUsed   IFree IUse% Mounted on
/dev/sda2     52428800 52428799      1  100% /var   <- INODE EXHAUSTED!

du — Disk Usage (find WHO is eating space)

$ du -sh /var/log                          # total size of one directory
$ du -h --max-depth=1 /var | sort -h       # top-level breakdown, sorted
$ du -hac --max-depth=2 . | sort -rh | head -20    # "what's eating my disk"
$ sudo du -ah / 2>/dev/null | sort -rh | head -20  # whole system scan (slow)

Partitioning: fdisk and parted

$ sudo fdisk -l                  # list all disks and partitions
$ sudo fdisk /dev/sdb            # interactive (n=new, p=print, d=delete, w=WRITE)

$ sudo parted -l
$ sudo parted /dev/sdb mklabel gpt
$ sudo parted /dev/sdb mkpart primary ext4 0% 100%

gdisk is the GPT-only cousin of fdisk. Use it when fdisk complains about a GPT disk.

lsblk and blkid — Inspection

$ lsblk            # tree of block devices and mountpoints
$ lsblk -f         # with FS type and UUID — best "what's on this disk?" command
$ sudo blkid       # UUIDs for fstab entries

mount / umount

$ sudo mount /dev/sdb1 /mnt/data
$ sudo mount -t ext4 /dev/sdb1 /mnt/data        # specify FS type
$ sudo mount -o ro /dev/sdb1 /mnt/data          # read-only
$ sudo mount -o remount,rw /                    # remount root rw (rescue trick)
$ sudo umount /mnt/data
$ sudo umount -l /mnt/data                      # lazy unmount if "device is busy"
$ findmnt                                        # pretty tree view of all mounts

/etc/fstab — Persistent Mounts

Format: device mountpoint fstype options dump pass

$ cat /etc/fstab
UUID=a3f2e1b4-...  /        ext4    defaults,noatime                0  1
UUID=b1c2d3e4-...  /data    xfs     defaults,nofail                 0  2
tmpfs              /tmp     tmpfs   defaults,size=2G,noexec         0  0
/swapfile          none     swap    sw                              0  0
nfs.internal:/exports/shared  /mnt/nfs  nfs  defaults,_netdev,soft,timeo=30  0  0

$ sudo mount -a                # mount everything in fstab — TEST before reboot!
$ sudo findmnt --verify         # sanity-check fstab without mounting

The pass column controls fsck order at boot: 0=skip, 1=root only, 2=other filesystems.

Caution: A typo in /etc/fstab (wrong UUID, missing mountpoint, bad option) drops the system into emergency mode on next reboot. Always test with sudo mount -a after editing. For non-critical mounts add nofail; for network filesystems add _netdev. To recover from a boot hang: single-user mode, mount -o remount,rw /, fix fstab, reboot.

Mounting by UUID (Best Practice)

Device names like /dev/sdb can change across reboots. UUIDs are baked into the filesystem and never change. Always use UUIDs in fstab.

$ sudo blkid /dev/sdb1
/dev/sdb1: UUID="b1c2d3e4-..." TYPE="xfs"
$ echo 'UUID=b1c2d3e4-... /data xfs defaults,nofail 0 2' | sudo tee -a /etc/fstab

Swap

$ sudo fallocate -l 2G /swapfile
$ sudo chmod 600 /swapfile          # required — else mkswap refuses
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ swapon --show
$ free -h                            # confirm swap appears
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab   # persist

LVM intro

LVM (Logical Volume Manager) adds a flexible layer between partitions and filesystems: PV (Physical Volume), VG (Volume Group), LV (Logical Volume). You can resize LVs without unmounting, take snapshots, and span multiple disks. Ubuntu Server defaults to LVM.

Real world: “No space left on device” but df shows free space? You’re out of inodes, not bytes. Every file/dir consumes one inode regardless of size. Diagnose with df -i — if any IUse% is 100%, that’s it. Common culprits: session files, mail queues, cache dirs with millions of tiny files. Fix: delete the small files; xfs allocates inodes dynamically and avoids this entirely.

Interview tip: “Disk is full, what do you do?” (1) df -h — which FS, how full. (2) df -i — rule out inode exhaustion. (3) du -h --max-depth=1 / | sort -h — find the heavy directory. (4) Check usual suspects: /var/log, /var/lib/docker (docker system prune), journal (journalctl --vacuum-size=500M). (5) Check deleted-but-open files: sudo lsof | grep deleted. (6) Extend the volume (cloud: resize EBS → growpartresize2fs/xfs_growfs).

Filesystems

Section 18 of 39 · ~4 min

A filesystem is the contract between raw blocks on a disk and the files you see. Choosing the right one matters: ext4 for general Linux, xfs for big files and parallel I/O, btrfs/zfs for snapshots, tmpfs for ephemeral RAM-backed storage, overlayfs for Docker layers.

The Main Linux Filesystems

  • ext4 — Default on Ubuntu/Debian. Mature, journaled, max file 16 TB. Resize online with resize2fs.
  • xfs — Default on RHEL/Rocky/Amazon Linux. Excellent for large files and parallel I/O. Allocates inodes dynamically. Can grow online (xfs_growfs) but cannot shrink.
  • btrfs — Copy-on-write, snapshots, subvolumes, built-in RAID, compression. Default on Fedora Workstation.
  • zfs — Enterprise-grade. Pools, end-to-end checksums, snapshots, send/receive replication. Not in mainline kernel (license clash) — install zfsutils-linux.
  • NTFS / FAT32 / exFAT — Windows/removable media. FAT32 has 4 GB per-file limit; exFAT removes it.
  • tmpfs — RAM-backed. Files vanish on reboot. Used for /tmp, /run, /dev/shm.
  • overlayfs — The magic behind Docker. Stacks a writable upper layer over read-only lower layers under /var/lib/docker/overlay2/.
  • proc, sysfs, devtmpfs — Virtual filesystems exposing kernel state.

Comparison Table

Featureext4xfsbtrfszfs
Max file size16 TB8 EB16 EB16 EB
JournalingYesYesCoWCoW + ZIL
Copy-on-WriteNoReflinksYesYes
SnapshotsNo (need LVM)No (need LVM)NativeNative
Online growYes (resize2fs)Yes (xfs_growfs)YesYes
Online shrinkNo (offline)No (never)YesNo
Built-in RAIDNoNoYesYes (RAID-Z)
CompressionNoNoYesYes
ChecksumsMetadata onlyMetadata onlyData + metadataData + metadata
Inode allocationStatic at mkfsDynamicDynamicDynamic
Default onUbuntu, DebianRHEL, Amazon LinuxFedora WS, SUSETrueNAS, Proxmox

Creating and Resizing Filesystems

$ sudo mkfs.ext4 /dev/sdb1
$ sudo mkfs.ext4 -L data -m 1 /dev/sdb1       # label "data", 1% reserved for root
$ sudo mkfs.xfs -L data -f /dev/sdb1
$ sudo mkfs.vfat -F 32 -n USB /dev/sdb1

# ext4 grow (online or offline)
$ sudo resize2fs /dev/sdb1            # grow to fill partition
# ext4 shrink (MUST unmount + fsck first)
$ sudo umount /data && sudo e2fsck -f /dev/sdb1 && sudo resize2fs /dev/sdb1 20G
# xfs grow ONLY (online, pass MOUNTPOINT not device)
$ sudo xfs_growfs /data

fsck — Filesystem Integrity

Caution: NEVER run fsck on a mounted filesystem — it corrupts it. Always unmount first, or boot from rescue media for the root filesystem.

$ sudo umount /dev/sdb1
$ sudo fsck.ext4 -f /dev/sdb1          # -f forces check even if "clean"
$ sudo fsck -y /dev/sdb1               # auto-answer "yes" to repairs
$ sudo xfs_repair /dev/sdb1            # xfs uses xfs_repair, not fsck
$ sudo xfs_repair -n /dev/sdb1         # dry run — report only

Inodes — Deep Dive

An inode is a fixed-size record storing all metadata for a file except its name: type, permissions, owner UID/GID, size, timestamps (atime/mtime/ctime), link count, and pointers to data blocks. The filename lives in the directory entry — a (name → inode number) mapping. This single design choice explains hard links, symlinks, rename, and “deleted but still open” files.

$ stat /etc/passwd      # full inode metadata
$ ls -i /etc/passwd     # show inode number
$ df -i /               # inode usage for a filesystem

File operations in terms of inodes:

  • Rename / mv (same FS): directory entry name changes; inode untouched — instant even for huge files.
  • Hard link (ln): a second directory entry pointing to the same inode. Link count increments. Cannot cross filesystems. Cannot link directories.
  • Symbolic link (ln -s): a tiny file with its own inode whose content is a path string. Can cross filesystems, can point to directories, can dangle.
  • Delete (rm): removes the directory entry and decrements link count. Inode and data freed only when link count reaches 0 and no process has the file open.

Interview tip: Hard link vs Symlink (inode perspective). Hard link: a second directory entry pointing to the same inode — both names are first-class, same FS only, can’t link directories, survives deletion of one name while link count > 0. Symlink: a separate file whose data is a textual path, resolved at access time, can cross filesystems, can link directories, dangles if target removed. In ls -l: a hard link looks like a normal file with link count > 1; a symlink shows l and -> target.

Real world: Resizing an EBS volume on EC2 — three steps, miss one and df shows no change. (1) Modify the EBS volume size in the AWS console/CLI. (2) Grow the partition: sudo growpart /dev/nvme0n1 1. (3) Grow the filesystem: ext4 sudo resize2fs /dev/nvme0n1p1, xfs sudo xfs_growfs -d /. Entire process is online — no reboot, no unmount.

Network Commands

Section 19 of 39 · ~2 min

Networking knowledge separates engineers who can debug production incidents from those who can’t. These tools let you inspect interfaces, test connectivity, trace traffic, and diagnose service failures.

ip — Modern Network Configuration

# Addresses
ip addr show                   # all interfaces and IPs
ip addr show eth0              # specific interface
ip addr add 192.168.1.10/24 dev eth0   # add IP (temporary)
ip addr del 192.168.1.10/24 dev eth0

# Routes
ip route show                  # routing table
ip route add default via 192.168.1.1   # add default gateway
ip route get 8.8.8.8           # which route is used to reach an IP

# Links
ip link show
ip link set eth0 up
ip link set eth0 down

ss — Socket Statistics (replaces netstat)

ss -tlnp          # TCP (-t) listening (-l) numeric (-n) with process (-p)
ss -ulnp          # UDP listening
ss -tnp           # all established TCP connections
ss -s             # summary statistics

# Common patterns
ss -tlnp | grep :80        # what's on port 80
ss -tnp state ESTABLISHED  # only established connections
ss -tnp dst 10.0.0.5       # connections to a specific host

dig — DNS Diagnostics

dig example.com              # A record (default)
dig example.com MX           # mail exchange records
dig example.com NS           # nameservers
dig example.com TXT          # TXT records (SPF, DKIM, etc.)
dig @8.8.8.8 example.com    # query specific DNS server
dig +short example.com       # just the IPs
dig -x 93.184.216.34         # reverse DNS lookup
dig +trace example.com       # trace full resolution chain

curl — HTTP Testing

curl https://api.example.com/health          # GET request
curl -I https://example.com                  # headers only (HEAD)
curl -X POST -H "Content-Type: application/json" \
     -d '{"key":"value"}' https://api.example.com/data
curl -o /tmp/file.tar.gz https://example.com/file.tar.gz  # download
curl -L https://short.url/abc               # follow redirects
curl -w "%{http_code} %{time_total}s\n" -o /dev/null -s https://example.com  # timing
curl --resolve example.com:443:10.0.0.5 https://example.com  # test before DNS propagation

Other Essential Network Tools

# ping / traceroute / mtr
ping -c 4 8.8.8.8
traceroute 8.8.8.8
mtr 8.8.8.8           # real-time traceroute (apt install mtr)

# nc (netcat) — the network Swiss Army knife
nc -zv host 22             # port scan / test connectivity
nc -l 9000                 # listen on port 9000
echo "PING" | nc -u host 514   # UDP test

# tcpdump — packet capture
sudo tcpdump -i eth0 port 80 -w capture.pcap     # capture to file
sudo tcpdump -i any host 10.0.0.5 and tcp        # filter by host+protocol
sudo tcpdump -r capture.pcap -A                  # read and print ASCII

# watch — repeat a command
watch -n 2 ss -tlnp        # refresh every 2 seconds

Tip: ss -tlnp is your first command when a service “can’t start” — it shows exactly which ports are already in use and by which PID. Then systemctl status that PID to find the conflict.

Network Configuration

Section 20 of 39 · ~2 min

Commands you can run, but understanding the actual configuration files is the next level. This section covers Ubuntu 24.04’s real config files — production servers, EC2 instances, on-prem bare metal all apply.

/etc/hosts — Local Hostname Resolution

Consulted before DNS (per nsswitch.conf order). The lifeline for local development and quick overrides.

$ cat /etc/hosts
127.0.0.1       localhost
127.0.1.1       devops-laptop
10.0.5.20       db.internal    db
10.0.5.30       redis.internal redis
::1             localhost ip6-localhost ip6-loopback

Tip: Production cutover testing — add the new server’s IP to /etc/hosts and test with curl/browser before changing DNS. Risk-free validation.

/etc/resolv.conf — DNS Resolvers

$ cat /etc/resolv.conf
nameserver 8.8.8.8
nameserver 1.1.1.1
search prod.example.com internal
options timeout:2 attempts:2

Caution: On Ubuntu 24.04, /etc/resolv.conf is usually a symlink to /run/systemd/resolve/stub-resolv.conf managed by systemd-resolved. Direct edits get overwritten. Use Netplan or resolvectl instead:

$ resolvectl status
$ sudo resolvectl dns eth0 1.1.1.1 8.8.8.8

/etc/nsswitch.conf — Lookup Order

$ grep ^hosts /etc/nsswitch.conf
hosts: files mdns4_minimal [NOTFOUND=return] dns
# files → /etc/hosts first, mdns4 → .local addresses, dns → /etc/resolv.conf

Netplan — Modern Ubuntu (Primary)

Ubuntu 18.04+ default. YAML-based frontend for NetworkManager or systemd-networkd.

# /etc/netplan/01-static.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: false
      addresses:
        - 192.168.1.50/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [8.8.8.8, 1.1.1.1]
        search: [prod.example.com]
      mtu: 1500
$ sudo netplan try          # test for 120s, auto-rollback if SSH dies
$ sudo netplan apply        # commit changes

Caution: YAML is strict — 2-space indentation, NO TABS. Wrong indent = network down. Always use netplan try first on remote boxes.

NetworkManager (nmcli) — Desktop/Laptop

$ nmcli device status
$ nmcli con show                              # all profiles
$ nmcli dev wifi connect "MySSID" password "secret"
$ nmcli con mod eth0 ipv4.addresses 10.0.1.50/24 ipv4.gateway 10.0.1.1 ipv4.method manual

systemd-networkd and Hostname

Cloud servers usually use systemd-networkd (lightweight). Netplan generates its config under /run/systemd/network/.

$ networkctl status
$ hostnamectl
$ sudo hostnamectl set-hostname prod-web-01

Tip: After a hostname change, also update /etc/hosts (especially the 127.0.1.1 line). Otherwise sudo warns “unable to resolve host”.

Bonding and VLANs

# Netplan: active-backup bond for HA
network:
  version: 2
  bonds:
    bond0:
      interfaces: [eth0, eth1]
      addresses: [10.0.1.50/24]
      parameters:
        mode: active-backup
        primary: eth0
  ethernets:
    eth0: {}
    eth1: {}

Common bonding modes: active-backup (failover), balance-rr (round robin), 802.3ad (LACP, needs switch support). VLANs tag one physical NIC into multiple logical networks (802.1Q).

Note: Cloud reality check: 95% of production cloud instances (AWS EC2, GCP, Azure VM) take their IP via DHCP / cloud-init — static IPs are rarely set at the OS level because VPC subnets and ENIs handle assignment, and auto-scaling groups use dynamic IPs. But for on-prem, hybrid, edge devices, bastion hosts, and NFS servers, static IP is a must. Know both perspectives for interviews.

Interview tip: “Netplan vs NetworkManager?” — Netplan is a YAML frontend. The real backend is NetworkManager (desktop) or systemd-networkd (server). Ubuntu cloud images default to renderer: networkd.

SSH Secure Shell Critical

Section 21 of 39 · ~3 min

SSH is how you access every cloud VM, container host, and remote server. Beyond basic login, it gives you encrypted tunnels, file transfer, and agent forwarding. Misconfiguring it creates security holes; understanding it properly lets you build zero-trust access patterns.

See also: File Permissions Ownership — SSH enforces strict permission checks on key files. Going deeper on networking? See Networking for DevOps.

Key-Based Authentication

# Generate a key pair (Ed25519 is faster and more secure than RSA)
ssh-keygen -t ed25519 -C "pushkar@work" -f ~/.ssh/id_ed25519

# Copy public key to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# or manually:
cat ~/.ssh/id_ed25519.pub | ssh user@server 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'

# Permission requirements (SSH will refuse with wrong perms)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519         # private key
chmod 644 ~/.ssh/id_ed25519.pub     # public key
chmod 600 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/known_hosts

# Start ssh-agent and load key
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l                          # list loaded keys

Note: The public key body (the long string after ssh-ed25519) is base64-encoded binary. If you ever need to inspect or transform an encoded blob like this — or a base64 secret in a Kubernetes manifest — the Base64 Encoder/Decoder is handy.

~/.ssh/config

The client config file eliminates typing long hostnames and options every time:

# ~/.ssh/config

Host bastion
    HostName 203.0.113.10
    User ec2-user
    IdentityFile ~/.ssh/aws-key.pem
    ServerAliveInterval 60

Host prod-db
    HostName 10.0.1.50             # private IP
    User ubuntu
    ProxyJump bastion              # tunnel through bastion
    IdentityFile ~/.ssh/id_ed25519

Host dev-*
    User developer
    IdentityFile ~/.ssh/dev-key
    StrictHostKeyChecking no       # only for throwaway dev VMs
    UserKnownHostsFile /dev/null
ssh bastion            # uses config above
ssh prod-db            # automatically tunnels via bastion

SSH Tunnels

# Local port forwarding: access remote service on your local port
ssh -L 5432:db-host:5432 bastion     # connect to postgresql via bastion
# Now: psql -h localhost -p 5432

# Remote port forwarding: expose local port on the remote server
ssh -R 8080:localhost:3000 server    # server:8080 → your localhost:3000

# Dynamic (SOCKS proxy): route browser traffic through the tunnel
ssh -D 1080 bastion                  # configure browser to use SOCKS5 localhost:1080

# Background / non-interactive
ssh -fNL 5432:db:5432 bastion       # -f = background, -N = no command

scp and rsync

# scp — simple copy
scp file.txt user@server:/tmp/
scp -r user@server:/var/www/ ./backup/
scp -P 2222 file.txt user@server:/tmp/    # custom port

# rsync — incremental sync (preferred for large transfers)
rsync -avz /local/dir/ user@server:/remote/dir/    # sync to remote
rsync -avz --delete /src/ /dst/                     # mirror (delete extra)
rsync -avz --exclude '.git' --exclude 'node_modules' ./ server:/opt/app/
rsync -e "ssh -i ~/.ssh/key.pem" /file user@host:/dest/

sshd — Server Configuration Hardening

sudo nano /etc/ssh/sshd_config
# /etc/ssh/sshd_config (key hardening settings)
Port 2222                          # non-standard port (minor obscurity)
PermitRootLogin no                 # never allow root login
PasswordAuthentication no          # key-only (most important setting)
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
AllowUsers pushkar deploy
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding yes             # needed for tunnels
sudo sshd -t                        # test config syntax
sudo systemctl reload sshd          # apply without dropping connections

Tip: Always test your sshd config with sshd -t and keep an existing SSH session open while applying changes. If you lock yourself out of a cloud VM, you’ll need console access or a rescue snapshot.

Bash Fundamentals

Section 22 of 39 · ~2 min

Bash (Bourne Again SHell) is the default interactive shell on Ubuntu, Debian, RHEL derivatives, and most Linux distros. As a DevOps engineer, bash is your daily driver — every CI pipeline, every Dockerfile RUN, every cron job, every systemd ExecStart is some flavour of shell glue.

Note: Ubuntu’s /bin/sh is actually dash (Debian Almquist Shell) — a POSIX-compliant shell with no bash extensions. Scripts written assuming bash but invoked as sh script.sh will fail on [[ ]], arrays, or $(()) arithmetic. Always be explicit about your shebang.

The Shebang and Running Scripts

#!/bin/bash            # Absolute path — works on virtually every Linux box
#!/usr/bin/env bash    # Looks up bash via PATH — portable to macOS/BSD
CommandShellNeeds +x?Variables persist in parent?
./script.shSubshell (from shebang)YesNo
bash script.shNew bash subshellNoNo
source script.sh or . script.shCurrent shellNoYes

Variables

The cardinal rule: no spaces around the = sign. Always quote when expanding to handle spaces and globs safely.

NAME="Pushkar"
GREETING="Hello, $NAME"
echo "${GREETING}!"                   # braces disambiguate variable name

# Parameter expansion
echo "${UNSET_VAR:-default}"         # use default if unset/empty
echo "${MAYBE:=set_now}"             # assign default if unset (sticky)
echo "${REQUIRED:?must be set}"      # error and exit if unset
echo "${NAME:+exists}"               # expand to 'exists' only if set

FILE="backup.tar.gz"
echo "${#FILE}"                      # 13  (length)
echo "${FILE%.gz}"                   # backup.tar  (strip shortest suffix)
echo "${FILE%%.*}"                   # backup       (strip longest suffix)
echo "${FILE/tar/zip}"               # backup.zip.gz (replace first match)

Positional Parameters

VariableMeaning
$0Script name
$1$9First nine positional arguments
$#Number of arguments
"$@"All args, each preserved as one quoted word
$$PID of current shell
$?Exit code of last command (0 = success)
$!PID of last background process

Bash Strict Mode

Put this at the top of every serious script:

set -euo pipefail
IFS=$'\n\t'
FlagWhat it does
-e (errexit)Exit immediately when any command returns non-zero
-u (nounset)Treat references to unset variables as errors
-o pipefailPipeline exit code is the rightmost non-zero status
IFS=$'\n\t'Restrict word-splitting to newlines/tabs

Arithmetic and Input

echo $((2 + 3 * 4))            # 14  — recommended form
x=5
((x++))                         # x is now 6
echo "scale=2; 7/3" | bc       # floating point: 2.33

read -p "Your name: " name
read -s -p "Password: " pass   # silent input
read -t 10 -p "Quick: " ans    # 10-second timeout

Tip: Use source only for shell configuration files (.bashrc, env loaders). Use ./script.sh for everything else — sourcing arbitrary scripts can pollute your interactive shell if they call exit or set -e.

Bash Control Flow

Section 23 of 39 · ~3 min

Control flow in bash looks unfamiliar at first — then, fi, do, done, double brackets. It’s because the shell parses commands by whitespace, so keywords need clear separators. Once you internalize the patterns, complex automation becomes muscle memory.

if / elif / else

if [[ "$USER" == "root" ]]; then
    echo "Running as root"
elif [[ "$USER" == "pushkar" ]]; then
    echo "Hi Pushkar"
else
    echo "Unknown user: $USER"
fi

# if on any command's exit code
if grep -q "ERROR" /var/log/syslog; then
    echo "Errors found in syslog"
fi

if systemctl is-active --quiet nginx; then
    echo "nginx is running"
fi

Tip: Use [[ ]] in bash scripts always. Use [ ] only when writing a portable /bin/sh script. Inside [[ ]], do not quote the right side of =~ — quoting turns the regex into a literal string.

Comparison Operators

Integer ([[ ]] or (( )))String ([[ ]])Meaning
-eq==Equal
-ne!=Not equal
-lt<Less than
-gt>Greater than
-z STRString is empty
-n STRString is non-empty
STR =~ REGEXString matches ERE regex

File Test Operators

OperatorTrue if…
-e FILEFile exists (any type)
-f FILERegular file
-d DIRDirectory
-r FILEReadable
-w FILEWritable
-x FILEExecutable
-s FILEExists and non-empty

case Statement

case "$action" in
    start)
        echo "Starting..."
        ;;
    stop|halt)
        echo "Stopping..."
        ;;
    *.log)
        echo "That looks like a log file"
        ;;
    *)
        echo "Unknown action: $action"
        exit 1
        ;;
esac

Loops

# for-in with a list
for env in dev staging prod; do
    echo "Deploying to $env"
done

# Brace expansion range
for i in {1..10}; do echo "loop $i"; done

# Glob iteration — always quote the variable
for f in /var/log/*.log; do
    [[ -f "$f" ]] || continue
    echo "Processing $f"
done

# C-style for
for ((i=0; i<5; i++)); do
    echo "index $i"
done

# while — read line by line (safest pattern)
while IFS= read -r line; do
    echo "> $line"
done < /etc/hosts

# until with retry
attempts=0
until curl -sf https://api.example.com/health > /dev/null; do
    ((attempts++))
    [[ $attempts -ge 5 ]] && { echo "giving up"; exit 1; }
    sleep 2
done

Warning: for f in $(ls *.log) is a classic anti-pattern. If filenames have spaces, they get split. Use for f in *.log (glob directly) instead.

Ten Practical Examples

1. Retry with exponential backoff:

URL="$1"
MAX_TRIES=5
DELAY=1
for ((try=1; try<=MAX_TRIES; try++)); do
    if curl -sfL "$URL" -o /tmp/out; then
        echo "Success on attempt $try"
        exit 0
    fi
    echo "Attempt $try failed, sleeping ${DELAY}s..."
    sleep "$DELAY"
    DELAY=$((DELAY * 2))
done
echo "All $MAX_TRIES attempts failed" >&2
exit 1

2. Wait for a port to be open (Docker Compose pattern):

HOST=db
PORT=5432
until (echo > /dev/tcp/"$HOST"/"$PORT") 2>/dev/null; do
    echo "Waiting for $HOST:$PORT..."
    sleep 1
done
echo "$HOST:$PORT is up"

3. Read a CSV line-by-line:

while IFS=, read -r name email role; do
    [[ "$name" == "name" ]] && continue   # skip header
    echo "User: $name <$email> ($role)"
done < users.csv

4. Interactive menu:

while true; do
    cat <<MENU
1) Show uptime
2) Show disk
q) Quit
MENU
    read -p "Choice: " ch
    case "$ch" in
        1) uptime ;;
        2) df -h ;;
        q|Q) break ;;
        *) echo "Invalid choice" ;;
    esac
done

Interview tip: A common screening question is “write a script to find the 5 largest files in a directory tree.” Answer: find /path -type f -printf '%s %p\n' | sort -rn | head -5.

Bash Functions and Advanced

Section 24 of 39 · ~3 min

Once your scripts grow past 50 lines, you need functions, arrays, and disciplined error handling. This section covers the constructs that separate “shell script someone hacked together” from “production automation.”

Function Definitions

# Style 1 — POSIX, works in /bin/sh too
greet() {
    echo "Hello, $1!"
}

# Style 2 — bash keyword form
function greet() {
    echo "Hello, $1!"
}

greet Pushkar     # Hello, Pushkar!

Function Arguments and Return Values

Inside a function, $1, $2, $@, $# refer to the function’s arguments — not the script’s. Bash return N only sets the exit status (0-255); to return data, echo it and capture with $():

get_kernel() { uname -r; }            # write to stdout
is_root()    { [[ $EUID -eq 0 ]]; }   # last command's exit status IS the return

KERNEL=$(get_kernel)
if is_root; then echo "Running as root"; fi

Local Variables

By default all bash variables are global. local (only valid inside functions) keeps a variable scoped:

counter=0
bump() {
    local counter=100    # shadows the global
    counter=$((counter + 1))
    echo "inside: $counter"
}
bump                # inside: 101
echo "outside: $counter"   # outside: 0

Caution: local var=$(some_cmd) masks the exit code of some_cmd because local itself succeeds. Under set -e, failures silently disappear. Safe idiom: declare first, assign second.

local var
var=$(some_cmd)   # now set -e catches a failure here

Arrays

servers=(web01 web02 db01 cache01)
echo "${servers[0]}"       # web01 (zero-indexed)
echo "${servers[@]}"       # all elements as separate words
echo "${#servers[@]}"      # 4 (length)
echo "${!servers[@]}"      # 0 1 2 3 (indices)
servers+=(db02 db03)         # append

for s in "${servers[@]}"; do echo "Server: $s"; done
echo "${servers[@]:1:2}"     # slice — from index 1, take 2
mapfile -t users < <(cut -d: -f1 /etc/passwd)   # read command output into array

Associative Arrays (Hash Maps)

Bash 4+ via declare -A. Ubuntu 24.04 ships bash 5.2:

declare -A env_url
env_url[dev]="https://dev.example.com"
env_url[prod]="https://example.com"
echo "${env_url[prod]}"
for key in "${!env_url[@]}"; do
    printf '%-10s -> %s\n' "$key" "${env_url[$key]}"
done
if [[ -v env_url[qa] ]]; then echo "qa defined"; else echo "qa missing"; fi

Exit Codes and Convention

CodeConvention
0Success
1General error (catch-all)
2Misuse / invalid arguments
126Command found but not executable
127Command not found
128 + NKilled by signal N (130 = Ctrl-C, 137 = SIGKILL)
255Exit code out of range

Note: Follow the exit-code convention. CI systems (Jenkins, GitLab, GitHub Actions) only know “did the job succeed or fail” by exit code. If your script exit 0s on error, the pipeline shows green while the deploy actually failed. Always non-zero exit on any failure path.

trap — Cleanup on Exit/Signal

#!/usr/bin/env bash
set -euo pipefail
TMPDIR=$(mktemp -d)
cleanup() {
    local rc=$?
    echo "Cleaning up (exit=$rc)..."
    rm -rf "$TMPDIR"
    return "$rc"
}
trap cleanup EXIT                          # EXIT = whenever the script ends
trap 'echo "Interrupted"; exit 130' INT TERM   # INT = Ctrl-C, TERM = kill
echo "working in $TMPDIR"

getopts — Argument Parsing

while getopts "e:v:dh" opt; do
    case "$opt" in
        e) ENV="$OPTARG" ;;
        v) VERSION="$OPTARG" ;;
        d) DRY_RUN=1 ;;
        h) usage ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))
[[ -z "$ENV" || -z "$VERSION" ]] && { echo "-e and -v required" >&2; exit 2; }

Heredocs and Here-Strings

NAME=Pushkar
cat <<EOF
Hello, $NAME
Today is $(date +%F)
EOF

cat <<'EOF'
Literal $NAME and $(date) — nothing expands here
EOF

grep -E '^[A-Z]' <<< "$LINE"     # here-string — feed a single string as stdin

The Source-Library Pattern

# File: lib/common.sh
log()  { printf '[%s] %s\n' "$(date +%FT%T)" "$*"; }
warn() { log "WARN: $*" >&2; }
die()  { log "FATAL: $*" >&2; exit 1; }
require_root() { [[ $EUID -eq 0 ]] || die "must run as root"; }

# File: deploy.sh
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
require_root
log "Starting deploy"

Tip: Install shellcheck (sudo apt install shellcheck) and run it on every script before committing. It catches ~80% of bash bugs — quoting issues, unset variables, deprecated syntax, common SC2086 word-splitting traps. Hook it into your CI.

Real DevOps Bash Scripts

Section 25 of 39 · ~6 min

Below are complete, working scripts you would actually run in production. Each uses strict mode, functions, error handling, and proper logging. They cover the automation patterns you will encounter repeatedly as a DevOps engineer.

Real world: Every script here is based on patterns used in production: locking, retries, Slack alerts, S3 upload, rollback on health-check fail. Tweak the paths and service names for your environment.

Script 1: server_health_check.sh

One-shot dashboard of CPU, memory, disk, and top processes. Exits non-zero if any threshold is breached so it can drive alerting from cron.

#!/usr/bin/env bash
# server_health_check.sh — quick host health snapshot
set -euo pipefail
IFS=$'\n\t'

CPU_WARN=80
MEM_WARN=85
DISK_WARN=80
EXIT_CODE=0

hr() { printf '%s\n' "------------------------------------------------------------"; }

header() {
    hr
    printf '%s @ %s\n' "$(hostname -f)" "$(date +'%F %T %Z')"
    printf 'kernel: %s   uptime:%s\n' "$(uname -r)" "$(uptime -p)"
    hr
}

cpu_section() {
    local idle1 total1 idle2 total2 usage
    read -r _ a b c idle1 _ < /proc/stat
    total1=$((a + b + c + idle1))
    sleep 1
    read -r _ a b c idle2 _ < /proc/stat
    total2=$((a + b + c + idle2))
    usage=$(( 100 * ((total2 - total1) - (idle2 - idle1)) / (total2 - total1) ))
    printf 'CPU usage:  %d%%   load: %s\n' "$usage" "$(cut -d' ' -f1-3 /proc/loadavg)"
    (( usage > CPU_WARN )) && { echo "  WARN: cpu > ${CPU_WARN}%"; EXIT_CODE=1; }
}

mem_section() {
    local total used pct
    read -r _ total used _ < <(free -m | awk '/^Mem:/')
    pct=$(( 100 * used / total ))
    printf 'Memory:     %d / %d MiB  (%d%%)\n' "$used" "$total" "$pct"
    (( pct > MEM_WARN )) && { echo "  WARN: memory > ${MEM_WARN}%"; EXIT_CODE=1; }
}

disk_section() {
    printf 'Disks:\n'
    df -hP -x tmpfs -x devtmpfs | awk 'NR>1 { printf "  %-25s %5s used of %-5s on %s\n", $1, $5, $2, $6 }'
    while read -r usage mount; do
        (( usage > DISK_WARN )) && { echo "  WARN: $mount > ${DISK_WARN}% (${usage}%)"; EXIT_CODE=1; }
    done < <(df -P -x tmpfs -x devtmpfs | awk 'NR>1 { gsub("%","",$5); print $5, $6 }')
}

main() {
    header
    cpu_section
    mem_section
    disk_section
    hr
    exit "$EXIT_CODE"
}

main "$@"

Script 2: deploy_app.sh

Git pull, npm build, systemd restart, health check, and automatic rollback on failure. Uses a lockfile to prevent concurrent deploys.

#!/usr/bin/env bash
# deploy_app.sh — pull, build, restart, verify, rollback on fail
set -euo pipefail

APP_DIR="/opt/myapp"
SERVICE="myapp"
HEALTH_URL="http://127.0.0.1:3000/health"
LOCK="/var/run/deploy_${SERVICE}.lock"
HEALTH_TRIES=20

log() { printf '[%s] %s\n' "$(date +%FT%T)" "$*"; }
die() { log "FATAL: $*" >&2; exit 1; }

# Single-instance lock
exec 9> "$LOCK"
flock -n 9 || die "another deploy is in progress"

cd "$APP_DIR" || die "$APP_DIR not found"
OLD_SHA=$(git rev-parse HEAD)
log "current SHA: $OLD_SHA"

git fetch --quiet origin
git reset --hard origin/main
NEW_SHA=$(git rev-parse HEAD)
[[ "$OLD_SHA" == "$NEW_SHA" ]] && { log "already at latest, no-op"; exit 0; }
log "new SHA: $NEW_SHA"

rollback() {
    log "ROLLING BACK to $OLD_SHA"
    git reset --hard "$OLD_SHA"
    npm ci --omit=dev && npm run build || true
    systemctl restart "$SERVICE"
    die "deploy failed, rolled back"
}

npm ci --omit=dev || rollback
npm run build || rollback
systemctl restart "$SERVICE" || rollback

for ((i=1; i<=HEALTH_TRIES; i++)); do
    if curl -fsS -m 3 "$HEALTH_URL" > /dev/null; then
        log "healthy after $i probe(s)"
        log "deploy OK: $OLD_SHA -> $NEW_SHA"
        exit 0
    fi
    sleep 2
done

rollback

Script 3: service_monitor.sh

Watchdog for systemd units. Restarts down services and posts to Slack on incident. Designed for a cron-every-minute or systemd timer.

#!/usr/bin/env bash
# service_monitor.sh — auto-restart services and Slack on failure
set -euo pipefail

SERVICES=(nginx postgresql redis-server docker)
MAX_RESTART=3
SLACK_URL="${SLACK_WEBHOOK_URL:-}"
HOST=$(hostname -f)

log() { printf '[%s] %s\n' "$(date +%FT%T)" "$*"; }

slack() {
    [[ -z "$SLACK_URL" ]] && { log "SLACK_WEBHOOK_URL not set, skipping notify"; return; }
    local msg="$1"
    curl -fsS -m 5 -X POST -H 'Content-Type: application/json' \
        --data "{\"text\":\":rotating_light: [$HOST] $msg\"}" \
        "$SLACK_URL" > /dev/null || log "WARN: slack post failed"
}

check_service() {
    local svc="$1"
    if systemctl is-active --quiet "$svc"; then
        log "$svc: OK"; return 0
    fi
    log "$svc: DOWN — attempting restart"
    local attempt=1
    while ((attempt <= MAX_RESTART)); do
        systemctl restart "$svc" || true
        sleep $((attempt * 2))
        if systemctl is-active --quiet "$svc"; then
            log "$svc: recovered on attempt $attempt"
            slack "$svc was DOWN, restarted successfully on attempt $attempt"
            return 0
        fi
        ((attempt++))
    done
    log "$svc: FAILED after $MAX_RESTART attempts"
    slack "$svc is DOWN and failed to restart after $MAX_RESTART attempts"
    return 1
}

failures=0
for s in "${SERVICES[@]}"; do
    check_service "$s" || ((failures++))
done
exit "$failures"

Script 4: log_rotate.sh

Minimal logrotate alternative for ad-hoc log directories. Gzips logs older than N days, deletes anything older than M days. Safe for cron.

#!/usr/bin/env bash
# log_rotate.sh — compress old logs, delete very old ones
set -euo pipefail

LOG_DIR="${1:?Usage: $0 <dir> [compress_days] [delete_days]}"
COMPRESS_DAYS="${2:-7}"
DELETE_DAYS="${3:-30}"

log() { printf '[%s] %s\n' "$(date +%FT%T)" "$*"; }
[[ -d "$LOG_DIR" ]] || { log "FATAL: $LOG_DIR not found"; exit 1; }
log "Rotating in $LOG_DIR (gzip >${COMPRESS_DAYS}d, delete >${DELETE_DAYS}d)"

compressed=0
while IFS= read -r -d '' f; do
    gzip -- "$f"
    ((compressed++))
done < <(find "$LOG_DIR" -type f -name '*.log' -mtime +"$COMPRESS_DAYS" -print0)
log "Compressed $compressed file(s)"

deleted=$(find "$LOG_DIR" -type f \( -name '*.log' -o -name '*.log.gz' \) \
            -mtime +"$DELETE_DAYS" -print -delete | wc -l)
log "Deleted $deleted file(s)"

Script 5: log_analysis.sh

Parse nginx access logs for top IPs, top URLs, and 4xx/5xx counts. Drop-in incident-response tool.

#!/usr/bin/env bash
# log_analysis.sh — top IPs, URLs, status code breakdown from nginx logs
set -euo pipefail

LOG="${1:-/var/log/nginx/access.log}"
TOP_N="${2:-10}"

[[ -r "$LOG" ]] || { echo "cannot read $LOG" >&2; exit 1; }

if [[ "$LOG" == *.gz ]]; then READ="zcat"; else READ="cat"; fi

echo "=== Summary of $LOG ==="
TOTAL=$($READ "$LOG" | wc -l)
echo "Total requests: $TOTAL"
echo

echo "--- Top $TOP_N IPs ---"
$READ "$LOG" | awk '{ print $1 }' | sort | uniq -c | sort -rn | head -n "$TOP_N"

echo
echo "--- Top $TOP_N URLs ---"
$READ "$LOG" | awk '{ print $7 }' | sort | uniq -c | sort -rn | head -n "$TOP_N"

echo
echo "--- Status code breakdown ---"
$READ "$LOG" | awk '{
    code=$9
    counts[code]++
    if (code+0 >= 400 && code+0 < 500) c4xx++
    else if (code+0 >= 500) c5xx++
}
END {
    for (c in counts) printf "%5d %s\n", counts[c], c | "sort -rn"
    close("sort -rn")
    printf "\nTotal 4xx: %d\nTotal 5xx: %d\n", c4xx+0, c5xx+0
}'

Key Patterns to Internalize

Functions and libraries: Split helpers into lib/common.sh and source it. Define log(), warn(), and die() functions in every serious script.

trap for cleanup:

TMPDIR=$(mktemp -d)
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXIT
trap 'echo "Interrupted"; exit 130' INT TERM

getopts for argument parsing:

while getopts "e:v:dh" opt; do
    case "$opt" in
        e) ENV="$OPTARG" ;;
        v) VERSION="$OPTARG" ;;
        d) DRY_RUN=1 ;;
        h) usage ;;
        *) usage ;;
    esac
done

Interview tip: When asked “design a deploy script,” walk through: (1) idempotency, (2) lockfile to prevent concurrent runs, (3) capture old state for rollback, (4) health check after restart, (5) automatic rollback path, (6) logging and notifications. The deploy_app.sh above hits all six — internalize the pattern.

Warning: Never write rm -rf "$VAR/" without set -u and an explicit non-empty check. Empty variable + slash = root-level delete. Always: [[ -n "${DIR:-}" && -d "$DIR" ]] || die "bad DIR" before any destructive operation.

Linux Logs

Section 26 of 39 · ~3 min

For a DevOps engineer, logs are the single source of truth. When a production app misbehaves at 3 AM, you don’t guess — you grep. On a Linux server you’ll be living inside /var/log and journalctl. Master these and you can debug anything.

Note: Mental model — Traditional Linux services write plain-text files to /var/log. Modern systemd services write to the binary journal, queried with journalctl. On Ubuntu 24.04 you have both.

Key Log Files — Cheat Sheet

PathDistroWhat’s in it
/var/log/syslogDebian/UbuntuGeneral system events — almost everything ends up here
/var/log/messagesRHEL/RockyRHEL equivalent of syslog
/var/log/auth.logDebian/UbuntuSSH logins, sudo, PAM, su — every auth event
/var/log/secureRHELRHEL equivalent of auth.log
/var/log/kern.logBothKernel ring buffer messages (drivers, OOM kills)
/var/log/apt/history.logDebian/UbuntuEvery apt install/remove/upgrade
/var/log/dpkg.logDebian/UbuntuLow-level package install/remove
/var/log/nginx/access.logAllNginx HTTP requests
/var/log/nginx/error.logAllNginx errors, upstream failures
/var/log/postgresql/postgresql-*.logUbuntuPostgreSQL queries, errors, checkpoints
/var/log/cloud-init.logCloud VMsEC2/GCE first-boot user-data output

journalctl — The systemd Journal

$ journalctl -e                       # jump to end (most recent)
$ journalctl -r                       # reverse — newest first
$ journalctl -n 50                    # last 50 lines
$ journalctl -f                       # follow (like tail -f)
$ journalctl -u nginx                 # only nginx.service
$ journalctl -u nginx -f              # follow nginx live
$ journalctl --since "1 hour ago"
$ journalctl --since today
$ journalctl -p err                   # priority err or worse
$ journalctl -b                       # this boot only
$ journalctl -b -1                    # previous boot
$ journalctl -k                       # kernel only (dmesg-style)
$ journalctl --disk-usage
$ sudo journalctl --vacuum-time=7d     # keep only last 7 days
$ sudo journalctl --vacuum-size=500M   # cap at 500 MB

Priority levels (numeric and named both work with -p): 0 emerg, 1 alert, 2 crit, 3 err, 4 warning, 5 notice, 6 info, 7 debug.

logrotate — Stops Disks From Filling Up

Logs grow forever unless rotated. logrotate runs daily via /etc/cron.daily/logrotate and reads /etc/logrotate.conf plus per-service files in /etc/logrotate.d/.

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14              # keep 14 old copies
    size 100M              # OR rotate when file > 100MB
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp adm
    sharedscripts
    postrotate
        systemctl reload myapp.service > /dev/null 2>&1 || true
    endscript
}
$ sudo logrotate -d /etc/logrotate.d/myapp      # dry run
$ sudo logrotate -f /etc/logrotate.d/myapp      # force run now

Tip: Without copytruncate or a postrotate reload, long-running daemons keep writing to the old (renamed) file descriptor — and you wonder why rotation did nothing. Always reload the service or use copytruncate.

rsyslog and Centralized Logging

rsyslog reads kernel and app messages and writes them to /var/log/* (or ships them to a remote server). Syntax: facility.priority destination.

auth,authpriv.*                 /var/log/auth.log
*.*  @@logserver.internal:514    # TCP to a central log server
$ logger -p local0.notice "Deploy v2.4.1 started by $USER"   # write to syslog

One server: grep is fine. Twenty servers: you need an aggregator.

StackComponentsBest for
ELK / ElasticFilebeat → Logstash → Elasticsearch → KibanaFull-text search, rich dashboards
Grafana LokiPromtail → Loki → GrafanaCheap, label-based
AWS CloudWatch LogsCW agent → CW Logs → Logs InsightsAWS-native, zero infra

Real DevOps Log Examples

$ awk '$9 ~ /^5/ {print $9}' /var/log/nginx/access.log | sort | uniq -c   # 5xx breakdown
$ awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head   # top IPs
$ sudo dmesg -T | grep -i "killed process"          # was the system OOM-killed?
$ systemctl --failed                                 # failed services
$ sudo du -sh /var/log/* | sort -h | tail -20        # disk usage per log dir

Real world: Tracing an SSH brute-force on a fresh VPS — auth.log balloons. Count failures and group by attacker IP:

$ sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head
$ sudo grep "Invalid user" /var/log/auth.log | awk '{print $8}' | sort | uniq -c | sort -rn | head

Mitigation: install fail2ban, set PasswordAuthentication no in sshd_config, move SSH off port 22, or restrict via ufw allow from <your-IP> to any port 22.

System Monitoring

Section 27 of 39 · ~3 min

Monitoring is the second pillar after logs. When a customer says “the site is slow,” you need to know within 30 seconds whether it’s CPU, memory, disk I/O, or network.

uptime — Load Averages

$ uptime
 11:42:18 up 14 days,  3:22,  2 users,  load average: 1.42, 0.87, 0.63
$ nproc
4

The three numbers are the average run-queue length over 1, 5, and 15 minutes. Load 1.42 on a 4-core box ≈ 35% utilized. Load 8.0 on 4 cores = processes queueing. 1-min > 15-min means load is increasing.

free — Memory

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7.7Gi       2.1Gi       312Mi        85Mi       5.3Gi       5.3Gi
Swap:          2.0Gi          0B       2.0Gi

Caution: Trap for beginners: “free” looks scary low — that’s not the number you want. Linux aggressively uses RAM for buff/cache (disk caches), reclaimable instantly. The number that matters is available.

vmstat and iostat

$ vmstat 2 5     # sample every 2s, 5 times
# watch: r (runnable, should be <= CPU count), si/so (swap, should be 0), wa (CPU% waiting on I/O)

$ sudo apt install -y sysstat
$ iostat -xz 2   # extended, skip idle devices
# %util near 100% = saturated. await > 20ms on SSD = problem.

sar — Historical Stats

sar (from sysstat) records metrics every 10 minutes, so you can look at yesterday’s CPU.

$ sar -u 1 5          # live CPU
$ sar -r 1 5          # live memory
$ sar -n DEV 1 5      # live network per interface
$ sar -u -f /var/log/sysstat/sa25   # CPU on the 25th

dmesg and Hardware Inventory

$ sudo dmesg -T                     # human-readable timestamps
$ sudo dmesg -T | grep -i oom       # OOM killer events
$ lscpu                              # CPU model, cores, flags
$ lsblk                              # block devices & mounts
$ lspci -nnk | grep -A2 Ethernet    # NIC + driver in use

top, htop, and Friends

$ top         # press 1=per-core, M=sort by memory, P=sort by CPU, c=full command, k=kill
$ sudo apt install -y htop glances nload iftop iotop ncdu
$ htop        # color, scrollable, tree view (F5)
$ glances     # CPU+MEM+NET+DISK+containers in one screen
$ sudo iotop -o    # disk I/O per process; -o = only active
$ ncdu /var        # interactive du — navigate and delete junk

In top: RES (resident set size — actual physical RAM) is the number that matters; VIRT is mostly meaningless.

Real Monitoring Scenarios

$ watch -n 5 'ps -o pid,rss,vsz,cmd -p $(pgrep -f myapp)'   # watch for a memory leak
$ ps aux --sort=-%mem | head -11                            # top 10 by memory
$ ps aux --sort=-%cpu | head -11                            # top 10 by CPU
$ iostat -xz 2 5                                            # find I/O bottleneck
$ ss -tn state established | awk 'NR>1{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
$ ps aux | awk '$8 ~ /Z/ {print}'                           # find zombie processes

Tip: Interview answer for “server slow, what do you do?” — in this exact order: uptime (load), top/htop (process), free -h (memory), iostat -xz 2 (disk), ss -s (network), dmesg -T | tail (kernel). This is Brendan Gregg’s USE Method: Utilization, Saturation, Errors.

Cron Jobs

Section 28 of 39 · ~3 min

Cron is the Unix scheduler — a daemon that wakes up every minute and runs jobs whose time has come. You’ll use it for backups, certificate renewals, log shipping, health checks, and cleanup scripts. It’s old (1975!) but still everywhere.

$ systemctl status cron           # Ubuntu/Debian
$ systemctl status crond          # RHEL

Crontab Syntax

# ┌───────────── minute        (0 - 59)
# │ ┌─────────── hour          (0 - 23)
# │ │ ┌───────── day of month  (1 - 31)
# │ │ │ ┌─────── month         (1 - 12)
# │ │ │ │ ┌───── day of week   (0 - 7, where 0 and 7 both = Sunday)
# │ │ │ │ │
# * * * * *  command-to-run
SymbolMeaningExample
*Any value* * * * * — every minute
*/NEvery N units*/15 * * * * — every 15 minutes
N-MRange0 9-17 * * * — every hour, 9 AM to 5 PM
N,MList0 8,12,18 * * * — 8 AM, noon, 6 PM

Named shortcuts replace the whole 5-field expression: @reboot, @hourly, @daily, @weekly, @monthly, @yearly.

Tip: Cron expressions are easy to get subtly wrong. Validate them against a human-readable description and the next few run times with the Cron Expression Tester before committing a crontab.

Managing Crontabs

$ crontab -e                # edit YOUR crontab
$ crontab -l                # list YOUR crontab
$ crontab -ri               # delete with confirmation
$ sudo crontab -u deploy -e # edit deploy user's crontab

User crontabs live in /var/spool/cron/crontabs/<user> — always use crontab -e, never edit directly.

System Cron

System crontab files have an extra “user” field between the schedule and the command:

$ sudo cat /etc/crontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# m h dom mon dow user  command
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || (cd / && run-parts --report /etc/cron.daily)
PathPurpose
/etc/crontabMain system schedule (with user field)
/etc/cron.d/Drop-in files (packages install here)
/etc/cron.hourly/ .daily/ .weekly/ .monthly/Drop a script in, runs on schedule via run-parts

run-parts executes every executable file in a directory. Two gotchas: scripts must be executable (chmod +x), and filenames must not contain dots.

Cron Logs

$ sudo grep CRON /var/log/syslog | tail -20
$ sudo journalctl -u cron --since today

Common DevOps Use Cases

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=devops@example.com

# Daily Postgres backup at 02:00
0 2 * * * /usr/local/bin/backup-postgres.sh >> /var/log/backup.log 2>&1
# Renew Let's Encrypt certs twice a day
17 3,15 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
# Health check every 5 minutes
*/5 * * * * /usr/local/bin/healthcheck.sh || /usr/local/bin/slack-alert.sh "API down"
# Clean Docker dangling images weekly
30 4 * * 1 /usr/bin/docker system prune -af --filter "until=168h" >> /var/log/docker-prune.log 2>&1

Best Practices and Common Mistakes

  • PATH is minimal. Cron’s default PATH is just /usr/bin:/bin. Your docker, aws, pg_dump, node may not be found. Set PATH= at the top of the crontab or use full paths.
  • No shell aliases / .bashrc. Cron runs /bin/sh non-interactively — no rc files are sourced.
  • Working directory is $HOME. Use absolute paths or cd /opt/app && first.
  • Always redirect output. Unredirected stdout/stderr becomes mail to the user. Use >> /var/log/myjob.log 2>&1.
  • Use scripts, not inline one-liners — testable and version-controllable.
  • Idempotency / overlap. Use a lockfile: flock -n /tmp/myjob.lock -c '/path/to/job.sh'.
  • % is special — a literal % must be escaped as \%.

Caution: The #1 cron PATH gotcha: your script works when you run ./backup.sh by hand, but cron says docker: command not found. That’s because your shell’s PATH includes /usr/local/bin but cron’s doesn’t. Always set PATH= at the top of the crontab OR call binaries by absolute path.

The at Command

Section 29 of 39 · ~1 min

Where cron is for recurring jobs, at is for one-shot jobs. “Restart this service at 3 AM tonight.” “Run this cleanup once, ninety minutes from now.” Then forget it.

$ sudo apt install -y at
$ sudo systemctl enable --now atd

Basic Usage

$ at now + 5 minutes
at> echo "Hello from the future" > /tmp/hello.txt
at> systemctl restart nginx
at> ^D
job 3 at Tue May 26 11:47:00 2026

Time formats at understands: now + 2 hours, now + 1 day, 14:30, 14:30 tomorrow, midnight, noon, teatime (16:00), 3pm Friday, 02:00 next month.

Piping Commands In

$ echo "/opt/scripts/restart-app.sh" | at now + 1 hour
$ echo "shutdown -h now" | sudo at 23:00
$ at -f /opt/scripts/cleanup.sh now + 30 minutes

Managing Jobs

$ atq                       # list pending jobs
$ at -c 7                   # show contents of job 7
$ atrm 7                    # cancel job 7

batch is at’s sibling: it runs queued commands only when the system load average drops below 1.5 — great for heavy one-off jobs.

NeedUse
Run every day / hour / weekcron (or systemd timer)
Run exactly once, in the futureat
”Restart server in 5 min if I don’t cancel”at (great safety net during config changes)
Run a heavy job when the system is idlebatch
Retries, dependencies, complex orchestrationsystemd timer, Airflow/Argo

Tip: The “dead man’s switch” trick: before editing risky SSH/firewall config, schedule a rollback: echo "cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config && systemctl restart ssh" | sudo at now + 5 minutes. Make your changes and test. If you lock yourself out, the job revokes them in 5 minutes. If everything works: sudo atrm <jobnum>.

systemd Timers

Section 30 of 39 · ~2 min

systemd timers are the modern replacement for cron. More verbose to set up, but they integrate with the rest of systemd: structured logging in the journal, dependency management, resource limits, retries, and the ability to catch up after downtime.

Note: Key idea — a timer is a unit that triggers another unit. You always create TWO files: foo.timer (when to run) and foo.service (what to run).

A Complete Example: Daily DB Backup

# /etc/systemd/system/myapp-backup.service
[Unit]
Description=Daily backup of myapp Postgres database
Wants=network-online.target
After=network-online.target postgresql.service

[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup-myapp.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/myapp-backup.timer
[Unit]
Description=Run myapp DB backup daily at 02:00

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5min
Unit=myapp-backup.service

[Install]
WantedBy=timers.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now myapp-backup.timer   # enable the TIMER, not the service
$ systemctl list-timers myapp-backup.timer
$ sudo systemctl start myapp-backup.service        # manually trigger once to test
$ journalctl -u myapp-backup.service -n 50          # view output

OnCalendar Syntax

Format: DayOfWeek Year-Month-Day Hour:Minute:Second.

OnCalendar=When
hourly / daily / weekly / monthlyCommon shortcuts
*-*-* 02:00:00Every day at 02:00
Mon-Fri 09:00Weekdays at 09:00
*-*-01 04:00:001st of every month at 04:00
*-*-* *:0/15:00Every 15 minutes
2026-12-31 23:59:00Once: New Year’s Eve 2026

Always validate before deploying:

$ systemd-analyze calendar "Mon-Fri 09:00"
$ systemd-analyze calendar --iterations=5 "*-*-* *:0/15:00"

Other Trigger Types

DirectiveMeaning
OnBootSec=15min15 minutes after boot
OnUnitActiveSec=1h1 hour after the service last activated
Persistent=trueIf a run was missed (server off), run on next boot
RandomizedDelaySec=5minSpread a fleet of identical jobs — prevents thundering herd
$ systemctl list-timers              # next/last run for every timer
$ systemctl list-timers --all
$ systemctl cat myapp-backup.timer

Cron vs systemd Timer

Featurecronsystemd timer
Setup effortOne lineTwo unit files
LogsCron log + your redirectsCentralized journal — journalctl -u name
DependenciesNoneAfter=, Wants=, Requires=
Retries on failureWrite your ownRestart=on-failure
Catch up missed runsanacron only, bluntPersistent=true
Resource limitsNoneMemoryMax=, CPUQuota=, etc.
Test/dry-runNonesystemd-analyze calendar "..." + systemctl start
FamiliarityUniversalLinux-only

Tip: Migration tip: if you already have a cron job, you can convert it to a timer in 10 minutes. Bonus: journalctl -u myjob gives you every run, exit code, and stdout/stderr without setting up your own log redirection. That alone justifies the two extra files.

Linux Security

Section 31 of 39 · ~5 min

Security in Linux is not a feature you turn on, it’s a posture you maintain. As a DevOps engineer you’re responsible for hardening servers, protecting credentials, and reducing attack surface.

Note: Security is a habit, not an event. Check daily: who is logging in, what ports are open, are the latest patches applied. Hardening once and forgetting is the biggest mistake — attackers only need one small gap.

Strong Passwords (PAM Password Quality)

PAM is the framework Linux uses for authentication. The pam_pwquality module enforces strong passwords system-wide via /etc/security/pwquality.conf:

minlen = 14
minclass = 4           # uppercase + lowercase + digit + symbol
dcredit = -1           # require at least 1 digit
ucredit = -1           # require at least 1 uppercase
ocredit = -1           # require at least 1 symbol
retry = 3
enforce_for_root = 1

Account lockout after failed attempts via pam_faillock in /etc/pam.d/common-auth (deny=5, unlock_time=900).

Caution: Test before you log out! A bad PAM config can lock you out of your own server. Always keep a second SSH session open while editing PAM files.

SSH Hardening Checklist (Recap)

Edit /etc/ssh/sshd_config:

PermitRootLogin no                 # disable root login
PasswordAuthentication no          # keys only — most important setting
Port 2222                          # reduce noise
AllowUsers deploy ops-alice
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
X11Forwarding no
PermitEmptyPasswords no
$ ssh-keygen -t ed25519 -a 100 -C "pushkar@workstation"
$ sudo sshd -t                      # test config syntax
$ sudo systemctl reload ssh

Firewall

# ufw — Ubuntu's friendly front-end
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw allow 2222/tcp comment 'SSH'
$ sudo ufw allow 80,443/tcp
$ sudo ufw allow from 192.168.1.0/24 to any port 5432   # Postgres LAN only
$ sudo ufw limit 2222/tcp           # rate-limit SSH
$ sudo ufw enable
$ sudo ufw status verbose
FirewallUsed ByNotes
ufwUbuntu, DebianSimple front-end to iptables/nftables
iptablesLegacy everywherePowerful, being replaced by nftables
nftablesModern kernel defaultUnified, faster, atomic rules
firewalldRHEL, FedoraZone-based, runtime + permanent
# firewalld (RHEL/Rocky)
$ sudo firewall-cmd --add-service=https --permanent
$ sudo firewall-cmd --add-port=2222/tcp --permanent
$ sudo firewall-cmd --reload

fail2ban — Intrusion Prevention

fail2ban scans log files and bans IPs that show malicious behaviour by updating firewall rules dynamically.

# /etc/fail2ban/jail.local
[sshd]
enabled  = true
port     = 2222
maxretry = 3
bantime  = 24h
$ sudo systemctl enable --now fail2ban
$ sudo fail2ban-client status sshd
$ sudo fail2ban-client set sshd unbanip 203.0.113.45

Real world: On any internet-facing VM you’ll see SSH brute-force attempts within minutes of booting. fail2ban + non-standard port + key-only auth reduces auth.log from thousands of failure lines per day to near-zero. It’s the single highest-leverage hardening step.

SELinux vs AppArmor — Mandatory Access Control

Standard Unix permissions are discretionary (the owner decides access). MAC adds a second layer: even root cannot violate kernel-enforced policies.

FeatureSELinuxAppArmor
Default onRHEL, Fedora, CentOSUbuntu, Debian, SUSE
Policy styleLabels on inodesPath-based profiles
GranularityVery fine-grainedEasier to read & write
# SELinux
$ getenforce                       # Enforcing | Permissive | Disabled
$ sudo setenforce 0                # Permissive (logs only)
$ ls -Z /var/www/html              # show SELinux contexts
$ sudo restorecon -Rv /var/www/html

# AppArmor
$ sudo aa-status
$ sudo aa-complain /etc/apparmor.d/usr.bin.nginx   # learning mode
$ sudo aa-enforce  /etc/apparmor.d/usr.bin.nginx

Caution: Don’t disable MAC because “the app won’t work.” Set it to Permissive/Complain mode, reproduce the issue, read the audit log, then write a targeted policy exception. Disabling SELinux/AppArmor in production is a major red flag in security audits.

File Integrity — Hashes

$ md5sum ubuntu-24.04.iso          # FAST but cryptographically broken — checksums only
$ sha256sum ubuntu-24.04.iso       # the standard for downloads
$ sha512sum kernel.tar.xz          # extra paranoid
$ sha256sum -c SHA256SUMS          # verify against a published checksum file

Tip: Need to hash a string or small file quickly, or compare a download against its published digest without dropping to a shell? The Hash Generator produces MD5/SHA-1/SHA-256/SHA-512 in the browser.

GPG Basics

GPG provides encryption, signing, and key management — used everywhere from APT repo signing to package release verification.

$ gpg --full-generate-key          # use ed25519 or RSA 4096
$ gpg --encrypt --armor -r alice@example.com secrets.txt
$ gpg --decrypt secrets.txt.asc > secrets.txt
$ gpg --detach-sign --armor release-1.4.0.tar.gz
$ gpg --verify kubectl.sha256.sig kubectl.sha256   # verify upstream releases

Finding Privilege-Escalation Vectors

$ sudo find / -perm -4000 -type f -xdev 2>/dev/null   # SUID binaries
$ sudo find / -perm -2000 -type f -xdev 2>/dev/null   # SGID binaries
$ sudo find / -perm -0002 -type f -xdev 2>/dev/null   # world-writable files (red flag)
$ sudo find / -nouser -o -nogroup 2>/dev/null         # files with no owner

Automated Updates

Unpatched systems are the #1 source of compromise. Automate it.

# Ubuntu
$ sudo apt install unattended-upgrades apt-listchanges
$ sudo dpkg-reconfigure --priority=low unattended-upgrades
# RHEL
$ sudo dnf install dnf-automatic
$ sudo systemctl enable --now dnf-automatic.timer

Real DevOps Security Checklist

  1. Update OS and patches — unattended-upgrades / dnf-automatic; reboot for kernel CVEs.
  2. Disable root SSH and use key-only authentication (ed25519).
  3. Firewall default deny — open only required ports.
  4. fail2ban on SSH and other internet-facing services.
  5. Audit users — remove unused accounts, lock service accounts, rotate keys.
  6. Disable unused servicessystemctl list-unit-files --state=enabled, mask what you don’t need.
  7. Centralized logs — ship to ELK/Loki/CloudWatch; local logs can be wiped by attackers.
  8. Time sync — chrony or systemd-timesyncd; broken clocks break TLS and audit timestamps.
  9. SELinux/AppArmor enforcing — never permanently disable MAC.
  10. Vulnerability scans — Lynis (sudo lynis audit system), OpenSCAP, Trivy for containers.
  11. Backups + tested restore — an untested backup is a hope, not a backup.
  12. Least privilege — sudo whitelists, no shared accounts, separate prod/staging credentials.
  13. Container security — non-root USER, drop capabilities, read-only rootfs, scan images (Trivy/Grype), use distroless/minimal base images.
  14. Secrets management — Vault, AWS Secrets Manager, sealed-secrets; never commit secrets to git.
  15. Monitor auth events — alert on failed sudo, new SSH keys, sudoers changes.

Real world: When you join a new DevOps team, run Lynis on a staging box in week one. It generates a 100+ point hardening report. Even mature shops typically score 60-70/100 — the gap is your low-hanging-fruit roadmap and an instant credibility builder.

Troubleshooting Methodology

Section 32 of 39 · ~4 min

Troubleshooting is the skill that separates a junior DevOps from a senior. Tools you can google. Methodology you have to internalise.

The Systematic Approach

  1. Define the problem clearly. “The website is slow” is not a definition. “p95 latency on /api/checkout went from 200ms to 3s at 14:02 UTC for all users” is.
  2. Gather data. Logs, metrics, kernel messages (dmesg), recent changes (deploys, config), system state (top, df, ss).
  3. Reproduce it. Either in staging, or get exact steps.
  4. Form a hypothesis. One thing at a time.
  5. Test with the smallest change. Don’t change five things at once.
  6. Document. Symptom, root cause, fix, prevention.

Note: The biggest mistake is skipping root cause in the rush to fix. A server restart gives 5 minutes of relief, but if you didn’t catch the root cause the issue returns in 3 days — and by then the logs have rotated. Slow down to go fast.

Where to Start, by Symptom

Symptom AreaFirst Places to Look
Boot issuesjournalctl -b, journalctl -b -1, dmesg, systemd-analyze blame
Auth / SSH/var/log/auth.log (or /var/log/secure), journalctl -u sshd
Service problemsystemctl status <svc>, journalctl -u <svc> -n 200
Diskdf -h, df -i, du -sh /*, lsblk -f, iostat -xz 1
Networkip a, ip r, curl -v, ss -tulpn, dig, tcpdump
Performancetop, vmstat 1, iostat -xz 1, sar -u 1 5, pidstat 1
Memory / OOMfree -h, dmesg | grep -i 'killed process', journalctl -k | grep -i oom

Common Troubleshooting Scenarios

Server slow / not responding:

$ uptime; top -o %CPU; top -o %MEM; iostat -xz 1 5; vmstat 1 5; ss -s

Identify the bottleneck (CPU / IO / memory / network), then kill/throttle the offender, scale resources, or add caching. 80% of “server slow” tickets are: a runaway log filling disk, a query without an index, or memory pressure causing swap thrashing — check disk and swap first.

Service won’t start:

$ sudo systemctl status nginx
$ sudo journalctl -u nginx -n 100 --no-pager
$ sudo nginx -t                  # config syntax check
$ sudo ss -tulpn | grep :80      # port already in use?

Common causes: config syntax error, port already bound, missing dependency in After=, wrong User=, SELinux/AppArmor denial.

SSH “Permission denied (publickey)”:

$ ssh -vvv -i ~/.ssh/id_ed25519 deploy@server     # which key was tried?
$ stat -c '%a %U %G %n' ~/.ssh ~/.ssh/authorized_keys
# Required: ~ = 755/750, .ssh = 700, authorized_keys = 600, owned by the user

SSH “Connection refused” vs “timed out”: Refused = something at the destination actively rejected (sshd down, or firewall REJECT). Timed out = packets never arrived (routing, cloud security group, or a DROP rule). AWS/GCP/Azure security groups are the most common culprit.

Out of memory (OOM killer):

$ dmesg -T | grep -i 'killed process'
$ journalctl -k | grep -i 'oom-killer'
$ free -h ; cat /proc/swaps

Fix: add swap (short term), increase memory, fix the leaking app, or set MemoryMax= in the systemd unit. Java/Node in containers often get OOM-killed because they read the host’s memory, not the cgroup limit — set -Xmx or NODE_OPTIONS=--max-old-space-size to match the container limit.

Container not starting:

$ docker ps -a                           # find the dead container's exit code
$ docker logs --tail=200 myapp
$ docker run --rm -it --entrypoint sh myapp:latest   # poke inside

Common exit codes: 0 = clean exit, 1 = app error, 125 = docker daemon error, 126 = not executable, 127 = command not found, 137 = OOM-killed, 139 = segfault, 143 = SIGTERM. Volume permission mismatch (UID inside container vs host) is the #1 silent failure in dev environments.

Power Tools

ToolUse ForExample
straceTrace system callsstrace -f -e openat,connect -p 1234
lsofList open files, sockets, FDslsof -i :8080, lsof +L1
perfCPU profiling, flame graphssudo perf top
tcpdumpPacket capturesudo tcpdump -i any -w cap.pcap port 443
bpftraceeBPF tracingsudo execsnoop-bpfcc

The USE Method

Brendan Gregg’s USE method: for every resource (CPU, memory, disk IO, disk capacity, network IO, file descriptors), check Utilization, Saturation, and Errors.

Real world: In every senior DevOps interview you’ll get an open-ended scenario: “Production API is returning 500s, walk me through what you do.” They’re testing whether you have a method: define → gather → hypothesise → test → document. Mention rollback as the first option before deep debugging in prod. That single framing puts you ahead of most candidates.

Common Interview Questions

Section 33 of 39 · ~4 min

These are the highest-frequency Linux questions asked in DevOps and SRE interviews, condensed to the answer that signals real understanding. Drill them out loud — if you can teach the answer in your own words, you own it.

1. Difference between Linux and Unix? Unix is a proprietary OS family from the 1970s (AT&T Bell Labs); Linux is a free, open-source, Unix-like kernel created by Linus Torvalds in 1991. Linux follows Unix design principles but shares no code with original Unix. macOS is certified Unix (BSD lineage); Linux is not.

2. Difference between the kernel and the operating system? The kernel is the core program that talks to hardware and manages CPU, memory, devices, and processes. The OS is the kernel plus userspace — shell, libraries, utilities — that makes it usable. Calling the kernel “the OS” is the classic blunder.

3. Difference between bash and sh? sh is the POSIX-standard minimal shell; bash is a superset with arrays, [[ ]], brace expansion, and the function keyword. On Debian/Ubuntu, /bin/sh is dash, not bash — so a #!/bin/sh script using bash-only features works on RHEL but breaks on Ubuntu. Match your shebang to the features you use.

4. How do Linux file permissions work? Three triplets (owner/group/other), each with read (4), write (2), execute (1). Set symbolically (chmod u+x) or octal (chmod 755). Special bits: setuid (run as owner), setgid (inherit group / run as group), sticky (only owner can delete, as on /tmp).

5. Hard link vs symbolic link? A hard link is a second directory entry pointing to the same inode — same FS only, can’t link directories, survives deletion of any one name while link count > 0. A symlink is a separate file whose content is a path string, resolved at access time — can cross filesystems, can point to directories, dangles if the target is removed.

6. What happens from power-on to login prompt? BIOS/UEFI → POST → MBR/GPT boot sector → GRUB loads kernel + initramfs → kernel mounts / and starts PID 1 → systemd starts services in dependency order and mounts /etc/fstab → reaches multi-user.target → login (getty/sshd). Most production outages happen here.

7. What’s the difference between a VM and a container? A VM virtualizes hardware and runs a full guest kernel. A container is a regular process on the host kernel isolated with namespaces (PID, net, mount, user) and cgroups (CPU, memory, I/O limits). Container = process; VM = virtual machine.

8. Difference between kill -9 and kill -15? -15 (SIGTERM) is a polite shutdown request the process can catch and clean up after. -9 (SIGKILL) is uncatchable and immediate — no cleanup, risk of corrupt state. Always try SIGTERM first.

9. Explain fork() and exec(). fork() duplicates the current process (child gets a new PID). exec() replaces the child’s memory image with a new program (same PID). The shell uses fork+exec+wait to run every command.

10. Disk shows full but df reports free space — what’s wrong? Either inode exhaustion (df -i shows 100% IUse) or a process holding a deleted-but-open file (lsof | grep deleted). For inodes, delete the millions of tiny files; for the open file, restart the holding process.

11. How do you debug a service that won’t start? systemctl status <svc>, then journalctl -u <svc> -n 100, then validate config (nginx -t, sshd -t), then check the port isn’t already bound (ss -tulpn). Common causes: config syntax, port conflict, wrong User=, missing dependency, MAC denial.

12. What’s the difference between cron and a systemd timer? Both schedule recurring work. systemd timers add journal logging, dependency ordering (After=/Wants=), retries (Restart=), catch-up of missed runs (Persistent=true), and resource limits. Cron is simpler and universal. Use timers when you need observability and dependencies.

13. How do you harden SSH on a production server? PasswordAuthentication no (key-only), PermitRootLogin no, restrict with AllowUsers, lower MaxAuthTries, non-standard port, add fail2ban, and use ed25519 keys. Always sshd -t and keep a second session open before reloading.

14. A server is slow — walk me through it. Triage in order: uptime (load vs core count), top/htop (CPU/mem hogs), free -h (look at available, not free), iostat -xz 2 (disk %util/await), ss -s (connections), dmesg -T | tail (kernel/OOM). This is the USE method: Utilization, Saturation, Errors.

15. How do you find what’s listening on a port / using a port? ss -tlnp | grep :80 or lsof -i :80 shows the listener and PID. To free it, identify the PID then stop that service (fuser -k 80/tcp as a blunt instrument).

Tip: In interviews, never just say “I don’t know” — add “but here’s how I’d find out.” Confidence with humility beats fake expertise. For an experienced developer pivoting into DevOps, that maturity is a genuine differentiator.

Quick Reference Cheat Sheets

Section 34 of 39 · ~26 min

Bookmark this section — these are the reference sheets you will hit Ctrl+F on during your first 6 months on the job. Print them, paste them on your wall, whatever works. Real production-grade reference, no fluff.

1. Top 100 Linux Commands (Grouped)

Files & Directories

CommandPurposeExample
lsList directoryls -lah --color
cdChange directorycd - (previous)
pwdPrint working dirpwd -P (resolve symlinks)
mkdirMake directorymkdir -p a/b/c
rmdirRemove empty dirrmdir empty_folder
rmRemove filesrm -rf node_modules
cpCopycp -av src/ dst/
mvMove/renamemv old.txt new.txt
lnLink (hard/soft)ln -s /opt/app /usr/local/app
touchCreate empty / update mtimetouch app.log
statFile metadatastat /etc/passwd
fileDetect file typefile binary.bin
findFind filesfind . -name "*.log" -mtime +7
locateFast filename searchlocate sshd_config
treeDirectory treetree -L 2
basenameStrip pathbasename /a/b/c.txt
dirnameStrip filenamedirname /a/b/c.txt
realpathResolve absolute pathrealpath ./app

Text Processing

CommandPurposeExample
catPrint filecat /etc/os-release
tacReverse cattac access.log
lessPaged viewerless +F app.log (follow)
moreOld pagermore big.txt
headFirst N lineshead -n 20 file
tailLast N linestail -f -n 100 app.log
grepPattern search`grep -rEn “TODO
egrep / fgrepERE / fixed`grep -E “a
sedStream editorsed -i 's/dev/prod/g' app.conf
awkField processorawk '{print $1,$9}' access.log
cutExtract columnscut -d: -f1 /etc/passwd
sortSort linessort -u -k2,2n
uniqDedupe (sorted)`sort file
wcCountwc -l *.py
trTranslate charstr 'a-z' 'A-Z'
teeWrite & pass through`cmd
diffComparediff -u a b
patchApply diffpatch -p1 < fix.patch
jqJSON processorjq '.items[].name' data.json
yqYAML processoryq '.services' docker-compose.yml
xargsBuild commands`find . -name “*.bak”

Process

CommandPurposeExample
psProcess snapshotps -ef or ps aux
topLive processestop -o %CPU
htopBetter tophtop (F6 sort, F9 signal)
killSend signalkill -9 1234
pkillKill by namepkill -f gunicorn
killallKill all by namekillall nginx
pgrepFind PIDspgrep -af java
niceRun with prioritynice -n 10 ./batch.sh
reniceChange priorityrenice 5 -p 1234
nohupSurvive logoutnohup ./run.sh &
jobsShell jobsjobs -l
bg / fgBackground/foregroundbg %1 / fg %1
straceTrace syscallsstrace -p 1234 -f -e network
lsofOpen fileslsof -i :8080

Network

CommandPurposeExample
ipModern net toolip -br a / ip r
ifconfigLegacyifconfig eth0
pingICMP testping -c 4 google.com
tracerouteHop tracetraceroute -n 8.8.8.8
mtrLive traceroutemtr 1.1.1.1
digDNS lookupdig +short A example.com
nslookupDNS querynslookup example.com 8.8.8.8
hostSimple DNShost example.com
curlHTTP clientcurl -fsSL https://x.com
wgetDownloadwget -c https://x.com/file
ssSocketsss -tulnp
netstatLegacy ssnetstat -tulnp
ncNetcatnc -zv host 443
nmapPort scannernmap -sV -p 1-1000 host
tcpdumpPacket capturetcpdump -i eth0 port 80
iptablesFirewall (legacy)iptables -L -n -v
nftnftablesnft list ruleset

System

CommandPurposeExample
unameKernel infouname -a
hostnamectlHostname/OS infohostnamectl
uptimeLoad avguptime
w / whoLogged usersw
lastLogin historylast -n 20
dateDate/timedate -u +%Y-%m-%dT%H:%M:%SZ
timedatectlTimezone/NTPtimedatectl set-timezone Asia/Kolkata
systemctlService controlsystemctl status nginx
journalctlLogsjournalctl -u nginx -f
dmesgKernel ring buffer`dmesg -T
freeMemoryfree -h
vmstatVirtual memory statsvmstat 1 5
iostatIO statsiostat -xz 1
sarHistorical perfsar -u 1 5
envEnv vars`env
which / typeLocate binarytype -a ls

Security & Users

CommandPurposeExample
sudoPrivilege escalationsudo -i
suSwitch usersu - deploy
passwdChange passwordsudo passwd alice
useraddAdd useruseradd -m -s /bin/bash alice
usermodModify userusermod -aG docker alice
userdelDelete useruserdel -r alice
groupaddAdd groupgroupadd devops
idShow IDsid alice
chagePass agingchage -l alice
getfacl/setfaclACLssetfacl -m u:alice:rw file
fail2ban-clientBan abusersfail2ban-client status sshd
ufw / firewalldFirewall frontendsufw allow 22/tcp
opensslCrypto Swiss-armyopenssl s_client -connect host:443

Compression

CommandPurposeExample
tarArchivetar -czvf a.tgz dir/
tar -xExtracttar -xzvf a.tgz
gzip / gunzipgzip filesgzip -9 big.log
bzip2 / bunzip2bzip2bzip2 file
xz / unxzxz (best ratio)xz -T0 huge.tar
zip / unzipZip formatunzip -d out/ a.zip
zcat / zlessView gzipped`zcat app.log.gz

Permissions

CommandPurposeExample
chmodChange modechmod 750 deploy.sh
chownChange ownerchown -R deploy:deploy /opt/app
chgrpChange groupchgrp www-data /var/www
umaskDefault maskumask 027

Disk

CommandPurposeExample
dfFilesystem usagedf -hT
duDirectory usage`du -sh *
lsblkBlock deviceslsblk -f
fdiskPartition toolsudo fdisk -l
partedGPT partitioningparted /dev/sdb print
mkfsMake filesystemmkfs.ext4 /dev/sdb1
mount / umountMount FSmount /dev/sdb1 /mnt
blkidUUIDs/labelsblkid /dev/sda1
ncduTUI disk usagencdu /var
fsckFilesystem checkfsck -y /dev/sda1
lvm toolsLVMpvs / vgs / lvs

Miscellaneous

CommandPurposeExample
man / infoManualsman 5 crontab
tldrPractical examplestldr tar
historyShell history`history
aliasDefine aliasalias ll='ls -lah'
watchRepeat commandwatch -n 2 'df -h'
timeTime a commandtime ./build.sh
timeoutLimit runtimetimeout 30s ./probe
yesRepeat string`yes
screen / tmuxTerminal multiplexertmux new -s work
echo / printfPrintprintf "%s\n" "$VAR"
true / falseExit code helperswhile true; do ...; done

2. Vim Cheat Sheet

Modes

ModeEnterPurpose
NormalEscDefault; movement & commands
Inserti I a A o OType text
VisualvCharacter selection
Visual lineVLine selection
Visual blockCtrl+vColumn / rectangular edit
Command:Ex commands (:w, :q)
ReplaceROverwrite mode
Terminal:terminalEmbedded shell
KeyAction
h j k lLeft, down, up, right
w / bNext / previous word
e / geEnd of word / previous word end
0 / ^ / $Line start / first non-blank / line end
gg / GFile top / file bottom
:42 or 42GJump to line 42
Ctrl+d / Ctrl+uHalf page down / up
Ctrl+f / Ctrl+bFull page down / up
{ / }Previous / next blank line
%Match bracket
f x / F xFind char forward / backward
* / #Search word under cursor forward / backward

Editing

KeyAction
i / aInsert before / after cursor
I / AInsert at line start / end
o / ONew line below / above
x / XDelete char forward / backward
dd / DDelete line / to end of line
dw / d$ / dGDelete word / to EOL / to EOF
yy / YYank line
p / PPaste after / before
cc / CChange line / to EOL
cw / ciw / ci"Change word / inner word / inside quotes
r / RReplace one char / replace mode
u / Ctrl+rUndo / redo
.Repeat last change
>> / <<Indent / dedent line
JJoin line below

Search & Replace

/pattern        " Search forward
?pattern        " Search backward
n / N           " Next / previous match
:noh            " Clear highlight

:s/old/new/         " Replace first on line
:s/old/new/g        " Replace all on line
:%s/old/new/g       " Replace all in file
:%s/old/new/gc      " Confirm each
:%s/old/new/gI      " Case-sensitive (capital I)
:5,15s/old/new/g    " Replace in line range
:'<,'>s/old/new/g    " Replace in visual selection
:g/pattern/d        " Delete all lines matching
:v/pattern/d        " Delete lines NOT matching

Visual Mode

KeyAction
v / V / Ctrl+vChar / line / block selection
d / y / cDelete / yank / change selection
> / <Indent / dedent block
~Toggle case
u / ULower / upper case selection
:'<,'>sortSort selected lines
Ctrl+v then I text EscInsert column prefix on all lines

Advanced — Registers, Marks, Macros, Splits, Tabs

" Registers (named clipboards)
"ayy            " Yank line to register a
"ap            " Paste register a
:reg            " Show all registers
"+y             " Yank to system clipboard (needs +clipboard)
"+p             " Paste from system clipboard

" Marks
ma              " Set mark a at cursor
'a              " Jump to line of mark a
`a              " Jump to exact position of mark a
:marks          " List all marks

" Macros
qa              " Start recording macro into a
...do stuff...
q               " Stop recording
@a              " Play macro a
@@              " Replay last macro
10@a            " Play macro 10 times

" Splits
:split file     " Horizontal split  (or :sp)
:vsplit file    " Vertical split    (or :vsp)
Ctrl+w h/j/k/l  " Move between splits
Ctrl+w =        " Equalize sizes
Ctrl+w _        " Maximize height
Ctrl+w |        " Maximize width
Ctrl+w c        " Close split

" Tabs
:tabnew file    " Open in new tab
gt / gT         " Next / previous tab
:tabclose       " Close current tab
:tabs           " List tabs

3. File Permissions Reference

Octal Table (000–777)

OctalBinaryrwxMeaning
0000---No access
1001--xExecute only
2010-w-Write only
3011-wxWrite + execute
4100r--Read only
5101r-xRead + execute
6110rw-Read + write
7111rwxFull access

Three digits = owner, group, others. So 750 = rwxr-x--- (owner full, group r-x, others nothing).

Special Bits

BitOctalSymbolEffect
setuid4000s in owner-xRun binary as file owner (e.g. passwd)
setgid2000s in group-xOn dir: new files inherit group
sticky1000t in others-xOnly owner can delete (e.g. /tmp)
chmod 4755 /usr/local/bin/myapp      # setuid + 755
chmod 2775 /shared/uploads           # setgid + 775
chmod 1777 /tmp                      # sticky + 777

Common Permission Patterns

ModeUse Case
644Regular file (configs, code)
600Sensitive file (SSH private key, secrets)
640Owner rw, group read (shared config)
755Executable script, directory
750Private executable / private dir
700Only owner (e.g. ~/.ssh)
444Read-only for everyone
777Almost always wrong — security smell

chmod Symbolic Operators

WhoOpPerm
u user+ addr read
g group- removew write
o others= setx execute
a allX exec only on dirs/already-exec files
chmod u+x deploy.sh             # add execute for owner
chmod go-rwx secret.key         # remove all from group + others
chmod a=r README.md             # read-only for all
chmod -R u=rwX,g=rX,o= /app     # safe recursive (capital X)
chmod u+s /usr/bin/myapp        # setuid

chown / chgrp

chown alice file                 # change owner
chown alice:devs file            # owner + group
chown :devs file                 # only group
chown -R deploy:deploy /opt/app  # recursive
chown --reference=other.txt file # copy ownership from other.txt
chgrp www-data /var/www          # change group only

4. Bash Scripting Cheat Sheet

Variables & Parameters

name="alice"           # no spaces around =
echo "$name"           # always quote variable expansions
echo "${name}_log"     # disambiguate with braces
readonly PI=3.14       # constant
unset name             # delete variable
export PATH="$PATH:/opt/bin"   # export to children

# Positional params inside a script
$0       # script name
$1..$9   # args 1..9
${10}    # 10th arg (need braces)
$#       # number of args
$@       # all args (each quoted separately when "$@")
$*       # all args (one string when "$*")
$?       # exit code of last command
$$       # current PID
$!       # PID of last backgrounded command

# Parameter expansion
${var:-default}    # use default if var unset/empty
${var:=default}    # also assign default
${var:?error}      # error if unset
${var:+alt}        # use alt if var IS set
${#var}            # string length
${var:2:4}         # substring from pos 2, length 4
${var/foo/bar}     # replace first foo with bar
${var//foo/bar}    # replace ALL foo
${var#prefix}      # strip shortest prefix
${var##prefix}     # strip longest prefix
${var%.txt}        # strip shortest suffix
${var%%.*}         # strip longest suffix

Test Operators

CategoryOperatorMeaning
Integer-eqequal
Integer-nenot equal
Integer-ltless than
Integer-leless or equal
Integer-gtgreater than
Integer-gegreater or equal
String= / ==equal
String!=not equal
String-zzero-length (empty)
String-nnon-empty
String< / >lexicographic (inside [[ ]])
File-eexists
File-fis regular file
File-dis directory
File-Lis symlink
File-r / -w / -xreadable / writable / executable
File-snon-empty (size > 0)
File-Oowned by you
File-Gowned by your group
Filea -nt ba newer than b
Filea -ot ba older than b

Control Structures

# if / elif / else
if [[ $count -gt 10 ]]; then
  echo "many"
elif [[ -z "$name" ]]; then
  echo "no name"
else
  echo "ok"
fi

# case
case "$env" in
  prod|production)  echo "prod" ;;
  dev|development)  echo "dev"  ;;
  *)                echo "unknown"; exit 1 ;;
esac

# for
for f in *.log; do
  gzip "$f"
done
for i in {1..5}; do echo "$i"; done
for ((i=0; i<10; i++)); do echo "$i"; done

# while / until
while read -r line; do
  echo "got: $line"
done < file.txt

count=0
until [[ $count -ge 5 ]]; do
  ((count++))
done

# functions
deploy() {
  local env="$1"      # 'local' avoids polluting global scope
  echo "deploying to $env"
  return 0
}
deploy prod

Common Idioms

# Strict mode (put at top of every serious script)
set -euo pipefail
IFS=$'\n\t'

# Check command exists
if ! command -v jq >/dev/null 2>&1; then
  echo "jq required" >&2; exit 1
fi

# Retry loop with backoff
for attempt in 1 2 3 4 5; do
  curl -fsSL "$URL" && break
  sleep $((2 ** attempt))
done

# Parse args (getopts)
while getopts "e:vh" opt; do
  case "$opt" in
    e) ENV="$OPTARG" ;;
    v) VERBOSE=1 ;;
    h) usage; exit 0 ;;
    *) usage; exit 1 ;;
  esac
done
shift $((OPTIND - 1))

# Trap cleanup
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM

# Read file line by line (safe)
while IFS= read -r line || [[ -n "$line" ]]; do
  process "$line"
done < input.txt

# Heredoc
cat <<EOF > /etc/myapp/config
host=$(hostname)
env=$ENV
EOF

Arrays

# Indexed array
servers=(web1 web2 web3)
echo "${servers[0]}"          # web1
echo "${servers[@]}"          # all elements
echo "${#servers[@]}"         # length = 3
servers+=(web4)               # append
unset 'servers[1]'            # delete element

for s in "${servers[@]}"; do echo "$s"; done

# Associative array (bash 4+)
declare -A user_role
user_role[alice]=admin
user_role[bob]=dev
echo "${user_role[alice]}"
for k in "${!user_role[@]}"; do
  echo "$k => ${user_role[$k]}"
done

Brace Expansion

echo file{1,2,3}.txt          # file1.txt file2.txt file3.txt
echo {a..e}                   # a b c d e
echo {01..05}                 # 01 02 03 04 05
echo {1..10..2}               # 1 3 5 7 9
mkdir -p project/{src,test,docs}/{api,web}
cp app.conf{,.bak}            # quick backup -> app.conf, app.conf.bak

5. Regular Expressions

BRE vs ERE Side-by-Side

FeatureBRE (grep, sed)ERE (egrep, grep -E, sed -E, awk)
Alternation|`
Grouping\( \)( )
One or more\++
Zero or one\??
Repetition\{m,n\}{m,n}
Zero or more**
Backrefs\1 \2\1 \2

Rule of thumb: use grep -E (ERE) — fewer backslashes, fewer tears.

Character Classes

ClassMatches
.Any single char (except newline)
[abc]a, b, or c
[^abc]NOT a, b, c
[a-z]Lowercase letters
[A-Za-z0-9_]”word” chars
\d / \DDigit / non-digit (PCRE)
\w / \WWord / non-word (PCRE)
\s / \SWhitespace / non-ws (PCRE)
[[:alpha:]]POSIX alpha
[[:digit:]]POSIX digit
[[:space:]]POSIX whitespace
[[:alnum:]]POSIX alphanumeric
[[:upper:]] / [[:lower:]]POSIX case
[[:punct:]]POSIX punctuation

Anchors & Quantifiers

PatternMeaning
^Start of line
$End of line
\bWord boundary
\BNon-word-boundary
\A / \ZStart / end of input (PCRE)
*0 or more
+1 or more
?0 or 1
{3}Exactly 3
{3,}3 or more
{3,5}Between 3 and 5
*? +?Lazy / non-greedy (PCRE)

10 Example Patterns

#MatchesPattern (ERE)
1Email (pragmatic)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}
2IPv4 address (loose)([0-9]{1,3}\.){3}[0-9]{1,3}
3IPv4 strict (0–255)`((25[0-5]
4URL (http/https)https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+
5Date YYYY-MM-DD`[0-9]{4}-(0[1-9]
6Time HH:MM:SS (24h)`([01][0-9]
7ISO timestamp`[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z
8E.164 phone number\+[1-9][0-9]{7,14}
9UUIDv4[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}
10Nginx access log IP^([0-9]{1,3}\.){3}[0-9]{1,3}
# Practical usage
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
sed -E 's/[0-9]{4}-[0-9]{2}-[0-9]{2}/REDACTED/g' file
awk '/ERROR|FATAL/ {print}' app.log

6. SSH Commands Cheat Sheet

Key Generation & Distribution

# Generate modern keypair (ed25519 preferred)
ssh-keygen -t ed25519 -C "pushkar@1buy.ai"
ssh-keygen -t rsa -b 4096 -C "fallback"     # legacy systems

# View public key
cat ~/.ssh/id_ed25519.pub

# Copy public key to server (easiest)
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@10.0.0.5

# Manually
cat ~/.ssh/id_ed25519.pub | ssh deploy@host \
  'mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
   cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

# Fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub

~/.ssh/config Snippets

Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3
  HashKnownHosts yes

Host prod-web
  HostName 10.0.0.5
  User deploy
  Port 2222
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes

Host bastion
  HostName bastion.1buy.ai
  User pushkar

# Jump through bastion to private host
Host db-private
  HostName 10.0.5.20
  User dbadmin
  ProxyJump bastion

# Now just: ssh prod-web   /   ssh db-private

Tunneling (L / R / D)

FlagDirectionUse Case
-LLocal → RemoteAccess remote service via local port
-RRemote → LocalExpose local service on remote host
-DDynamic (SOCKS)Use server as SOCKS5 proxy
# Local forward: localhost:5433 -> db.internal:5432 via bastion
ssh -L 5433:db.internal:5432 bastion
psql -h localhost -p 5433 -U app

# Remote forward: expose local dev server on remote:9000
ssh -R 9000:localhost:3000 user@remote

# Dynamic / SOCKS proxy on localhost:1080
ssh -D 1080 -C -N bastion
# Configure browser SOCKS5 -> localhost:1080

# Run tunnel in background, no shell
ssh -fN -L 5433:db:5432 bastion

SSH Agent

eval "$(ssh-agent -s)"          # start agent
ssh-add ~/.ssh/id_ed25519       # load key
ssh-add -l                      # list loaded keys
ssh-add -D                      # remove all
ssh -A user@host                # forward agent (use cautiously)

scp / sftp / rsync

# scp (simple)
scp file.txt user@host:/tmp/
scp -r dir/ user@host:/opt/
scp -P 2222 -i key.pem ...      # custom port, custom key
scp user@host:/etc/nginx.conf .

# sftp (interactive)
sftp user@host
> put localfile
> get remotefile
> ls -la
> bye

# rsync (best for repeated syncs)
rsync -avz --progress src/ user@host:/dst/
rsync -avz --delete src/ user@host:/dst/        # mirror (deletes extras)
rsync -avz -e "ssh -p 2222 -i key.pem" src/ host:/dst/
rsync -avz --exclude '.git' --exclude 'node_modules' src/ host:/dst/
rsync -avzn src/ host:/dst/                     # -n = dry run

sshd_config Hardening (server side)

# /etc/ssh/sshd_config
Port 2222                          # change default
PermitRootLogin no                 # never allow root login
PasswordAuthentication no          # keys only
PubkeyAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy pushkar
AllowGroups sshusers
Protocol 2
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

# Apply
sudo sshd -t                       # test config first!
sudo systemctl reload sshd

7. System Monitoring

top

top                           # launch
# Interactive keys:
P    sort by CPU
M    sort by memory
T    sort by time
N    sort by PID
k    kill process (asks PID + signal)
r    renice
c    toggle full command line
1    toggle per-CPU view
H    toggle threads
W    save config to ~/.toprc
q    quit
top -b -n 1                   # batch mode (for scripts)
top -p 1234,5678              # only specific PIDs
top -u deploy                 # only user deploy

htop

htop                          # nicer top
# F1 help  F2 setup  F3 search  F4 filter  F5 tree
# F6 sort  F7/F8 nice +/-  F9 kill  F10 quit
# Space = tag a process; u = filter by user

ps

ps aux                              # BSD style, all processes
ps -ef                              # System V style
ps -eLf                             # threads too
ps -eo pid,user,%cpu,%mem,cmd --sort=-%cpu | head
ps --ppid 1234                      # children of PID 1234
ps -C nginx                         # find by command name
pstree -p                           # process tree with PIDs

Memory & CPU

free -h                             # memory in human units
free -m -s 2                        # refresh every 2s
cat /proc/meminfo
cat /proc/cpuinfo

vmstat 1 5                          # 5 samples, 1 sec apart
# columns: r b | swpd free buff cache | si so | bi bo | in cs | us sy id wa st

iostat -xz 1                        # extended IO stats, skip idle
iostat -mx 2 5                      # MB/s, 5 samples

mpstat -P ALL 1 3                   # per-CPU stats

sar (historical perf)

sar -u 1 5                          # CPU
sar -r 1 5                          # memory
sar -b 1 5                          # IO
sar -n DEV 1 5                      # network
sar -q 1 5                          # load avg + run queue
sar -f /var/log/sysstat/sa15        # read past data (day 15)

Kernel & Hardware

dmesg -T | tail -50                 # kernel messages w/ timestamps
dmesg -w                            # follow
lscpu                               # CPU info
lsblk -f                            # block devs + filesystems
lspci                               # PCI devices
lsusb                               # USB devices
lsmod                               # loaded modules
uname -r                            # kernel version

Disk & Sockets

df -hT                              # disk usage by FS
df -i                               # inode usage
du -sh /var/log/*                   # size per item
du -sh * 2>/dev/null | sort -h      # sorted
ncdu /                              # interactive TUI

ss -tulnp                           # listening TCP/UDP sockets + processes
ss -s                               # socket summary
ss -tan state established           # established TCP conns

8. Network Commands

ip (the modern tool)

ip a                          # show all addresses
ip -br a                      # brief view
ip -4 a show eth0             # only IPv4 on eth0
ip r                          # routing table
ip -br link                   # link state of interfaces
ip neigh                      # ARP table (neighbors)

# Modify
sudo ip addr add 10.0.0.10/24 dev eth1
sudo ip addr del 10.0.0.10/24 dev eth1
sudo ip link set eth1 up
sudo ip link set eth1 down
sudo ip route add default via 10.0.0.1

ping & traceroute

ping -c 4 google.com          # 4 packets then stop
ping -i 0.2 -c 20 host        # 20 packets, 0.2s apart
ping -s 1472 -M do host       # MTU test (don't fragment)

traceroute -n 8.8.8.8         # numeric, no DNS
traceroute -T -p 443 host     # TCP traceroute on port 443
mtr -rwn 1.1.1.1              # report mode, wide, numeric

dig (DNS)

dig example.com               # default A record
dig +short A example.com
dig +short MX example.com
dig +short NS example.com
dig +short TXT example.com
dig AAAA example.com          # IPv6
dig @8.8.8.8 example.com      # query specific resolver
dig +trace example.com        # full delegation chain
dig -x 8.8.8.8                # reverse lookup
dig +noall +answer example.com

curl (essential flags)

FlagMeaning
-X METHODHTTP method (GET/POST/PUT/DELETE)
-H "Header: val"Add request header
-d 'data'POST body
--data-binary @filePOST file contents as body
-F 'k=v'multipart/form-data
-u user:passBasic auth
-o fileOutput to file
-OSave to remote filename
-LFollow redirects
-IHEAD request, headers only
-iInclude response headers
-sSilent (no progress)
-fFail on HTTP errors (4xx/5xx)
-kInsecure (skip cert validation)
-v / --trace-ascii -Verbose / full trace
--resolve host:port:ipOverride DNS
-w "%{http_code}\n"Print response info
curl -fsSL https://api.example.com/v1/users | jq
curl -X POST -H 'Content-Type: application/json' \
     -d '{"name":"alice"}' https://api/x
curl -w "code=%{http_code} time=%{time_total}\n" -o /dev/null -s https://x.com

ss (sockets)

ss -tulnp                     # TCP/UDP listening + process
ss -tan                       # all TCP
ss -tan state established
ss -tan state time-wait | wc -l
ss -tnp '( dport = :443 )'    # outbound HTTPS
ss -lx                        # unix sockets

nmap

nmap -sn 10.0.0.0/24                  # ping sweep (host discovery)
nmap -p 22,80,443 host                # specific ports
nmap -p- host                         # all 65535 ports
nmap -sV -p 1-1000 host               # service version detect
nmap -sS -O host                      # SYN scan + OS detect (needs root)
nmap -A host                          # aggressive: version+OS+scripts+traceroute
nmap --script vuln host               # vulnerability scripts

tcpdump

sudo tcpdump -i eth0                          # capture on eth0
sudo tcpdump -i any -nn port 80               # HTTP traffic, numeric
sudo tcpdump -i eth0 host 10.0.0.5            # to/from a host
sudo tcpdump -i eth0 -w out.pcap port 443     # write capture
sudo tcpdump -r out.pcap -nn                  # read capture
sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'   # SYN packets
sudo tcpdump -i eth0 -A port 80               # print ASCII payload

nc & telnet

nc -zv host 22 443 8080         # port check (z=scan, v=verbose)
nc -l -p 9000                   # listen on port 9000
nc host 9000                    # connect (chat)
nc -l 9000 > file.bin           # receive file
nc host 9000 < file.bin         # send file
echo "GET / HTTP/1.0" | nc host 80   # raw HTTP request
telnet host 25                  # legacy, still handy for SMTP debug

9. Process Management

ps Options You’ll Actually Use

CommandOutput
psYour shell’s processes only
ps auxAll processes (BSD): USER PID %CPU %MEM … COMMAND
ps -efAll processes (SysV): UID PID PPID C STIME TTY TIME CMD
ps -eLfInclude threads (LWP)
ps -fp 1234Full info for PID 1234
ps -ef --forestASCII tree view
`ps -eo pid,user,pri,ni,%cpu,%mem,stat,start,cmd —sort=-%memhead`

top / htop Keys

Keytophtop
h / F1HelpHelp
PSort by CPU
MSort by memory
F6Choose sort column
k / F9Kill (asks PID + signal)Kill (signal menu)
r / F7/F8ReniceNice +/-
F3Search
F4Filter
F5Tree view
q / F10QuitQuit

Signals Table

SignalNumDefaultMeaning
SIGHUP1TerminateReload config / parent shell exited
SIGINT2TerminateCtrl+C from terminal
SIGQUIT3Core dumpCtrl+\
SIGILL4Core dumpIllegal instruction
SIGABRT6Core dumpabort() called
SIGKILL9TerminateForce kill (cannot be caught)
SIGSEGV11Core dumpInvalid memory access
SIGPIPE13TerminateWrite to closed pipe
SIGTERM15TerminateGraceful kill (default for kill)
SIGUSR110TerminateApp-defined (often “rotate logs”)
SIGUSR212TerminateApp-defined
SIGCHLD17IgnoreChild stopped/exited
SIGCONT18ContinueResume a stopped process
SIGSTOP19StopStop (cannot be caught)
SIGTSTP20StopCtrl+Z

kill / pkill / killall / pgrep

kill 1234                      # SIGTERM (default)
kill -9 1234                   # SIGKILL by number
kill -SIGTERM 1234             # by name
kill -HUP $(pidof nginx)       # reload nginx config

pgrep -f gunicorn              # find PIDs by full cmdline
pgrep -af gunicorn             # also show cmdline
pkill -f gunicorn              # kill all matching
pkill -HUP -f nginx            # signal all matching

killall nginx                  # kill by exact process name
killall -u alice               # kill all of user alice's processes

nice / renice

# Nice value range: -20 (highest priority) to 19 (lowest)
nice -n 10 ./batch.sh                # start with niceness 10
renice 5 -p 1234                     # change running process
renice -n 15 -u alice                # all of alice's processes
ionice -c 3 -p 1234                  # IO scheduling class (3=idle)

nohup & Job Control

nohup ./long_job.sh > out.log 2>&1 &     # survives logout
disown %1                                # detach job from shell
./job.sh &                               # background
jobs                                     # list shell jobs
jobs -l                                  # with PIDs
fg %1                                    # foreground job 1
bg %1                                    # resume in background
Ctrl+Z                                   # suspend foreground job

systemctl

systemctl status nginx
systemctl start  nginx
systemctl stop   nginx
systemctl restart nginx
systemctl reload  nginx                  # signal reload, no restart
systemctl enable  --now nginx            # enable + start
systemctl disable --now nginx
systemctl is-active nginx
systemctl is-enabled nginx
systemctl list-units --type=service
systemctl list-units --failed
systemctl daemon-reload                  # after editing unit files
systemctl cat nginx                      # show unit file
systemctl edit nginx                     # override
systemctl mask nginx                     # prevent any start (strong disable)

journalctl

journalctl -u nginx                      # logs for nginx
journalctl -u nginx -f                   # follow (like tail -f)
journalctl -u nginx --since "10 min ago"
journalctl -u nginx --since "2026-05-26 09:00" --until "2026-05-26 10:00"
journalctl -u nginx -p err               # priority err and above
journalctl -k                            # kernel only (= dmesg)
journalctl -b                            # current boot
journalctl -b -1                         # previous boot
journalctl --disk-usage
journalctl --vacuum-time=7d              # keep only last 7 days

10. Cron Expressions

Field Syntax

┌───────────── minute        (0 - 59)
 ┌─────────── hour          (0 - 23)
 ┌───────── day of month  (1 - 31)
 ┌─────── month         (1 - 12)  or JAN-DEC
 ┌───── day of week   (0 - 6)   or SUN-SAT  (0 and 7 = Sun)

* * * * *  command_to_run

Special Characters

CharMeaningExample
*Every value* * * * * = every minute
,List0 9,12,18 * * * = 9am, 12pm, 6pm
-Range0 9-17 * * 1-5 = 9–5 Mon–Fri
/Step*/15 * * * * = every 15 minutes
L (extended)Last0 0 L * * = last day of month (not standard cron)
? (extended)No valueUsed in Quartz/Spring

20+ Real Examples

ExpressionRuns
* * * * *Every minute
*/5 * * * *Every 5 minutes
*/15 * * * *Every 15 minutes
0 * * * *Every hour on the hour
0 */2 * * *Every 2 hours
30 3 * * *3:30 AM every day
0 0 * * *Midnight every day
0 9 * * 1-59 AM weekdays
0 9 * * 19 AM every Monday
0 9 * * 09 AM every Sunday
0 9-17 * * 1-5Every hour, 9–5, weekdays
0 0 1 * *Midnight on 1st of month
0 0 1 1 *Midnight on Jan 1 (yearly)
0 0 1,15 * *1st and 15th at midnight
0 2 * * 62 AM every Saturday (weekly backup)
15 14 1 * *2:15 PM on the 1st of each month
0 22 * * 1-510 PM weekdays
5 4 * * SUN4:05 AM every Sunday
0 4 1-7 * 14 AM on first Monday of month (kind of)
*/10 9-17 * * 1-5Every 10 min, business hours, weekdays
0 0 */3 * *Midnight every 3 days
0 12 * * 1#1Noon on first Monday (extended/Quartz)

@ Shortcuts

ShortcutEquivalentMeaning
@rebootOnce at startup
@yearly / @annually0 0 1 1 *Once a year
@monthly0 0 1 * *Once a month
@weekly0 0 * * 0Once a week (Sunday)
@daily / @midnight0 0 * * *Once a day
@hourly0 * * * *Once an hour

Managing Crontabs

crontab -l                    # list current user's crontab
crontab -e                    # edit (uses $EDITOR)
crontab -r                    # remove (be careful!)
crontab -u alice -l           # list alice's crontab (need root)

# System-wide files:
/etc/crontab                  # has a 'user' column
/etc/cron.d/*                 # drop-in files (also need user column)
/etc/cron.{hourly,daily,weekly,monthly}/   # scripts dir (no schedule)

# Always set env + redirect output in cron jobs:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@1buy.ai

0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# Useful for verifying: test online at crontab.guru

Keyboard Shortcuts and Aliases

Section 35 of 39 · ~7 min

Speed comes from muscle memory, not from typing faster. Until these shortcuts live in your fingers, you won’t feel the productivity boost. Pick 10 from this list, drill them this week, then come back for the next 10.

Terminal Keyboard Shortcuts (Readline / Bash)

Cursor & Editing

ShortcutAction
Ctrl+AMove to beginning of line
Ctrl+EMove to end of line
Ctrl+B / Ctrl+FBack / forward one character
Alt+B / Alt+FBack / forward one word
Ctrl+UCut from cursor to beginning of line
Ctrl+KCut from cursor to end of line
Ctrl+WCut previous word
Alt+DCut next word
Ctrl+YPaste (yank) last cut
Ctrl+TSwap current and previous character
Alt+TSwap current and previous word
Ctrl+_Undo last edit
Ctrl+LClear screen (keeps current line)

Process & Session

ShortcutAction
Ctrl+CSend SIGINT (interrupt) to foreground process
Ctrl+ZSend SIGTSTP (suspend) — resume with fg or bg
Ctrl+DEOF — exits shell if line is empty
Ctrl+SFreeze terminal output (XOFF) — try not to hit this
Ctrl+QResume frozen terminal (XON)
Ctrl+\SIGQUIT — like Ctrl+C but with core dump
ShortcutAction
Ctrl+RReverse-i-search through history (press again for next match)
Ctrl+GCancel reverse search
Ctrl+P / Ctrl+NPrevious / next history entry
Alt+.Insert last argument of previous command (press repeatedly)

History Expansion (the bang stuff)

ExpansionMeaning
!!Previous command in full (e.g. sudo !!)
!$Last argument of previous command
!*All arguments of previous command
!NCommand number N from history
!-NN commands back
!abcMost recent command starting with abc
!?abc?Most recent command containing abc
^old^newRe-run previous command, replacing first old with new
!!:gs/old/new/Re-run previous command, global substitution
# Forgot sudo?
$ apt update
E: Permission denied
$ sudo !!         # expands to:  sudo apt update

# Typo in long command?
$ systemctl restrat nginx
$ ^restrat^restart

# Reuse last argument
$ mkdir /opt/myapp
$ cd !$           # cd /opt/myapp

Bash History Management

# View / search
history                   # show full history
history 20                # last 20
history | grep ssh        # filter

# Re-run by number
!457                      # run history entry 457
!-2                       # run 2 commands ago

# Delete entries
history -d 457            # delete entry 457
history -c                # clear ALL history (session)
history -w                # write current session to file

# Tune behavior in ~/.bashrc
HISTSIZE=10000                       # in-memory entries
HISTFILESIZE=20000                   # on-disk entries
HISTCONTROL=ignoreboth:erasedups     # skip dupes & lines starting with space
HISTTIMEFORMAT="%F %T "              # add timestamps
HISTIGNORE="ls:ll:pwd:exit:clear:history"   # never record these
shopt -s histappend                  # append, don't overwrite
shopt -s cmdhist                     # multi-line commands as one entry

# Share history across sessions in real time:
PROMPT_COMMAND='history -a; history -n'

Tip: Prepending a command with a space (when HISTCONTROL includes ignorespace or ignoreboth) keeps it out of history — handy when typing one-off commands with secrets.

Tab Completion

TriggerWhat it completes
Tab (once)Complete if unambiguous
Tab Tab (double)Show all possible completions
After cd Directories only
After ssh / scp Hostnames from ~/.ssh/config and ~/.ssh/known_hosts
After $Environment variable names
After ~Usernames (for home dirs)
After git , kubectl , docker Subcommands, flags, resources (with completion installed)
# Enable programmable completion (most distros do this by default)
# /etc/bash_completion or in ~/.bashrc:
[ -r /usr/share/bash-completion/bash_completion ] && \
  . /usr/share/bash-completion/bash_completion

# Add tool-specific completion
source <(kubectl completion bash)
source <(helm completion bash)
complete -F __start_kubectl k        # so 'k <tab>' also works

# Show completions for a command
complete -p git

20+ Useful Aliases for ~/.bashrc

# ---------- Navigation & Listing ----------
alias ll='ls -lah --color=auto'           # long, human, hidden, colored
alias la='ls -A --color=auto'             # show dotfiles (no . and ..)
alias l='ls -CF --color=auto'             # quick column listing
alias ..='cd ..'                          # up one
alias ...='cd ../..'                      # up two
alias ....='cd ../../..'                  # up three
alias -- -='cd -'                         # toggle to previous dir

# ---------- Safety ----------
alias rm='rm -i'                          # ask before remove
alias cp='cp -i'                          # ask before overwrite
alias mv='mv -i'                          # ask before overwrite
alias mkdir='mkdir -pv'                   # parents + verbose

# ---------- Search / Filters ----------
alias grep='grep --color=auto'            # colored matches
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'

# ---------- Disk / System ----------
alias df='df -hT'                         # human + filesystem type
alias du='du -h --max-depth=1'            # human, one level
alias free='free -h'                      # human memory
alias ports='ss -tulnp'                   # what's listening?
alias psg='ps aux | grep -v grep | grep -iE'  # process search

# ---------- Network ----------
alias myip='curl -s https://ifconfig.me && echo'   # public IP
alias localip="ip -4 -br a | grep -v '^lo'"        # local IPs
alias weather='curl -s wttr.in/?format=3'          # quick weather
alias headers='curl -sI'                           # response headers only

# ---------- Git ----------
alias g='git'
alias gs='git status -sb'                 # short branch + status
alias gco='git checkout'
alias gcm='git commit -m'
alias gp='git pull --rebase'
alias gpush='git push'
alias gl='git log --oneline --graph --decorate -20'
alias gd='git diff'

# ---------- DevOps Tools ----------
alias k='kubectl'                         # the universal short form
alias kgp='kubectl get pods'
alias kgs='kubectl get svc'
alias kdp='kubectl describe pod'
alias kx='kubectl config use-context'
alias kns='kubectl config set-context --current --namespace'
alias d='docker'
alias dc='docker compose'
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias tf='terraform'
alias jc='journalctl -u'                  # usage: jc nginx -f
alias sc='systemctl'                      # usage: sc status nginx

# ---------- Misc Quality of Life ----------
alias path='echo -e ${PATH//:/\\n}'       # print PATH one entry per line
alias now='date "+%Y-%m-%d %H:%M:%S"'     # quick timestamp
alias reload='source ~/.bashrc'           # re-source after edits
alias ports-listening='sudo lsof -i -P -n | grep LISTEN'
alias ..git='cd $(git rev-parse --show-toplevel)'  # jump to repo root

Rationale. Aliases like ll, .., k, d, gs shave 5–10 keystrokes off the dozens of times you type them daily. Safety aliases (rm -i, cp -i) save you from career-ending mistakes — yes, even seniors have done rm -rf / at 2 AM. The myip/localip/ports ones replace “wait, what’s that command again?” with one word.

To make aliases work in non-interactive contexts (rare but useful for scripts), put them in ~/.bash_aliases and source that file from ~/.bashrc.

Useful Bash Functions

# --- mkcd: make a directory and cd into it in one step ---
mkcd() {
  if [[ -z "$1" ]]; then
    echo "Usage: mkcd <dir>" >&2
    return 1
  fi
  mkdir -p -- "$1" && cd -P -- "$1"
}
# Usage:  mkcd /opt/projects/new-app

# --- extract: smart extractor for any common archive ---
extract() {
  if [[ -z "$1" ]]; then
    echo "Usage: extract <archive>" >&2
    return 1
  fi
  if [[ ! -f "$1" ]]; then
    echo "extract: '$1' is not a regular file" >&2
    return 1
  fi
  case "$1" in
    *.tar.bz2|*.tbz2)   tar -xjvf "$1"   ;;
    *.tar.gz|*.tgz)     tar -xzvf "$1"   ;;
    *.tar.xz|*.txz)     tar -xJvf "$1"   ;;
    *.tar.zst)          tar --zstd -xvf "$1" ;;
    *.tar)              tar -xvf  "$1"   ;;
    *.bz2)              bunzip2   "$1"   ;;
    *.gz)               gunzip    "$1"   ;;
    *.xz)               unxz      "$1"   ;;
    *.zip)              unzip     "$1"   ;;
    *.rar)              unrar x   "$1"   ;;
    *.7z)               7z x      "$1"   ;;
    *.Z)                uncompress "$1"  ;;
    *) echo "extract: don't know how to handle '$1'" >&2; return 1 ;;
  esac
}
# Usage:  extract release-v2.4.1.tar.gz

# --- bak: quick timestamped backup of a file ---
bak() {
  if [[ -z "$1" ]]; then
    echo "Usage: bak <file>" >&2
    return 1
  fi
  if [[ ! -e "$1" ]]; then
    echo "bak: '$1' does not exist" >&2
    return 1
  fi
  local ts
  ts=$(date +%Y%m%d-%H%M%S)
  cp -a -- "$1" "$1.bak.$ts" && \
    echo "Backed up to $1.bak.$ts"
}
# Usage:  bak /etc/nginx/nginx.conf
#         -> /etc/nginx/nginx.conf.bak.20260526-101530

# --- Bonus: cdf -- cd into the directory of a file ---
cdf() {
  if [[ -z "$1" ]]; then
    echo "Usage: cdf <file>" >&2
    return 1
  fi
  cd -- "$(dirname -- "$1")"
}

# --- Bonus: fkill -- fuzzy kill (needs fzf) ---
fkill() {
  local pid
  pid=$(ps -ef | sed 1d | fzf -m | awk '{print $2}')
  [[ -n "$pid" ]] && echo "$pid" | xargs kill -"${1:-15}"
}

Drop these into ~/.bashrc (or a sourced ~/.bash_functions file), run source ~/.bashrc, and they become first-class commands. The extract function alone will save you “wait, was it xjvf or xzvf?” lookups for the rest of your career. Set it up once, and it pays off for life.

Advanced Linux for DevOps

Section 36 of 39 · ~13 min

This is where the real DevOps game begins. Containers, kernel tuning, boot internals, LVM, RAID — this is the layer that completes the transition from frontend developer to six-figure (USD) DevOps engineer. An interviewer will ask you: “How does a Docker container work under the hood?” — if you can explain namespaces + cgroups + overlayfs + capabilities, you’re already in the top 20% of candidates.

Containers Are Not Magic — They Are Linux Features

A container = namespaces (isolation) + cgroups (resource limits) + overlayfs (layered FS) + capabilities (privilege control). Docker, containerd, Podman — they all sit on this foundation. You don’t need a full hypervisor like a VM; the kernel is shared with the host, but each process sees an isolated world as if it had its own OS.

Linux Namespaces — A Process Gets Its Own Universe

Namespaces show a process group only a subset of the system. Inside a container, a process thinks it’s PID 1, with its own network stack and its own filesystem root.

NamespaceFlagWhat It IsolatesPractical Effect
pidCLONE_NEWPIDProcess IDsThe container’s PID 1 might be PID 14523 on the host
mntCLONE_NEWNSMount points / filesystem viewThe container sees its own /, not the host’s /
netCLONE_NEWNETNetwork interfaces, routes, iptables, socketsIts own eth0, its own loopback, its own iptables rules
ipcCLONE_NEWIPCSysV IPC, POSIX message queuesShared memory segments isolated
utsCLONE_NEWUTSHostname, domain nameEach container gets its own hostname (hostname command)
userCLONE_NEWUSERUID/GID mappingsThe container’s root (UID 0) maps to unprivileged user 100000 on the host
cgroupCLONE_NEWCGROUPCgroup root viewThe container sees only a subset of the cgroup hierarchy

Manual Namespace Creation — Build a Container Without Docker

# Create a new PID + network + mount namespace
sudo unshare --pid --net --mount --uts --ipc --fork --mount-proc bash

# Go inside and check
echo $$                    # Shows PID 1 (it's something else on the host)
hostname container-demo    # Changes only inside this namespace
ps aux                     # Only the processes in this namespace
ip addr                    # Only loopback (network is isolated)

# Everything is cleaned up on exit
exit

This is exactly what Docker does — it just adds an image, overlayfs, and orchestration on top.

Inspect Namespaces — lsns and /proc

# List all namespaces
lsns

# Specific type
lsns -t net
lsns -t pid

# Look at your own shell's namespaces
ls -l /proc/$$/ns/
# Output:
# lrwxrwxrwx 1 user user 0 May 26 10:23 cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 ipc    -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 mnt    -> 'mnt:[4026531840]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 net    -> 'net:[4026531992]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 pid    -> 'pid:[4026531836]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 user   -> 'user:[4026531837]'
# lrwxrwxrwx 1 user user 0 May 26 10:23 uts    -> 'uts:[4026531838]'

# Enter a container's namespace (once you have the Docker PID)
sudo nsenter --target 14523 --pid --net --mount

cgroups v2 (Ubuntu 24.04 Default)

cgroups (control groups) limit and account for the resource usage of processes. Ubuntu 24.04 uses the unified cgroup v2 — a single hierarchy at /sys/fs/cgroup.

# Verify cgroup v2
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)

# Available controllers
cat /sys/fs/cgroup/cgroup.controllers
# cpu io memory pids hugetlb cpuset rdma misc

# Create a new cgroup and set limits
sudo mkdir /sys/fs/cgroup/myapp
echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# CPU: 50% of one core (50000 us out of 100000 us period)
echo "50000 100000" | sudo tee /sys/fs/cgroup/myapp/cpu.max

# Memory: 512 MB hard limit
echo "536870912" | sudo tee /sys/fs/cgroup/myapp/memory.max

# PIDs: max 100 processes
echo "100" | sudo tee /sys/fs/cgroup/myapp/pids.max

# IO: weight (10-1000, default 100), per-device limits also possible
echo "default 50" | sudo tee /sys/fs/cgroup/myapp/io.weight

# Put a process into the cgroup
echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs

# Check current usage
cat /sys/fs/cgroup/myapp/memory.current
cat /sys/fs/cgroup/myapp/cpu.stat

Tip: Docker’s --memory 512m --cpus="0.5" writes to exactly these cgroup files internally. A Kubernetes pod’s resources.limits also end up here via kubelet → containerd → runc.

How Docker Stitches It All Together

LayerTechnologyRole
IsolationNamespaces (pid, net, mnt, uts, ipc, user)Each process in its own universe
Resource limitscgroups v2CPU, memory, IO, PIDs cap
Filesystemoverlayfs (lowerdir + upperdir = merged)Image layers + writable container layer
PrivilegesCapabilities (drop most by default)Container root != host root
Securityseccomp, AppArmor/SELinuxSyscall filtering, MAC
Networkingveth pair + bridge (docker0) + iptablesContainer-to-host-to-internet
# Try overlayfs yourself
mkdir -p /tmp/ovl/{lower,upper,work,merged}
echo "from image" > /tmp/ovl/lower/file.txt

sudo mount -t overlay overlay \
  -o lowerdir=/tmp/ovl/lower,upperdir=/tmp/ovl/upper,workdir=/tmp/ovl/work \
  /tmp/ovl/merged

cat /tmp/ovl/merged/file.txt              # "from image"
echo "container write" > /tmp/ovl/merged/file.txt
cat /tmp/ovl/lower/file.txt               # Still "from image" — read-only
cat /tmp/ovl/upper/file.txt               # "container write" — copy-on-write

Linux Capabilities — The Modern Replacement for Setuid

In old Unix, a binary was either root (UID 0, all-powerful) or a normal user. Linux capabilities break root’s powers into 40+ smaller chunks. ping needs a raw socket — it used to be setuid-root, now it’s given only CAP_NET_RAW.

# Look at your shell's capabilities
capsh --print

# Look at a file's capabilities
getcap /usr/bin/ping
# /usr/bin/ping cap_net_raw=ep

# Give Nginx the capability to bind port 80 (a privileged port) without running as root
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/nginx
getcap /usr/local/bin/nginx
# /usr/local/bin/nginx cap_net_bind_service=ep

# Common capabilities
# CAP_NET_BIND_SERVICE  - bind ports < 1024
# CAP_NET_ADMIN         - network config (ip, iptables)
# CAP_SYS_ADMIN         - "almost root" — very powerful, be careful
# CAP_CHOWN             - change file ownership
# CAP_DAC_OVERRIDE      - bypass permissions
# CAP_KILL              - send a signal to any process
# CAP_SETUID/SETGID     - change UID/GID

# Remove a capability
sudo setcap -r /usr/local/bin/nginx

# Docker drops a lot of caps by default
docker run --rm alpine sh -c 'apk add -q libcap; capsh --print' | grep Current
# Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,...
# (NOT full root capabilities)

Performance Tuning — sysctl, ulimit, I/O Schedulers

Kernel Parameters via sysctl

# Check the current value
sysctl vm.swappiness
sysctl net.core.somaxconn

# Runtime change (gone after reboot)
sudo sysctl -w vm.swappiness=10
sudo sysctl -w net.core.somaxconn=4096

# Persist it — create a file in /etc/sysctl.d/
sudo tee /etc/sysctl.d/99-devops-tuning.conf <<'EOF'
# Memory: prefer dropping cache over swapping (DB/app servers)
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.overcommit_memory = 1

# Network: high-connection web server
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 5000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65535

# File descriptors
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288

# Connection tracking (for nat/firewalls)
net.netfilter.nf_conntrack_max = 1048576
EOF

# Apply it
sudo sysctl --system

# All current values
sysctl -a | less

ulimit — Per-Process Resource Limits

# Current limits
ulimit -a

# Open files limit (the most common bottleneck)
ulimit -n               # soft
ulimit -Hn              # hard

# Raise it temporarily (current shell only)
ulimit -n 65535

# Permanent — /etc/security/limits.conf or /etc/security/limits.d/
sudo tee /etc/security/limits.d/99-app.conf <<'EOF'
# domain  type  item   value
*         soft  nofile 65535
*         hard  nofile 1048576
*         soft  nproc  32768
*         hard  nproc  65535
www-data  soft  nofile 100000
www-data  hard  nofile 200000
EOF

# For systemd services — in the service unit file
# [Service]
# LimitNOFILE=1048576
# LimitNPROC=65535

I/O Schedulers

# Look at the current scheduler
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none

# 'none' is best for NVMe — NVMe has its own queue management
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler

# For SATA SSD / HDD use 'mq-deadline' or 'bfq'
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler

# Persist via udev rule
sudo tee /etc/udev/rules.d/60-ioschedulers.rules <<'EOF'
# NVMe
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
# SSD
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
# HDD
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
EOF

Caution: The most common cause of production outages — ulimit -n set too low. Under load, Node.js / Java apps crash at 1024 file descriptors with “EMFILE: too many open files”. Always set it to 65535+.

Boot Process Deep Dive

Press the power button → kernel running → systemd → login prompt. There are many layers in between:

StageUEFI BootLegacy BIOS
1. FirmwareUEFI firmware (modern)BIOS (legacy)
2. Boot loader locationEFI System Partition (ESP), FAT32, mounted at /boot/efiMBR (first 512 bytes of disk)
3. Boot loaderGRUB EFI binary (/boot/efi/EFI/ubuntu/grubx64.efi)GRUB stage 1 → stage 2
4. Config/boot/grub/grub.cfg (generated)/boot/grub/grub.cfg
5. Kernel + initramfsLoaded from /boot/Loaded from /boot/
6. Secure BootSupported (shim + signed kernel)Not supported
7. Disk size> 2 TB OK (GPT)2 TB limit (MBR)
# Check whether you booted in UEFI mode
ls /sys/firmware/efi && echo "UEFI" || echo "BIOS"

# Look at the ESP partition
df -h /boot/efi
mount | grep /boot/efi
# /dev/nvme0n1p1 on /boot/efi type vfat ...

# EFI boot entries
sudo efibootmgr -v
# BootCurrent: 0001
# Boot0001* ubuntu  HD(1,GPT,...)/File(\EFI\ubuntu\shimx64.efi)

GRUB2 Configuration

# Edit defaults
sudo nano /etc/default/grub

# Common settings:
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX=""

# Add a kernel parameter (example: disable IPv6, enable cgroup v2 — already default in 24.04)
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash ipv6.disable=1"

# Regenerate grub.cfg
sudo update-grub
# OR:  sudo grub-mkconfig -o /boot/grub/grub.cfg

# Rebuild initramfs (after a driver change or crypto change)
sudo update-initramfs -u -k all

# Kernel command line currently running
cat /proc/cmdline

initramfs (initial RAM filesystem)

Right after the kernel boots, it needs drivers to mount the real root filesystem (NVMe, RAID, LVM, encrypted disk). All of this happens in a temporary RAM-based filesystem — the initramfs. /boot/initrd.img-* is a compressed cpio archive.

# See what's inside
lsinitramfs /boot/initrd.img-$(uname -r) | head -50

# Rebuild
sudo update-initramfs -u -k all

systemd Boot Targets & Analysis

# Current target
systemctl get-default          # graphical.target or multi-user.target

# Set it
sudo systemctl set-default multi-user.target

# Total boot time for a server
systemd-analyze
# Startup finished in 3.124s (kernel) + 8.456s (userspace) = 11.580s

# Which service is slow?
systemd-analyze blame
# 4.123s snapd.service
# 2.456s cloud-init.service
# 1.234s apt-daily.service

# Critical path (the serial dependency chain of boot)
systemd-analyze critical-chain
systemd-analyze critical-chain nginx.service

# Visual SVG plot
systemd-analyze plot > /tmp/boot-plot.svg
# Open it in a browser — you get a beautiful timeline

LVM — Logical Volume Manager (Production Essential)

LVM abstracts your disks. You pool multiple physical disks (PVs) into a group (VG), and carve flexible chunks (LVs) out of it — runtime resize, snapshots, and online migration all come for free.

The LVM stack, top to bottom: Filesystems (/ ext4, /var/lib/docker, /data xfs) sit on LVs (lv-root 20G, lv-docker 50G, lv-data 200G), which are carved from the VG (vg_main — Volume Group, 500 GB total, 230 GB free), which pools the PVs (/dev/nvme0n1p3 200G, /dev/nvme1n1 200G, /dev/nvme2n1 100G — physical disks / partitions / raw block devices).

Full End-to-End LVM Example

# 1. Create Physical Volumes (turn raw disks into LVM-managed devices)
sudo pvcreate /dev/nvme1n1 /dev/nvme2n1
sudo pvs

# 2. Create a Volume Group (a pool of PVs)
sudo vgcreate vg_main /dev/nvme1n1 /dev/nvme2n1
sudo vgs
sudo vgdisplay vg_main

# 3. Create Logical Volumes
sudo lvcreate -L 50G -n lv-docker vg_main
sudo lvcreate -L 200G -n lv-data vg_main
# Or all the free space:
# sudo lvcreate -l 100%FREE -n lv-data vg_main
sudo lvs

# 4. Create filesystems
sudo mkfs.ext4 /dev/vg_main/lv-docker
sudo mkfs.xfs  /dev/vg_main/lv-data

# 5. Mount them
sudo mkdir -p /var/lib/docker /data
sudo mount /dev/vg_main/lv-docker /var/lib/docker
sudo mount /dev/vg_main/lv-data /data

# 6. Add to /etc/fstab (UUID is best practice)
echo "/dev/vg_main/lv-docker /var/lib/docker ext4 defaults,noatime 0 2" | sudo tee -a /etc/fstab
echo "/dev/vg_main/lv-data   /data           xfs  defaults,noatime 0 2" | sudo tee -a /etc/fstab

# ===== NOW THE LIVE EXTEND — this is the real power of LVM =====

# The disk is filling up. Grow lv-data by another 100G
sudo lvextend -L +100G /dev/vg_main/lv-data
sudo xfs_growfs /data                    # XFS online grow

# For ext4:
# sudo lvextend -L +50G /dev/vg_main/lv-docker
# sudo resize2fs /dev/vg_main/lv-docker

# Combined (lvextend + filesystem resize in a single command):
sudo lvextend -r -L +50G /dev/vg_main/lv-docker

# Out of space in the VG? Add a new disk
sudo pvcreate /dev/nvme3n1
sudo vgextend vg_main /dev/nvme3n1
sudo lvextend -r -L +500G /dev/vg_main/lv-data

# ===== SNAPSHOTS — for backups =====
sudo lvcreate -L 10G -s -n lv-data-snap /dev/vg_main/lv-data
sudo mount -o ro /dev/vg_main/lv-data-snap /mnt/snapshot
tar czf /backup/data-$(date +%F).tar.gz -C /mnt/snapshot .
sudo umount /mnt/snapshot
sudo lvremove -f /dev/vg_main/lv-data-snap

Tip: Cloud relevance — why LVM on AWS? To stripe or pool multiple EBS volumes. Take 4×500GB gp3 volumes on an instance and make a single 2TB volume with LVM — combined IOPS goes up and growing it is easy.

RAID — Redundancy and Performance

RAID LevelMin DisksRedundancyCapacityRead/WriteUse Case
RAID 0 (stripe)2None — 1 fail = data gone100%Fastest R + WScratch / cache (cloud: avoid)
RAID 1 (mirror)21 disk fail OK50%Fast R, normal WOS disk, boot
RAID 5 (parity)31 disk fail OK(N-1)/NGood R, slow W (parity calc)Bulk storage (legacy)
RAID 6 (double parity)42 disks fail OK(N-2)/NGood R, slower WLarge arrays, long rebuilds
RAID 10 (1+0)41 per mirror pair OK50%Fastest R + W with redundancyDatabases, production
# Software RAID with mdadm
sudo apt install mdadm

# RAID 10 from 4 disks
sudo mdadm --create /dev/md0 --level=10 --raid-devices=4 \
  /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1 /dev/nvme4n1

# Status
cat /proc/mdstat
sudo mdadm --detail /dev/md0

# Save config (otherwise it disappears on boot)
sudo mdadm --detail --scan | sudo tee -a /etc/mdadm/mdadm.conf
sudo update-initramfs -u

# Use as an LVM PV or a direct filesystem
sudo mkfs.xfs /dev/md0

Caution: Cloud reality — AWS EBS / GCP PD / Azure Managed Disks are already replicated (3 copies cross-AZ). You rarely run RAID in the cloud — the provider already does it. Just attach multiple EBS volumes + LVM stripe for IOPS — that’s the cloud pattern. mdadm is only critical on bare-metal / on-prem.

Filesystem Tuning

# ext4: lower reserved blocks (root-only) from 5% to 1% — save space on big data disks
sudo tune2fs -m 1 /dev/vg_main/lv-data

# Turn off forced fsck interval / mount count (production servers — avoid fsck on boot)
sudo tune2fs -c 0 -i 0 /dev/vg_main/lv-data

# Show all options
sudo tune2fs -l /dev/vg_main/lv-data

# Mount options that matter in production
# noatime    — don't update atime on every read (huge perf gain, esp. for DBs, web)
# nodiratime — skip directory atime
# discard    — SSD TRIM on delete (alternative: fstrim weekly via timer)
# nofail     — don't fail boot if the disk is missing (cloud detached EBS scenario)
# nosuid,nodev,noexec — security for /tmp, /var

# /etc/fstab example for production
# UUID=...  /data       ext4  defaults,noatime,nodiratime,nofail  0  2
# UUID=...  /tmp        tmpfs defaults,nosuid,nodev,noexec,size=2G 0  0

# tmpfs (RAM-backed) in production — fast scratch / build directories
sudo mount -t tmpfs -o size=4G,mode=1777 tmpfs /var/cache/build

Note: Quick recap — A Docker container = unshare + cgroups + overlayfs + capability drop. Boot = UEFI → GRUB → kernel → initramfs → systemd. LVM = flexible disks, runtime grow. RAID = redundancy. You rarely need RAID in the cloud, but LVM and tuning are useful everywhere. All of these show up in interviews as the answer to “how does X work internally?”

Linux in Cloud Context

Section 37 of 39 · ~11 min

So far we’ve looked at Linux on a single isolated machine. In the cloud the same Linux is running, but with different patterns — ephemeral instances, the metadata service, cloud-init bootstrapping, EBS attach/grow, replacing SSH with SSM. A developer-turned-DevOps engineer spends roughly 70% of each day on AWS Linux instances + Kubernetes nodes. This section covers that 70%.

AWS EC2 Linux Distributions

DistroDefault UserPackage MgrNotes
Amazon Linux 2023 (AL2023)ec2-userdnfAWS optimized, kernel 6.x, glibc 2.34, free, fast-boot. Default choice for AWS.
Amazon Linux 2 (legacy)ec2-useryumEOL 2026. Migrate to AL2023.
Ubuntu Server 24.04 LTSubuntuaptMost popular, huge community, debs, snap. Best for general use.
Rocky Linux 9 / RHEL 9rocky / ec2-userdnfEnterprise, RHEL-compatible, FIPS, certifications.
Debian 12adminaptMinimal, stable, like Ubuntu’s parent.
Custom AMIWhatever you bakeWhateverPre-configured golden image via Packer.

Tip: Practical pick — Production Kubernetes nodes → Amazon Linux 2023 (also consider Bottlerocket). General app servers → Ubuntu 24.04. Enterprise compliance → RHEL/Rocky.

Instance Metadata Service (IMDS) — The Cloud’s /proc

Every EC2 instance gets a special endpoint: http://169.254.169.254. From here the instance can learn everything about itself — region, AZ, instance type, IAM role credentials, user-data, public IP. SDKs (boto3, aws-cli) also pull their IAM creds from here.

# IMDSv1 (legacy, insecure — SSRF attacks possible)
curl http://169.254.169.254/latest/meta-data/

# IMDSv2 (token-based, mandatory in new accounts) — SECURE PATTERN
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# Use token in all requests
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/

# Useful endpoints
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/security-groups
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Tags (if enabled on instance)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/tags/instance/

# User-data (the bootstrap script)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/user-data

Caution: IMDSv1 is vulnerable to SSRF attacks — the Capital One breach happened through it. Always enforce IMDSv2 (HttpTokens=required) in the instance metadata options.

EBS Volumes — Attach, Format, Mount, Grow

# After attaching a new volume, look at the device list
lsblk
# NAME         SIZE TYPE MOUNTPOINTS
# nvme0n1       30G disk
# `-nvme0n1p1   30G part /
# nvme1n1      100G disk                  <-- new EBS volume

# No filesystem on it yet
sudo file -s /dev/nvme1n1
# /dev/nvme1n1: data        (i.e. empty)

# Create a filesystem
sudo mkfs.ext4 /dev/nvme1n1
# Modern alternative: sudo mkfs.xfs /dev/nvme1n1

# Create a mountpoint and mount it
sudo mkdir -p /data
sudo mount /dev/nvme1n1 /data

# /etc/fstab — UUID best practice; nofail critical for cloud
# (if the instance reboots with the volume detached, boot won't fail)
UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1)
echo "UUID=$UUID  /data  ext4  defaults,nofail,noatime  0  2" | sudo tee -a /etc/fstab

# Verify
sudo mount -a
df -h /data

Grow EBS Volume In-Place (Zero Downtime)

# Step 1: Increase the volume size from the AWS console or CLI
aws ec2 modify-volume --volume-id vol-0abc123 --size 200

# Wait for "optimizing" state
aws ec2 describe-volumes-modifications --volume-id vol-0abc123

# Step 2: Inside the instance, the kernel sees the new size
lsblk
# nvme1n1      200G disk
# `-nvme1n1p1  100G part /data         <-- partition is still 100G

# Step 3: Grow the partition (if you're using a partition)
sudo growpart /dev/nvme1n1 1

# Filesystem directly on the raw device (no partition) — skip this step

# Step 4: Grow the filesystem
# ext4:
sudo resize2fs /dev/nvme1n1p1
# OR direct:
sudo resize2fs /dev/nvme1n1

# xfs (must be mounted):
sudo xfs_growfs /data

# Verify
df -h /data
# /dev/nvme1n1   200G ...

Instance Store vs EBS

EBSInstance Store
PersistencePersistent — survives stop/startEphemeral — gone on stop/terminate
Replication3× within AZNone
SpeedNetwork-attached (NVMe over fabric)Physically attached NVMe SSD — fastest
SnapshotsYesNo
Detach & reattachYesNo
Use caseOS root, databases, app dataCache, scratch, temp big-data shuffle

SSH to Cloud Instances

# Classic: keypair-based SSH
chmod 400 ~/.ssh/my-key.pem
ssh -i ~/.ssh/my-key.pem ec2-user@54.221.10.45    # Amazon Linux
ssh -i ~/.ssh/my-key.pem ubuntu@54.221.10.45      # Ubuntu

# A convenient alias in ~/.ssh/config
cat >> ~/.ssh/config <<'EOF'
Host prod-web
  HostName 54.221.10.45
  User ubuntu
  IdentityFile ~/.ssh/my-key.pem
  ServerAliveInterval 60
EOF
ssh prod-web

# Bastion host pattern (private instance via public jump box)
ssh -i key.pem -J bastion.example.com ubuntu@10.0.5.20
# Or ProxyJump in your config
Host prod-db
  HostName 10.0.5.20
  User ubuntu
  IdentityFile ~/.ssh/my-key.pem
  ProxyJump bastion.example.com

# ===== MODERN: SSM Session Manager (NO PORT 22, NO KEYS) =====
# Pre-requisites: SSM agent on instance, IAM role with AmazonSSMManagedInstanceCore

aws ssm start-session --target i-0abc123def456

# Port forwarding via SSM (RDS access through private instance)
aws ssm start-session \
  --target i-0abc123def456 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["mydb.cluster-xyz.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["15432"]}'

# SSH over SSM (best of both)
# ~/.ssh/config:
Host i-* mi-*
  ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p"

ssh ec2-user@i-0abc123def456

Tip: Production gold-standard — never open port 22 in your security groups. Use SSM Session Manager — no public IP, no SSH key management, full audit log in CloudTrail, IAM-based access control. This is the modern DevOps practice.

cloud-init — First Boot Bootstrap

cloud-init is Linux’s standard first-boot configuration tool. It runs on AWS, GCP, Azure, OpenStack — everywhere. The user-data field you provide at launch time is parsed and executed by cloud-init.

# Status check
cloud-init status
# status: done

cloud-init status --long
cloud-init analyze show              # boot time breakdown

# Config files
ls /etc/cloud/
# cloud.cfg               main config
# cloud.cfg.d/            drop-ins (vendor + user)
# templates/              hostname etc.

# Logs (THE MOST IMPORTANT thing for debugging)
sudo tail -f /var/log/cloud-init.log              # detailed
sudo tail -f /var/log/cloud-init-output.log       # stdout/stderr of user-data

# Re-run cloud-init (for testing)
sudo cloud-init clean --logs
sudo cloud-init init
sudo cloud-init modules --mode=config
sudo cloud-init modules --mode=final

Format 1: Bash Script User-Data

#!/bin/bash
# user-data.sh — simple bash, runs as root on first boot
set -euxo pipefail
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1

echo "=== Starting bootstrap at $(date) ==="

# Idempotency guard
if [ -f /var/lib/bootstrap-done ]; then
  echo "Already bootstrapped, exiting"
  exit 0
fi

# Update + install
apt-get update -y
apt-get upgrade -y
apt-get install -y nginx awscli docker.io curl jq

# Enable services
systemctl enable --now nginx docker
usermod -aG docker ubuntu

# Pull config from S3 (IAM role attached)
aws s3 cp s3://my-config-bucket/nginx.conf /etc/nginx/nginx.conf
systemctl reload nginx

# Mark done
touch /var/lib/bootstrap-done
echo "=== Bootstrap complete at $(date) ==="
#cloud-config
# Declarative — much cleaner than bash

hostname: web-prod-01
fqdn: web-prod-01.internal.example.com
manage_etc_hosts: true

# Users
users:
  - name: deploy
    groups: [docker, sudo]
    shell: /bin/bash
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1... deploy@laptop

# Package install
package_update: true
package_upgrade: true
packages:
  - nginx
  - docker.io
  - awscli
  - jq
  - htop
  - curl

# Drop config files
write_files:
  - path: /etc/nginx/sites-available/default
    permissions: '0644'
    content: |
      server {
        listen 80 default_server;
        root /var/www/html;
        index index.html;
        location /health { return 200 'ok'; add_header Content-Type text/plain; }
      }
  - path: /etc/sysctl.d/99-tuning.conf
    content: |
      vm.swappiness = 10
      net.core.somaxconn = 65535
      fs.file-max = 2097152

# Run commands at end (after packages, files)
runcmd:
  - sysctl --system
  - systemctl enable --now docker nginx
  - usermod -aG docker ubuntu
  - docker pull nginx:alpine
  - [ sh, -c, 'echo "Bootstrap done at $(date)" >> /var/log/bootstrap.log' ]

# Reboot if kernel updated (optional)
power_state:
  mode: reboot
  condition: test -f /var/run/reboot-required

AMI Creation — Golden Images

In production, instead of doing a full user-data setup on every deploy, you use pre-baked AMIs — boot in 30 seconds instead of 5 minutes. But before creating an AMI, a “sysprep” (clean) is essential.

# Pre-AMI cleanup script (run before creating image)
#!/bin/bash
set -e

# Stop services that hold state
sudo systemctl stop docker nginx

# Remove SSH host keys (they'll regenerate on the instance's first boot)
sudo rm -f /etc/ssh/ssh_host_*

# Clear machine-id (cloud-init will regenerate)
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id

# Clear cloud-init state
sudo cloud-init clean --logs --seed

# Clear logs
sudo find /var/log -type f -exec truncate -s 0 {} \;
sudo rm -rf /var/log/journal/*

# Clear bash history
cat /dev/null > ~/.bash_history
history -c
sudo rm -f /root/.bash_history

# Clear authorized_keys for default user (cloud-init re-injects)
sudo rm -f /home/ubuntu/.ssh/authorized_keys

# Trim filesystem (smaller snapshot)
sudo fstrim -av

# Now from your laptop/CI:
aws ec2 create-image \
  --instance-id i-0abc123 \
  --name "web-base-$(date +%Y%m%d-%H%M)" \
  --description "Nginx + Docker baseline" \
  --no-reboot=false

# Packer (recommended for repeatable builds) — basic config
# packer build webserver.pkr.hcl
# Packer launches temp instance, runs provisioners, creates AMI, terminates.

Provider Differences

AWS EC2GCP Compute EngineAzure VM
Metadata endpoint169.254.169.254metadata.google.internal / 169.254.169.254169.254.169.254 (with header)
Default usersec2-user, ubuntu, rocky, adminusername from SSH key (gcloud creates one)azureuser (you pick at create)
AgentSSM Agent + ec2-instance-connectGoogle guest agent / OS LoginWALinuxAgent (waagent)
Agent config/etc/amazon/ssm//etc/default/instance_configs.cfg/etc/waagent.conf
Block storageEBSPersistent Disk (PD)Managed Disks
Bootstrapuser-data + cloud-initstartup-script (metadata) + cloud-initcustomData / cloud-init
Keyless SSHSSM Session ManagerOS Login + IAP tunnelBastion / Just-In-Time
# GCP metadata example
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/name" \
  -H "Metadata-Flavor: Google"

# Azure metadata example (note required header)
curl -s -H "Metadata:true" \
  "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq

Real-World DevOps Patterns

1. Immutable Infrastructure

Don’t patch the server — launch a new instance from a new AMI, shift traffic, terminate the old one. Configuration drift = zero. Rollback = re-launch the previous AMI. This is the modern way.

2. cloud-init + Configuration Management Combo

#cloud-config
# cloud-init only "bootstraps Ansible/Chef" — that tool does the rest
package_update: true
packages: [ansible, git]
runcmd:
  - git clone https://github.com/myorg/ansible-playbooks.git /opt/ansible
  - ansible-pull -U https://github.com/myorg/ansible-playbooks.git -i localhost, site.yml

3. Centralized Logging

Local logs on an instance (/var/log/*) are ephemeral — when the instance is terminated, the logs are gone. In production, always ship your logs:

# CloudWatch agent
sudo amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:cw-config.json -s

# Datadog agent
DD_API_KEY=xxx DD_SITE="datadoghq.com" \
  bash -c "$(curl -L https://install.datadoghq.com/scripts/install_script_agent7.sh)"

# Promtail (Grafana Loki)
# /etc/promtail/config.yml → ships journald + /var/log/* to Loki

# rsyslog → centralized syslog server (old-school but reliable)
# /etc/rsyslog.d/50-remote.conf:
# *.* @@logserver.internal:514

4. EBS Snapshots — Automated Backup

# AWS Backup or Data Lifecycle Manager (DLM) — preferred
# Snapshot daily, retain 7 days, cross-region copy weekly

# Manual snapshot (one-off)
aws ec2 create-snapshot \
  --volume-id vol-0abc123 \
  --description "pre-migration backup $(date +%F)" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Backup,Value=manual}]'

# Restore: snapshot → new volume → attach to instance
aws ec2 create-volume \
  --snapshot-id snap-0xyz \
  --availability-zone us-east-1a \
  --volume-type gp3

5. SSH via SSM — No Port 22 Open

# Security group inbound rules: (none — yes, ZERO)
# Instances are in private subnet
# Access only via:
aws ssm start-session --target i-0abc123

# And if you need the SSH protocol (scp, rsync, git):
ssh ec2-user@i-0abc123    # ProxyCommand via SSM (config shown earlier)
scp -r ./code/ ec2-user@i-0abc123:/home/ec2-user/

6. Putting It All Together — A Realistic Stack

# Terraform spins up:
#   - VPC with private subnets
#   - Auto Scaling Group with Launch Template
#   - Launch Template references golden AMI (built by Packer)
#   - user-data does minimal "ansible-pull" for last-mile config
#   - IAM role attached with SSM + CloudWatch + S3 read
#   - Application Load Balancer in public subnet
#   - Security groups: ALB → instances on app port only, port 22 NEVER open
#   - EBS volumes with daily DLM snapshots
#   - CloudWatch agent ships /var/log + custom metrics
#   - Logs to CloudWatch Logs → subscription filter → Datadog/Splunk

# Deploys happen by:
#   1. Packer builds new AMI on git push
#   2. Terraform updates Launch Template's AMI ID
#   3. ASG instance refresh rolls instances one-by-one
#   4. Old AMI kept for 7 days (instant rollback)

# Day-2 ops:
#   - SSH via `aws ssm start-session`
#   - Logs in CloudWatch / Datadog
#   - Metrics in CloudWatch / Grafana
#   - Disk grow: modify-volume + growpart + resize2fs (zero downtime)
#   - Bigger instance: ASG rolling update with new instance type

Note: Wrap-up — Five things you’ll use daily with cloud Linux: (1) fetching instance data via IMDSv2, (2) EBS volume attach + format + mount + fstab nofail, (3) bootstrapping via cloud-init user-data, (4) keyless SSH access via SSM Session Manager, (5) the golden AMI + immutable infra pattern. You’ll repeat these so often they become muscle memory.

Tip: The interview question that lands you a senior-level (USD six-figure) offer — “Describe what happens from aws ec2 run-instances until an application is serving traffic.” Answer: an EBS root volume is created from the AMI snapshot, an ENI is attached, the instance boots (UEFI → GRUB → kernel → initramfs → systemd) → cloud-init pulls user-data from IMDS → packages install, config files are written, services start → the application binds to its port → the health check passes → the ALB target group marks it healthy → traffic routes. If you can explain that whole chain in 60 seconds, the job is yours.

Further Learning

Section 38 of 39 · ~5 min

You’ve built the foundation. Now you need to keep the muscle warm. This section is a curated map of where to go next — channels, platforms, books, and certifications that actually move the needle in the global DevOps market. Spend 30 minutes a day here for the next three months and you will be unrecognizable.

Tip: Study advice #1 — Pick ONE channel and finish it. Don’t channel-hop. Bookmark 50 videos and watch zero. Pick Abhishek Veeramalla’s Linux + DevOps playlist OR NetworkChuck’s Linux for hackers, and finish it end-to-end before you touch anything else.

YouTube channels worth your subscription

Concept-first creators (great for building intuition):

  • Abhishek Veeramalla — a gold standard for DevOps aspirants. His “DevOps Zero to Hero” playlist is one of the most recommended free resources in DevOps communities. Real interview questions, real scenarios.
  • Telusko (Navin Reddy) — clean Linux fundamentals, scripting basics, Docker primers. Great when you want a relaxed explanation.
  • Cyber Platter — strong on shell scripting, system administration, and Red Hat workflows. Useful for RHCSA prep.

Production-polished creators (depth + polish):

  • NetworkChuck — high-energy, hands-on. His “Linux for Hackers” series is the most fun way to fall in love with the terminal.
  • Learn Linux TV (Jay LaCroix) — calm, thorough, no-fluff. The Ubuntu Server and Bash scripting series are excellent.
  • TechWorld with Nana — best DevOps overview channel on YouTube. Her Docker, Kubernetes, and CI/CD crash courses are essentially free certification prep.
  • DistroTube — pure Linux culture, window managers, dotfiles, tooling. Watch when you want to fall deeper down the rabbit hole.

Free practice platforms (this is where skill actually grows)

  • OverTheWire — Bandit (overthewire.org/wargames/bandit) — 34 levels of SSH-driven Linux puzzles. If you can clear Bandit 0–20, you are operationally literate. This is non-negotiable.
  • SadServers (sadservers.com) — “broken Linux server” scenarios. You SSH in, diagnose, and fix. This is the closest thing to a real on-call shift you can practice for free.
  • Linux Journey (linuxjourney.com) — beautifully structured beginner-to-intermediate text path. Perfect for revision.
  • KillerCoda (killercoda.com) — browser-based Linux, Docker, Kubernetes labs. Free tier is generous.
  • HackTheBox — privilege escalation and CTF-style Linux. Sharpens your understanding of permissions, processes, and networking.
  • TryHackMe — gentler than HTB. Their “Linux Fundamentals” and “Linux PrivEsc” rooms are interview-grade prep.
  • LeetCode Shell — small but mighty. Bash one-liners that show up in screening rounds (word frequency, transpose file, valid phone numbers).

Caution: Study advice #2 — Tutorial hell is real. For every 1 hour of video you watch, spend 2 hours in a terminal breaking things. Recruiters do not care that you watched 200 hours of content. They care that you can fix a full disk in 90 seconds on a shared screen.

Documentation — the most underrated tier

  • man — the original. man bash, man 5 crontab, man 7 signal. Learn to navigate with / and n.
  • tldr — community-maintained “just the examples” pages. Install with npm i -g tldr or apt install tldr. tldr tar beats reading the man page when you just need the syntax.
  • info — GNU’s hyperlinked docs. Heavier than man, but the coreutils info pages are gold.
  • DigitalOcean tutorials (digitalocean.com/community/tutorials) — the cleanest English Linux/server tutorials on the internet. Tagged by Ubuntu version. Bookmark this.
  • ArchWiki (wiki.archlinux.org) — even if you never touch Arch, the ArchWiki is the most accurate, distro-agnostic Linux reference that exists.
  • tldp.org — The Linux Documentation Project. Older but the “Advanced Bash-Scripting Guide” is a classic.

Books that earn shelf space

  • “How Linux Works” by Brian Ward (No Starch Press) — the single best book to understand why Linux behaves the way it does. Bootloaders, init, devices, networking. Read it twice.
  • “The Linux Command Line” by William Shotts — legally free PDF at linuxcommand.org/tlcl.php. The reference for shell mastery.
  • “Linux Bible” by Christopher Negus — fat, slightly dry, but covers RHEL/Fedora workflows thoroughly. Perfect companion for RHCSA.
  • “Site Reliability Engineering” (Google SRE Book) — free at sre.google/books. Not Linux-specific but it teaches you how to think like the engineers earning top-of-market (USD six-figure) salaries.
  • Bonus: “UNIX and Linux System Administration Handbook” by Nemeth et al. — the “purple book.” If you go senior sysadmin, this is the bible.

Certifications — what actually matters in the DevOps market

Ranked by ROI for someone at your stage (an experienced developer pivoting to DevOps, targeting six-figure USD roles):

  1. RHCSA (Red Hat Certified System Administrator) — EX200. The most respected Linux cert in the industry. Hands-on exam, no MCQs. Recruiters at enterprises, Red Hat partners, and most product companies recognize it instantly. It isn’t cheap, but it’s worth it if your goal is Linux-heavy DevOps roles.
  2. LFCS (Linux Foundation Certified Sysadmin). Vendor-neutral, performance-based, cheaper (~$300, often 50% off). Less brand recognition than RHCSA but globally respected.
  3. CompTIA Linux+. Decent foundational cert. MCQ-based. Good for resumes scanned by ATS bots, weaker signal to senior engineers.
  4. LPIC-1. Vendor-neutral, two exams. Solid content but lower brand pull than RHCSA.
  5. Honest take: AWS Solutions Architect Associate (SAA-C03) + provable Linux skills (Bandit + a GitHub of scripts) beats any pure-Linux cert for six-figure (USD) DevOps roles. Hiring managers want cloud + Linux, not Linux alone. If you can only afford one cert this year, do SAA-C03 first, then RHCSA.

Online sandboxes (practice without burning your laptop)

  • Killercoda — instant Ubuntu/CentOS/K8s playgrounds in the browser. No signup needed for basic labs.
  • JSLinux (bellard.org/jslinux) — a full Linux kernel running in your browser via JavaScript. Useful for quick “what does this command do” checks on a locked-down machine.
  • DigitalOcean — $200 free credit for new accounts, droplets from ~$6/month after. Cleanest UX for spinning up real VMs.
  • Linode (Akamai) — similar to DO, $100 credit, slightly cheaper at the low end.
  • AWS EC2 free tier — t2.micro / t3.micro free for 12 months. This should be your daily-driver sandbox because it also teaches you AWS.
  • GCP free tier — e2-micro is free forever in select regions. Great for a persistent personal Linux server.

Note: Study advice #3 — Build in public. Push every script, every dotfile, every broken-then-fixed config to a GitHub repo called linux-journey-2026. Tweet/LinkedIn one thing you learned each week. In six months that repo + timeline is worth more than any certificate, because it’s proof of consistent practice — which is exactly what DevOps hiring managers screen for.

Next Steps After Linux

Section 39 of 39 · ~7 min

Linux is the floor, not the ceiling. Every DevOps tool you’ll touch — Docker, Kubernetes, Ansible, Terraform, AWS, GCP, CI/CD runners — is just Linux wearing a costume. Here’s how each next step builds directly on what you already know.

Linux → Bash (automate everything you just learned)

You already know cp, grep, find, awk, sed. Bash scripting is just gluing those into reusable files. The leap is small but career-changing: a sysadmin who writes scripts becomes a DevOps engineer.

  • Goal: be able to write a 50-line script with functions, argument parsing, error handling, and logging without Googling syntax.
  • Resource: “Bash Scripting Cheatsheet” (devhints.io/bash) + ShellCheck (shellcheck.net) for every script you write.
  • Project: write a backup script that tars a directory, uploads to S3, rotates old backups, and emails on failure. That one script demonstrates 80% of what mid-level DevOps does daily.

Linux → Docker (namespaces + cgroups + overlayfs, nothing magical)

Docker is not a separate technology. It’s three Linux kernel features in a trench coat: namespaces (process/network/mount isolation), cgroups (CPU/memory limits), and overlayfs (layered filesystems). You already learned about processes, mounts, and users — Docker is just those concepts with sharper edges.

7-day Docker plan:

  1. Day 1: Install Docker. Run docker run -it ubuntu bash. Explore. Notice it feels exactly like a Linux box because it is one.
  2. Day 2: Images vs containers. docker pull, docker ps -a, docker exec, docker logs. Build mental model.
  3. Day 3: Write your first Dockerfile. Containerize a simple Node/React app (use your existing skills).
  4. Day 4: Volumes and bind mounts. Networks (bridge, host, none). Port publishing.
  5. Day 5: docker-compose. Spin up a 3-service stack (React + Node API + Postgres).
  6. Day 6: Image optimization — multi-stage builds, alpine base images, .dockerignore. Cut a 1.2GB image to 80MB.
  7. Day 7: Push to Docker Hub. Pull on a cloud VM. Run in production-like mode with restart policies and healthchecks.

Linux → Kubernetes (nodes are Linux, pods are processes)

A Kubernetes cluster is a fleet of Linux machines (nodes) running containers (pods, which are groups of Linux processes) coordinated by an API server. Everything you debug in K8s ultimately resolves to kubectl exec -it pod -- sh followed by the same Linux commands you already know.

Path:

  1. Install minikube or kind locally. One-node cluster on your laptop.
  2. Learn pods → deployments → services → ingress in that order. Don’t skip ahead.
  3. Do the free Kubernetes Basics tutorial on kubernetes.io.
  4. KillerCoda K8s scenarios — 30 minutes/day for 3 weeks.
  5. Aim for CKAD (Certified Kubernetes Application Developer). It’s hands-on, performance-based, and can add a meaningful bump to your offer. CKA comes later when you’re ops-focused.

Linux → AWS (EC2 is Linux, Lambda is Linux, Fargate is Linux)

You will be shocked how much of “learning AWS” is actually “applying Linux on rented hardware.” An EC2 instance is a Linux VM. A Lambda function runs on Amazon Linux 2. ECS Fargate runs containers on Linux. RDS is Postgres/MySQL on Linux. The cloud is just someone else’s Linux box with an API in front.

Path:

  1. AWS Solutions Architect Associate (SAA-C03) — your gateway cert. ~$150, three months of evening study. Stephane Maarek’s Udemy course is the standard.
  2. Hands-on: build a 3-tier app (VPC + EC2 + RDS + S3 + CloudFront) and tear it down with Terraform.
  3. AWS DevOps Engineer Professional — once you have 1+ year of real AWS experience. This is the cert that anchors senior-level (USD six-figure) conversations.

30-day Linux-to-DevOps ramp (one focused task per day)

Ready to apply what you’ve learned? Work through the Hands-on DevOps Projects to build real systems with Linux, Docker, and CI/CD.

  1. Install Ubuntu 24.04 in a VM. Customize .bashrc, install zsh + oh-my-zsh.
  2. Complete OverTheWire Bandit levels 0–10.
  3. Complete Bandit levels 11–20.
  4. Write a shell script that audits your home directory: file count, largest files, oldest files.
  5. Master grep, awk, sed with the GNU “Mastering Text Processing” cheatsheet.
  6. Set up SSH key auth between your laptop and a free-tier EC2 instance.
  7. Write a systemd service unit for a Node.js app. Enable, start, check status, view logs with journalctl.
  8. Configure UFW or iptables on your VM. Allow only SSH + HTTP/HTTPS.
  9. Install nginx, serve a static site, configure a reverse proxy to a Node app on :3000.
  10. Add Let’s Encrypt (certbot) for HTTPS on a free domain (DuckDNS or Freenom).
  11. Write a cron job that backs up /etc nightly to a tarball.
  12. Learn rsync. Sync the backups to an S3 bucket using the AWS CLI.
  13. Read /proc/cpuinfo, /proc/meminfo. Write a script that summarizes system health.
  14. Use strace on a misbehaving process. Identify which syscall is failing.
  15. Install Docker. Run hello-world, then Ubuntu, then nginx.
  16. Containerize your favorite React side project. Write the Dockerfile from scratch.
  17. Multi-stage build: cut your image size by 80%.
  18. Write a docker-compose.yml for React + Express + Postgres + Redis.
  19. Push your image to Docker Hub. Pull and run it on the EC2 instance.
  20. Install minikube locally. Deploy your app as a Kubernetes Deployment + Service.
  21. Add a Kubernetes ConfigMap and Secret. Wire them to your pod.
  22. Add Horizontal Pod Autoscaler. Generate load with hey or ab and watch it scale.
  23. Write a Bash script that deploys your app: build image → push → kubectl apply → check rollout.
  24. Set up GitHub Actions: on push to main, build Docker image and push to Docker Hub.
  25. Extend GitHub Actions: SSH to EC2 and pull the new image (a tiny CD pipeline).
  26. Sign up for AWS free tier. Launch an EC2 via the console, then via AWS CLI.
  27. Create an S3 bucket. Upload, download, set lifecycle policy to delete after 30 days.
  28. Write a Terraform file that creates a VPC + 1 EC2 + 1 S3 bucket. Apply, destroy, repeat.
  29. Install Prometheus + Grafana on minikube. Scrape node metrics. Build one dashboard.
  30. Final boss: deploy your React + Express + Postgres app to EC2, behind nginx with HTTPS, with a GitHub Actions pipeline that auto-deploys on push, and a cron-based DB backup to S3. Take a screenshot. Post it on LinkedIn with the GitHub repo. You are now a junior DevOps engineer.

30-60-90 day vision (and the 6-month payoff)

  • Day 30 — Linux comfort. You can SSH anywhere, debug a stuck process, write a 100-line bash script, and edit any system config without panic. You speak fluent terminal.
  • Day 60 — Docker + AWS basics. You’ve containerized 3+ apps, you understand VPC/EC2/S3/IAM, and you’ve deployed something real to production with HTTPS. You can answer “explain Docker to a junior” in an interview.
  • Day 90 — K8s + CI/CD interview-ready. You’ve passed (or can pass) CKAD-style scenarios, you have a working GitHub Actions pipeline in a public repo, and you’ve cleared 3-5 mock interviews. Target: junior/mid DevOps roles at strong product companies. With your background, recruiters will fast-track you because you bring product sense most freshers lack.
  • Month 6 — Mid-level inflection. One real production system on your resume, SAA-C03 passed, CKAD passed, contributing to an open-source DevOps tool. Target: mid-level DevOps / SRE / Platform Engineer roles. The frontend-to-DevOps story becomes your superpower — you understand what developers actually need from a platform, which most pure-ops engineers don’t.

Note: Prefer a day-by-day path? This is covered in Mission 90 Days 1–20 — a free 90-day guided DevOps program with browser terminal missions.

Real world: The pep talk you came here for. You are not late. The global DevOps market in 2026 is desperate for engineers who can talk to developers and ops — because most candidates can only do one. Your years of shipping real features is not a liability; it is a wedge. Recruiters will pay a premium for someone who has actually pushed code to production and now understands the pipeline that delivers it.

Six months from now, a hiring manager is going to look at your GitHub, see 180 days of consistent commits, see one solid end-to-end project, see SAA-C03 and maybe CKAD on the resume, and make you a strong offer without blinking. The only thing standing between you and that offer is showing up to the terminal every day between now and then.

Close this tab. Open one. Type ssh. Begin.

Frequently asked questions

Do you need to learn Linux for DevOps?

Yes — the overwhelming majority of servers, containers, and CI runners are Linux. Comfort with the shell, the filesystem, permissions, processes, and systemd is foundational for any DevOps role.

What Linux skills do DevOps engineers need most?

Navigating the filesystem, file permissions and ownership, text processing (grep/awk/sed), managing services with systemd, package management, networking and SSH, and bash scripting for automation.

How do Linux file permissions work?

Each file has read/write/execute bits for owner, group, and others. They can be set symbolically (chmod u+x) or with octal digits (chmod 755), where 4=read, 2=write, 1=execute summed per class.

How do I use grep, awk, and sed for log analysis?

grep filters lines by pattern, awk extracts and computes over columns, and sed edits streams. Chaining them through pipes is the core log-wrangling workflow on Linux.

How do I manage services with systemd?

Use systemctl to start, stop, enable, and check the status of units, and journalctl to read their logs. Unit files in /etc/systemd/system define how services run.

How do I SSH into a remote Linux server securely?

Use key-based authentication instead of passwords, disable root login, change defaults in sshd_config, and use an SSH agent. ssh user@host connects; scp/rsync transfer files.

Keep going

Continue your Linux path

Pick the next topic on the roadmap and track what you’ve covered — your progress is saved in this browser.