Linux cheat sheet
A scannable Linux reference: 39 short snippets across 14 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Shell basics and navigation | The shell expands globs before the command runs, and expands variables inside double quotes but not single quotes. Get | lesson |
| Files and permissions | Permissions are evaluated in order: owner, then group, then others. The first category you match decides what you get — | lesson |
| Working with text | | sends one program's output into the next. File descriptor 1 is stdout, 2 is stderr — that is why 2>&1 means | lesson |
| Processes, ports and jobs | 'Address already in use' means something holds the port. Find it with lsof -i :PORT rather than blindly restarting | lesson |
| SSH, packages and services | Key-based login without passwords, copying files safely, installing software, and managing services | lesson |
| Users, groups and privilege | Every process runs as a numeric user id. Names are only a convenience: the kernel checks the UID and GID, and root is | lesson |
| Installing software across distributions | Linux distributions ship a package manager that resolves dependencies against signed repositories. The commands differ | lesson |
| Disks, filesystems and mounts | Block devices, partitions, mkfs, fstab entries by UUID, and extending a volume without losing data | lesson |
| Networking: addresses, DNS and firewalls | Interfaces and routes, DNS resolution, the tools that prove where a connection dies, and firewall rules that do not | lesson |
| Archiving, syncing and backups | tar and compression formats, rsync options and semantics, incremental snapshots, and a backup you have actually | lesson |
| Scheduling work: cron and systemd timers | Cron runs your command through /bin/sh with a nearly empty environment and a working directory of $HOME. Aliases, node | lesson |
| systemd units and reading logs | Unit types and dependencies, a service unit you can trust, restart policies, and getting real answers out of journalctl | lesson |
| Environment, dotfiles and shell configuration | Variables and PATH precedence, which startup file runs when, and keeping dotfiles reproducible across machines | lesson |
| Performance triage and troubleshooting | A repeatable method for a slow server: load, memory, CPU, disk and I/O, then syscall-level evidence | lesson |
Quick snippets
Shell basics and navigation
Paths
pwd # print working directory
ls -la # long list, including hidden files
cd /var/log
cd ~/projects
cd -
# tab completion prevents typos
cat /etc/ho<TAB>
The daily dozen
mkdir -p src/components # -p creates parents, no error if exists
touch notes.txt
cp file.txt backup.txt
cp -r src/ src.bak/
mv old.txt new.txt
rm -i important.txt # ask before deleting
Wildcards and quoting
ls *.js # zero or more characters
ls file?.txt # exactly one character
cp src/*.js dist/
grep 'error 500' app.log # quote anything with spaces
grep "$PATTERN" app.log # expand the variable
grep '$100' prices.txt # single quotes keep the $ literalFull lesson: Shell basics and navigation →
Files and permissions
Reading ls -l
-rwxr-xr-- 1 ada dev 4096 Sep 17 10:00 deploy.sh
│└┬┘└┬┘└┬┘
│ │ │ └── others: r-- (read only)
│ │ └───── group: r-x (read + execute)
│ └──────── owner: rwx (full)
└────────── file type: - file, d directory, l symlink
Changing permissions
chmod +x deploy.sh # add execute for everyone
chmod u+x,g-w file # symbolic: user +x, group -w
chmod 755 script.sh # numeric: owner 7, group 5, others 5
chmod 600 id_rsa # private key: owner read/write only
chmod -R 644 public/ # recursive (careful!)
Ownership
chown ada:dev file.txt # owner:group
chown -R www-data:www-data /srv/app
id # who am I, which groups
groups
usermod -aG docker ada # append to a group (needs logout)Full lesson: Files and permissions →
Working with text
Looking at files
less app.log # space to page, / to search, q to quit
head -20 app.log
tail -50 app.log
tail -f app.log # follow - watch new lines arrive
nl file.txt # numbered lines
wc -l file.txt # count lines
Searching with grep
grep 'ERROR' app.log
grep -i 'error' app.log # case-insensitive
grep -n 'timeout' app.log # include line numbers
grep -r 'TODO' src/ # recursive
grep -v 'DEBUG' app.log # invert: lines NOT matching
grep -E '4[0-9]{2}|5[0-9]{2}' access.log # extended regex
grep -c 'ERROR' app.log # count matching lines
Pipes and redirection
cat app.log | grep ERROR | wc -l # count errors
ps aux | grep node | grep -v grep
history | awk '{print $2}' | sort | uniq -c | sort -rn | head
grep ERROR app.log > errors.txt # overwrite
grep ERROR app.log >> errors.txt # append
command 2> errors.txt # stderr only
command > out.txt 2>&1 # both togetherFull lesson: Working with text →
Processes, ports and jobs
What is running
ps aux | grep nginx
top # live overview, q to quit
htop # friendlier top (install if missing)
pgrep -l node
pstree -p | head # parent/child relationships
Stopping processes
kill 1234 # SIGTERM - polite request to stop
kill -9 1234 # SIGKILL - cannot be caught, use last
pkill -f 'node app'
killall nginx
Ports and connections
ss -ltnp # listening TCP ports + process
lsof -i :3000 # who owns port 3000
ss -tuln
curl -I localhost:3000 # quick health check
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.comFull lesson: Processes, ports and jobs →
SSH, packages and services
Connecting
ssh [email protected]
ssh -p 2222 [email protected]
ssh -i ~/.ssh/id_ed25519 ada@server
# keys beat passwords: immune to brute force, better automation
ssh-keygen -t ed25519 -C 'ada@example'
ssh-copy-id ada@server
Connecting
# ~/.ssh/config - then just: ssh web
Host web
HostName 203.0.113.10
User ada
Port 2222
IdentityFile ~/.ssh/id_ed25519
Copying files
scp file.txt ada@web:/var/www/
scp -r ./dist ada@web:/var/www/app/
scp ada@web:/var/log/app.log ./ # download
# rsync: only transfers differences, resumable
rsync -avz --progress ./dist/ ada@web:/var/www/app/
rsync -avz --exclude 'node_modules/' ./ ada@web:/srv/app/Full lesson: SSH, packages and services →
Users, groups and privilege
Accounts, UID and the password files
whoami # effective user name
id # uid, gid and every supplementary group
groups
getent passwd ada # resolves through NSS, so LDAP/SSSD accounts work too
getent passwd www-data # a service account: no login shell by design
Delegating privilege with sudo
sudo -l # what am I allowed to run?
sudo -u postgres psql # run one command as another user
sudo -i # interactive root login shell
sudo -s # root shell that keeps your environment
sudo visudo # validates syntax before saving
sudo visudo -f /etc/sudoers.d/deploy
Delegating privilege with sudo
# /etc/sudoers.d/deploy
# %group syntax applies to every member of the group
%developers ALL=(ALL) ALL
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginxFull lesson: Users, groups and privilege →
Installing software across distributions
apt and dpkg in practice
# add a third-party repository the careful way
curl -fsSL https://example.com/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/example.gpg
echo "deb [signed-by=/usr/share/keyrings/example.gpg] https://example.com/apt stable main" |
sudo tee /etc/apt/sources.list.d/example.list
sudo apt update
Beyond the system package manager
sudo dnf install nginx
sudo dnf history # every transaction, and its id
sudo dnf history undo 42 # roll one back
dnf provides "*/nginx.conf" # which package ships this path
sudo pacman -Syu # refresh and upgrade together (Arch)
# building from source when no package fits
sudo apt install build-essential pkg-config
./configure --prefix=/usr/local && make -j"$(nproc)" && sudo make installFull lesson: Installing software across distributions →
Disks, filesystems and mounts
Seeing what you have
lsblk -f # tree of disks, partitions, filesystems, UUIDs
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
blkid # UUID and filesystem type per device
df -h # space per mounted filesystem
df -i # inode usage - full inodes look like a full disk
du -sh /var/log/* | sort -h | tail
findmnt / # where a mount came from and with which options
mount | column -t
Partitioning, formatting and LVM
# LVM: grow a filesystem later without downtime
sudo pvcreate /dev/sdc
sudo vgcreate vgdata /dev/sdc
sudo lvcreate -L 20G -n app vgdata
sudo mkfs.ext4 /dev/vgdata/app
# later, after adding a disk
sudo pvcreate /dev/sdd
sudo vgextend vgdata /dev/sdd
sudo lvextend -r -l +100%FREE /dev/vgdata/app # -r resizes the filesystem tooFull lesson: Disks, filesystems and mounts →
Networking: addresses, DNS and firewalls
Interfaces, routes and DNS
ip -br a # one line per interface: up/down plus address
ip a show eth0
ip -4 a
ip r # routing table; look for the default gateway
ip neigh # ARP/neighbour cache
nmcli device status
nmcli con show eth0
resolvectl status # what systemd-resolved actually uses
cat /etc/resolv.conf # often a stub pointing at 127.0.0.53
cat /etc/hosts # checked before DNS
Interfaces, routes and DNS
dig example.com A +short
dig example.com MX
dig -x 203.0.113.10 # reverse lookup
getent hosts example.com # the same path the application library uses
# getent honours /etc/hosts and NSS; dig does not - compare the two when they disagree
Proving where a connection dies
ping -c 3 1.1.1.1 # is there a path at all?
mtr -rwzc 20 example.com # per-hop loss, better than a single traceroute
traceroute -T -p 443 example.com
curl -v https://example.com # DNS, TCP, TLS and HTTP in one trace
curl -sS -o /dev/null -w '%{http_code} %{time_connect}s %{time_total}s\n' https://example.com
curl --resolve example.com:443:203.0.113.10 https://example.com # test before DNS changes
sudo ss -tnp state established # who is connected right now
sudo tcpdump -i any -nn port 443 -c 20Full lesson: Networking: addresses, DNS and firewalls →
Archiving, syncing and backups
tar and compression
tar -czf app-2026-09-18.tar.gz -C /srv app # archive a directory
tar -tzf app-2026-09-18.tar.gz | head # list without extracting
tar -xzf app-2026-09-18.tar.gz -C /tmp/restore # extract somewhere else
tar --exclude='node_modules' --exclude='*.log' -czf backup.tar.gz -C /srv app
tar -cJf archive.tar.xz big-directory # xz: smallest, slowest
tar -c --zstd -f archive.tar.zst dir # zstd: fast and small
tar -cf - dir | zstd -T0 > dir.tar.zst # compress in parallel
id # note the numeric uid/gid of the owner
tar --numeric-owner -xvzf backup.tar.gz # restore ownership as recorded
rsync and what the trailing slash means
rsync -av --progress src/ dest/ # contents of src into dest
rsync -av src dest/ # the directory src itself, inside dest
rsync -av --delete --dry-run ./public/ deploy@web:/var/www/public/
rsync -av --delete ./public/ deploy@web:/var/www/public/
rsync -avz --partial --append-verify big.iso deploy@web:/srv/
rsync -av -e "ssh -p 2222" ./data/ deploy@web:/srv/data/
rsync -av --exclude-from=.rsyncignore ./ ./backup/
# incremental snapshots: unchanged files become hard links, storage stays flat
rsync -av --link-dest=../2026-09-17 ./ /backups/2026-09-18/Full lesson: Archiving, syncing and backups →
Scheduling work: cron and systemd timers
Crontab syntax
crontab -e # edit your own crontab
crontab -l # list it
sudo crontab -u deploy -l # someone else's
sudo crontab -u deploy -e
Crontab syntax
# /etc/cron.d/nightly-backup (root, note the user column)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
30 2 * * * deploy /opt/app/backup.sh >> /var/log/backup.log 2>&1
# in a personal crontab there is no user column
15 3 * * 0 /usr/bin/find /srv/tmp -type f -mtime +14 -delete
The environment trap
#!/usr/bin/env bash
# a cron-friendly script: no assumptions about the caller
set -euo pipefail
export PATH="/usr/local/bin:/usr/bin:/bin"
cd /opt/app || exit 1
. /opt/app/venv/bin/activate
echo "$(date -Is) starting"
/opt/app/bin/run.sh
echo "$(date -Is) finished with status $?"Full lesson: Scheduling work: cron and systemd timers →
systemd units and reading logs
Units and dependencies
systemctl list-units --type=service --state=running
systemctl list-unit-files 'app*'
systemctl cat app.service # every file that contributes to the unit
systemctl show app -p ExecStart -p Restart -p User
systemctl status app --no-pager
systemctl daemon-reload # after ANY unit file change
journalctl -u app -n 100 --no-pager
journalctl without guessing
journalctl -u app -f # follow one unit
journalctl -u app --since "1 hour ago" --no-pager
journalctl -u app --since "2026-09-18 08:00" --until "2026-09-18 09:00"
journalctl -b -p err # errors since the last boot
journalctl -b -1 -p warning # the previous boot
journalctl _PID=1234 # everything one process logged
journalctl -o json-pretty -n 1 # all structured fields
journalctl --disk-usage
sudo journalctl --vacuum-time=14d
sudo journalctl --vacuum-size=500M
journalctl without guessing
# /etc/systemd/journald.conf
Storage=persistent # keep logs across reboots
SystemMaxUse=1G
MaxRetentionSec=1month
ForwardToSyslog=yes # also feed rsyslog if you collect logs centrallyFull lesson: systemd units and reading logs →
Environment, dotfiles and shell configuration
Variables, export and PATH
NAME=ada # shell variable: not visible to child processes
export NAME # now it is in the environment of every child
export EDITOR=vim LOG_LEVEL=info # multiple at once
env | sort | head # the whole environment
printenv PATH
printenv LANG || echo unset
MYVAR=local bash -c 'echo $MYVAR' # scoped to one command only
unset MYVAR
Variables, export and PATH
echo "$PATH" | tr ':' '\n' # one entry per line, in search order
export PATH="$HOME/.local/bin:$PATH" # prepend: your version wins
export PATH="$PATH:/opt/tool/bin" # append: fallback
type -a python3 # every definition: alias, function, file
command -v python3 # the file that would run
hash -r # forget the shell's command cache after a move
Keeping dotfiles reproducible
bash -n ~/.bashrc # syntax check without running it
bash -x ~/.bashrc 2>&1 | head -30 # trace what actually executes
echo "$PROMPT_COMMAND"Full lesson: Environment, dotfiles and shell configuration →
Performance triage and troubleshooting
A method before a tool
uptime # load average over 1, 5 and 15 minutes
free -h # memory, buffers/cache and swap
df -h / # space; df -i for inodes
dmesg -T | tail -30 # kernel messages: OOM kills, I/O errors, link flaps
systemctl --failed
The main tools and what each answers
sudo apt install sysstat # provides iostat, pidstat and sar
vmstat 1 5
iostat -xz 1 3
sudo iotop -oPa
pidstat -u -r -d 1 5
# read the columns that matter
# vmstat: r = runnable, b = blocked, si/so = swap in/out,
# us/sy/id = cpu split, wa = waiting on I/O
# iostat: %util near 100 and high await = the device is the bottleneck
Going deeper
# raise the limit for a service, not for the whole machine
# /etc/systemd/system/app.service.d/limits.conf
[Service]
LimitNOFILE=65535
sudo systemctl daemon-reload && sudo systemctl restart app
# a container needs it too - it inherits the daemon's limit
docker run --ulimit nofile=65535:65535 app:1.0Full lesson: Performance triage and troubleshooting →
FAQ
Is this Linux cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Git Docker Kubernetes Nginx CI / CD Bash Scripting
Last refreshed 2026-09-27.