Learn linux, with practice after every lesson
8 lessons, about 125 minutes of reading, and 24 multiple-choice questions. Each lesson names the mistake that most often costs people the interview, because that is where the hours actually go. Part of the free coding course.
Moving around the filesystem
BasicsAlmost every server you will ever touch runs Linux, and it has no desktop. The command line is not a nostalgic preference β it is the only interface available once you SSH into a machine, which is why it appears in DevOps and backend postings so consistently and why the fear of it is worth getting over early.
Everything is a file, and every file hangs off a single root directory written as /. There are no drive letters. Paths starting with / are absolute; anything else is relative to where you currently are, and most beginner confusion in the first week is that one distinction. pwd tells you where you are and is worth typing whenever a command behaves unexpectedly.
The commands that carry most real work are small in number. ls to look, cd to move, cat and less to read, and tail -f to follow a log as it is written β that last one is probably the single most-used command during an incident, because it shows you what a misbehaving service is doing right now rather than what it did earlier.
Syntax
pwd # where am I
ls -lah # list, long form, human sizes, including hidden
cd /var/log # go somewhere absolute
cd .. # up one level
cd ~ # home
cat app.log # print a whole file
less app.log # page through it (q to quit)
tail -f app.log # follow a log as it is written β the one you will use daily
head -20 data.csv # first 20 lines
find . -name "*.csv" # find files by name, recursively
du -sh * # what is taking up space here
Key points
- tail -f is how you watch a service misbehave in real time. It is probably the single most-used command in an incident.
- Paths starting with / are absolute; anything else is relative to where you are. Most beginner confusion is this one distinction.
- Hidden files start with a dot and need ls -a. Configuration usually lives in them.
Practice challenge
A service is failing right now and app.log is 2 GB. Give the command to watch new lines as they are written, and the command to see just the last 50 lines β without loading the whole file.
tail -f app.log
tail -50 app.log
Watch live: $ ______
Last 50: $ ______
Show a hint
- cat would print all 2 GB to your terminal
- The live one is the command you use most during an incident
Check yourself
1. Which command follows a log as new lines are written?
Show answer
C. tail -f file
2. What does a leading / in a path mean?
Show answer
B. Absolute path from the root
3. Why use less instead of cat on a big file?
Show answer
B. It pages instead of flooding the terminal
Pipes, grep and doing real work
Working levelThe idea that makes Linux powerful is that small programs pass text to each other. One command's output becomes the next one's input, joined by a pipe. Nobody memorises a thousand tools; they learn six and compose them, which is why a one-line pipeline can answer a question about a two-gigabyte log while someone else is still waiting for an editor to open it.
This composability is what people mean when they say someone is comfortable on the command line. Not obscure flags β the ability to break a question into stages and chain them. Build a pipeline one stage at a time, checking the output as you go, rather than writing six stages and then debugging the whole thing at once.
The idiom worth memorising is sort | uniq -c | sort -rn, which counts occurrences and ranks them. The subtlety that produces wrong answers is that uniq only collapses ADJACENT duplicates, so the first sort is required rather than optional. Skip it and the command still succeeds and still prints numbers β they are simply wrong, which is what makes it dangerous rather than merely broken.
Syntax
# how many 500 errors today, by endpoint?
grep " 500 " access.log \
| awk '{print $7}' \
| sort \
| uniq -c \
| sort -rn \
| head
# 142 /api/search
# 38 /api/apply
# 9 /api/profile
grep -i "error" app.log # case-insensitive
grep -v "healthcheck" app.log # everything EXCEPT matches
grep -c "timeout" app.log # just the count
wc -l data.csv # how many lines
Key points
- sort | uniq -c is the counting idiom. uniq only collapses ADJACENT duplicates, so the sort is required, not optional.
- grep -v inverts the match and is how you strip noise like healthchecks before counting anything.
- Build a pipeline one stage at a time, checking output as you go. Writing six stages then debugging the whole thing is far slower.
Practice challenge
From access.log, count how many 500 errors each endpoint produced today and show the worst offenders first. The endpoint is field 7. Write the pipeline.
grep " 500 " access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head
$ grep " 500 " access.log | ______ | ______ | ______ | ______
Show a hint
- uniq only collapses ADJACENT duplicates, so something must come before it
- The final sort needs -rn to rank numerically, highest first
Check yourself
1. Why must sort come before uniq -c?
Show answer
B. uniq only collapses adjacent duplicates
2. What does grep -v "healthcheck" do?
Show answer
C. Shows everything except matches
3. What does a pipe do?
Show answer
B. Sends one command's output into the next
Permissions, processes and a stuck server
AdvancedTwo things account for most 'it works on my machine' incidents: the file cannot be read by the user the service runs as, or something is already holding the port. Both are diagnosable in about a minute once you know where to look, and being the person who checks those two things first is quietly valuable on any team.
Permissions are three groups β owner, group, everyone β each with read, write and execute. The numeric form is just those bits added up: 4 read, 2 write, 1 execute. So 644 is owner reads and writes, everyone else reads; 600 is owner only, which is what a file containing secrets should be. A script that returns 'Permission denied' is usually missing its execute bit rather than broken.
For the second class of problem, ss -tulpn shows what is listening on which port, ps aux shows what is running, and df -h shows disk space. That last one deserves a habit: a full disk causes failures that look like anything but a full disk β databases refusing writes, services failing to start, logs stopping mid-sentence β and checking it takes two seconds.
Syntax
ls -l deploy.sh
# -rw-r--r-- 1 priya staff 482 Aug 17 09:14 deploy.sh
# ^ no execute bit β running it gives 'Permission denied'
chmod +x deploy.sh # make it executable
chmod 600 secrets.env # owner only β nobody else reads it
chown app:app /var/app # hand ownership to the service user
ps aux | grep node # what is running
top # live CPU and memory (q to quit)
kill 4821 # ask a process to stop
kill -9 4821 # force it, only if asking failed
ss -tulpn | grep 3000 # WHAT is holding port 3000
df -h # disk space β a full disk breaks things strangely
Key points
- Permission denied on a script almost always means a missing execute bit, not a broken script.
- kill asks politely and lets the process shut down cleanly. kill -9 does not, so unsaved state is lost β reach for it second.
- When something behaves inexplicably, check df -h. A full disk causes failures that look like anything but a full disk.
Practice challenge
deploy.sh returns 'Permission denied', and after fixing that the service will not start because port 3000 is in use. Give the command to fix the script, the command to find what holds the port, and say why chmod 777 is the wrong fix.
chmod +x deploy.sh
ss -tulpn | grep 3000
777 makes the file writable by every user on the machine, is flagged in any review, and hides which user actually needed access
Fix script: $ ______
Find port: $ ______
Why not 777: ______
Show a hint
- 'Permission denied' on a script is usually one missing bit, not a broken script
- The port command lists listening sockets with the owning process
Check yourself
1. What does chmod 600 secrets.env allow?
Show answer
B. Owner reads and writes, nobody else
2. Which command shows what is holding port 3000?
Show answer
C. ss -tulpn
3. Why is chmod 777 the wrong fix?
Show answer
B. It grants write access to every user and hides the real problem
Debug a live incident from the shell
Job-readySomeone says "the site is down" and you have a terminal. The instinct is to start restarting things. The discipline is to spend ninety seconds finding out what is actually true first, because a restart destroys the evidence and often fixes the symptom for twenty minutes.
Work outside in. Is the service running at all, is it listening on the port you think, is anything answering locally, and only then is it a network or DNS problem. Each of those is one command, and answering them in order stops you debugging a firewall when the process died. The four resources that cause most incidents are disk, memory, file descriptors and CPU β check them early, because a full disk presents as a hundred unrelated errors.
Write down what you find as you go, even in a scratch file. Incidents end with someone asking what happened, and reconstructing it afterwards from memory is unreliable. The other reason is that stating a finding forces you to be specific: "disk is at 100% on /var" is a fact you can act on, "something's wrong with the server" is not.
Syntax
# 1. Is it even running, and is it listening?
systemctl status api.service
ss -tulpn | grep :3000
# 2. Does it answer locally? (separates app from network)
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:3000/health
# 3. The four usual suspects, in ten seconds
df -h # disk β a full /var breaks everything, confusingly
free -m # memory β check swap too
uptime # load average vs core count
ls /proc/$(pgrep -f api | head -1)/fd | wc -l # file descriptors
# 4. What did it say before it died?
journalctl -u api.service --since '15 min ago' --no-pager | tail -50
# 5. Only now: act. And write down what you found.
Key points
- Check before you restart. A restart clears the state that would have told you the cause, and buys twenty minutes at the price of the next incident.
- Work outside in: process, port, local response, then network. It stops you debugging DNS when the service is not running.
- Check disk first among the resources. A full filesystem produces errors that look like anything except a full filesystem.
Practice challenge
You are told the API is down and you have a shell. Write the first five commands in order and say what each one rules out. Then say why restarting the service first would be the wrong move.
1. systemctl status api - is the process even running
2. ss -tulpn | grep :3000 - is anything listening on the port
3. curl localhost:3000/health - does it answer locally (separates app from network)
4. df -h - a full filesystem produces many unrelated-looking errors
5. journalctl -u api --since '15 min ago' - what did it say before it died
Why not restart: it destroys the evidence, and usually 'works' - so the real cause survives and pages someone again next fortnight
1. ______ rules out: ______
2. ______ rules out: ______
3. ______ rules out: ______
4. ______ rules out: ______
5. ______ rules out: ______
Why not restart first: ______
Show a hint
- Work outside in: process, port, local response, then network
- One of these five is checked early because its failure mode is deceptive rather than because it is likely
Check yourself
1. Why not restart the service first?
Show answer
B. It destroys the evidence and hides a recurring cause
2. Which command tells you whether anything is listening on a port?
Show answer
B. ss -tulpn
3. Why check disk usage early in an incident?
Show answer
B. A full filesystem produces many unrelated-looking errors
Paths, globs and the commands that bite
BasicsAn absolute path starts at the root and works anywhere: /var/log/app.log means the same thing from any directory. A relative path is read from where you currently are, so app.log, ./app.log and ../config/app.log all depend on your working directory, which is why a script that works when you run it by hand fails from cron. When a path matters, make it absolute, and use pwd to answer 'where am I' before blaming the command.
The shell expands wildcards before the command ever sees them, and that one fact explains most surprises. When you type rm *.log the shell turns it into rm a.log b.log c.log and hands that list over; rm never saw a star. So a filename with a space becomes two arguments, an empty match may be passed through literally, and a file beginning with a dash gets read as an option. Quoting fixes the first, and -- ends option parsing for the last.
Then there is the command with no undo. rm has no recycle bin β rm -rf on the wrong path is simply gone, and rm -rf / with an accidental space in the middle has ended real companies. Two habits cost nothing: replace rm with ls first to see exactly what the glob matched, and never build a path by string-joining a variable that might be empty, because rm -rf "$DIR"/ with DIR unset expands to rm -rf /.
Syntax
pwd # where am I, before anything else
cd /var/log # absolute: works from anywhere
cd ../tmp # relative: depends on where you are
# see what the glob matches BEFORE you destroy it
ls *.log # <- always do this first
rm *.log
# filenames with spaces: quote, always
rm "quarterly report.csv" # right
rm quarterly report.csv # WRONG: removes two files
# a file literally named -f
rm -- -f
# the empty-variable trap
DIR=""
rm -rf "$DIR"/ # expands to rm -rf / -- catastrophic
rm -rf "${DIR:?DIR is unset}"/ # refuses instead
du -sh /var/log/* # what is actually big
ls -lh # sizes a human can read
Key points
- The shell expands globs before the command runs, so quoting and spaces matter more than they look. ls with the same glob shows you exactly what will be affected.
- Absolute paths for anything that runs unattended. Relative paths depend on the working directory, which is why cron jobs fail the way they do.
- rm has no undo. "${VAR:?message}" makes the shell refuse when the variable is empty rather than expanding to a path you did not mean.
Practice challenge
A deploy script contains rm -rf $BUILD_DIR/ and BUILD_DIR is set from an environment variable that is sometimes unset. Explain what happens when it is unset, write the safe version, and give the command you would run first to see what would be removed.
When unset it becomes: rm -rf / - it deletes the filesystem
Safe version: rm -rf "${BUILD_DIR:?BUILD_DIR is unset}"/
Check first with: ls -d "${BUILD_DIR:?}"/ (substitute ls for rm and read the list)
When BUILD_DIR is unset the command becomes: ______
Safe version: ______
Check first with: ______
Show a hint
- The shell substitutes an empty string, it does not refuse
- ${VAR:?message} makes the shell refuse instead of expanding to nothing
Check yourself
1. Who expands *.log into a list of filenames?
Show answer
B. The shell, before the command runs
2. Why do absolute paths matter in a cron job?
Show answer
B. Cron's working directory is not yours, so relative paths resolve elsewhere
3. DIR is empty. What does rm -rf "$DIR"/ do?
Show answer
B. Expands to rm -rf / and deletes the filesystem
find, xargs and doing one thing to many files
Working levelfind walks a directory tree and selects files by predicate β name, age, size, type β and it is the tool for every 'all the files thatβ¦' question. Its argument order reads as a sentence: where to look, then what to match, then what to do. find /var/log -name '*.log' -mtime +30 means, in /var/log, files named *.log, last modified more than thirty days ago. Quote the pattern, or the shell expands it against the current directory before find ever runs.
Once you have the list you need to act on it, and that is where xargs comes in: it reads names on standard input and builds command lines from them. The naive pipeline breaks the moment a filename contains a space or a newline, because xargs splits on whitespace by default. The fix is a pair that always travels together β find -print0 emits names separated by a null byte, which cannot occur in a filename, and xargs -0 reads them that way.
find -exec does the same job without a pipe and is safe by default. The difference worth knowing is the terminator: -exec cmd {} \; runs the command once per file, while -exec cmd {} + batches many files into one invocation, which for thousands of files is the difference between an hour and a second. And whichever you use, run it with echo in front the first time. A find that selects the wrong set is discovered instantly when it prints and much later when it deletes.
Syntax
# find: where, what, then what to do
find /var/log -name '*.log' -mtime +30 # older than 30 days
find . -type f -size +100M # big files
find . -type d -name node_modules # directories
# ALWAYS dry-run first
find /var/log -name '*.log' -mtime +30 -print
# null-separated: the only safe pipeline for arbitrary names
find . -name '*.tmp' -print0 | xargs -0 rm
# -exec: once per file
find . -name '*.py' -exec grep -l "TODO" {} \;
# -exec with +: batched, vastly faster on many files
find . -name '*.py' -exec grep -l "TODO" {} +
# rename every .txt to .bak
find . -name '*.txt' -print0 | xargs -0 -I{} mv {} {}.bak
# count matches per file, sorted
grep -rc "ERROR" /var/log/*.log | sort -t: -k2 -rn | head
Key points
- Quote the -name pattern. Unquoted, the shell expands it against the current directory and find receives something you did not type.
- find -print0 with xargs -0 is the only pipeline safe for filenames containing spaces or newlines. Use the pair together or not at all.
- -exec {} + batches files into one command instead of one per file. On thousands of files that is seconds instead of an hour.
Practice challenge
Delete every .log file under /var/log that has not been modified in more than 30 days. Some filenames contain spaces. Write the dry-run command first, then the deletion, and say why the naive pipeline is unsafe.
Dry run: find /var/log -name '*.log' -mtime +30 -print
Delete: find /var/log -name '*.log' -mtime +30 -print0 | xargs -0 rm
Why unsafe: xargs splits on whitespace by default, so "error log.log" becomes two arguments. -print0 and -0 separate on a null byte, which cannot occur in a filename.
Dry run: ______
Delete: ______
Why find ... | xargs rm is unsafe: ______
Show a hint
- Quote the -name pattern or the shell expands it first
- One flag pair makes the pipeline safe for any filename
Check yourself
1. Why quote the pattern in find . -name '*.log'?
Show answer
B. Unquoted, the shell expands it before find sees it
2. What makes find -print0 | xargs -0 safe?
Show answer
B. Null cannot appear in a filename, so names with spaces stay intact
3. Difference between -exec cmd {} \; and -exec cmd {} + ?
Show answer
B. \; runs once per file, + batches many files per invocation
The disk is full and the box is slow
Advanced'Disk full' has a specific investigation and it is short. df -h shows usage per filesystem and tells you which one is full β often not the one you assumed, because /var is frequently mounted separately. Then du -sh on candidate directories narrows it down by halving: run it on the top level, follow the biggest number down, and you reach the offender in a handful of steps rather than by guessing.
The trap that wastes an afternoon is deleting a large file and watching free space not change. On Linux, space is only released when the last process holding the file open lets go β delete a 40 GB log that the application still has open and the inode survives, invisible to du, until you restart the process or truncate the file in place. lsof +L1 lists exactly these deleted-but-held files, and truncating with : > file frees the space immediately without breaking the writer.
Slow is a different question and the answer is which resource is exhausted. top or htop shows CPU and memory at a glance; the load average is the queue of runnable processes, so a load of 8 on 4 cores means work is waiting. High memory pressure shows as swap activity, and the kernel's OOM killer terminating your process is recorded in the system log β which is why 'the service just died with no error' is so often answered by dmesg or journalctl rather than by the application's own logs.
Syntax
df -h # which filesystem is full
df -i # inodes: full even with free space
du -sh /var/* | sort -h # halve your way down
du -sh /var/log/* | sort -h | tail
# deleted but still held open -- space not released
lsof +L1
# free it without restarting the writer:
: > /var/log/huge.log # truncate in place
# what is using the machine
top # or htop
uptime # load average: 1, 5, 15 min
free -h # memory and swap
# did the kernel kill it?
dmesg -T | grep -i -E "oom|killed process"
journalctl -u myapp --since "1 hour ago"
journalctl -p err -b # errors this boot
# rotate logs before they fill the disk again
ls /etc/logrotate.d/
Key points
- Deleting a file held open by a process frees nothing until the process closes it. lsof +L1 finds these, and : > file truncates in place without a restart.
- df -i as well as df -h. A filesystem out of inodes reports plenty of free space and still refuses to create a file, which reads as an impossible error.
- Load average is the queue of runnable processes, so compare it to core count. Load 8 on 4 cores means waiting; load 8 on 32 cores is idle.
Practice challenge
df reports /var at 100% but du -sh /var/* adds up to far less than the disk size. You deleted a 30 GB log an hour ago and free space did not change. Explain the cause, give the command that proves it, and give the command that frees the space without restarting the application.
Cause: the application still holds the deleted file open, so the inode and its blocks are not released. du cannot see it because the directory entry is gone.
Prove it: lsof +L1
Free the space: : > /var/log/app.log (truncate in place; restarting the process would also work but is not required)
Cause: ______
Prove it: ______
Free the space: ______
Show a hint
- Deleting removes the name, not necessarily the data
- du walks directory entries - a deleted-but-open file has none
Check yourself
1. You deleted a 40 GB log but df shows no change. Why?
Show answer
B. A process still holds the file open, so the space is not released
2. df shows 40% free but writes fail with 'No space left'. What do you check?
Show answer
A. df -i for inode exhaustion
3. A service died with nothing in its own log. Where do you look?
Show answer
B. dmesg or journalctl for an OOM kill
A script that can run unattended
Job-readyA script you run by hand and a script that runs at 3am are different programs. Interactively you see errors and react; unattended, nothing is watching, so the script must fail loudly rather than continue with bad state. Three settings at the top do most of that work. set -e stops on the first failing command instead of ploughing on. set -u makes an unset variable an error rather than an empty string, which is what turns rm -rf "$DIR"/ into a refusal. set -o pipefail makes a pipeline fail when any stage fails, not just the last β without it, a failing curl piped into a successful grep reports success.
Cron gives you almost no environment. Not your PATH, not your shell aliases, not your working directory, and often not the variables your login shell sets. A script that runs perfectly in your terminal and does nothing under cron has usually just failed to find a binary. Use absolute paths for commands and files, cd to a known directory at the top, and set the variables you depend on explicitly instead of inheriting them.
Then make it observable and safe to repeat. Exit non-zero when it fails, because that is the only signal cron and every scheduler understands. Log with timestamps to a file that rotates. And guard against overlap: a job that runs every five minutes but sometimes takes ten will eventually run twice at once, corrupting whatever it writes, unless a lock file stops the second copy β flock does this in one line and is the difference between a job that is reliable and one that is reliable most of the time.
Syntax
#!/usr/bin/env bash
set -euo pipefail # exit on error, unset var, or failed pipe stage
cd "$(dirname "$0")" # never rely on cron's working directory
LOG=/var/log/nightly.log
log() { echo "[$(date -Is)] $*" >> "$LOG"; }
# do not run twice at once
exec 9>/var/lock/nightly.lock
flock -n 9 || { log "already running, exiting"; exit 0; }
log "starting export"
/usr/bin/python3 /opt/app/export.py --out /data/out.csv
log "finished, $(wc -l < /data/out.csv) rows"
# --- crontab -e ---
# min hour dom mon dow command
0 3 * * * /opt/app/nightly.sh
# capture failures too:
# 0 3 * * * /opt/app/nightly.sh >> /var/log/cron.log 2>&1
# test it the way cron will run it (empty environment)
env -i /bin/bash --noprofile --norc /opt/app/nightly.sh
Key points
- set -euo pipefail at the top of every unattended script. Without pipefail a failing command piped into a successful one reports success and the job looks fine.
- Cron has almost no environment and a different working directory. Absolute paths for binaries and files, and cd at the top of the script.
- Use flock for anything on a schedule. A job that occasionally overruns its interval will eventually run twice at once, and the corruption that causes is hard to attribute.
Practice challenge
A nightly export script works when you run it but does nothing under cron, and twice it has run two copies at once. Write the first four lines it should start with, say why it fails under cron, and give the command that tests it the way cron will run it.
First lines:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
Why it fails: cron provides almost no environment - a different PATH and working directory, so relative paths and unqualified binaries are not found
Prevent overlap: exec 9>/var/lock/export.lock; flock -n 9 || exit 0
Test as cron: env -i /bin/bash --noprofile --norc /opt/app/export.sh
First lines: ______
Why it fails under cron: ______
Prevent overlap: ______
Test as cron: ______
Show a hint
- Three settings on one line stop it continuing after a failure
- A lock file is what stops the second copy starting
Check yourself
1. What does set -o pipefail change?
Show answer
B. A pipeline fails if any stage fails, not just the last
2. Why does a script that works in your terminal fail under cron?
Show answer
B. Cron provides almost no environment β different PATH and working directory
3. A job runs every 5 minutes but sometimes takes 10. What prevents two copies running at once?
Show answer
B. A lock file, e.g. with flock
Common questions
Do I need any background to start Linux?
No. This track begins at its own beginning and assumes nothing, and the first lesson explains what the thing is before showing you any syntax.
How long does the Linux track take?
About 125 minutes of reading across 8 lessons, plus the practice challenges and 24 multiple-choice questions, which is where the time actually goes.
Is it free?
Yes, and there is no account. Everything runs in your browser.
More: all 15 tracks · what employers actually ask for · the full syllabus