← CLI Cheatsheets

Linux CLI

Table of Contents

Linux CLI Cheatsheet

Deep-reference for DevOps engineers. Every section covers the mechanics behind the command — not just syntax. Knowing why a flag behaves a certain way saves hours of debugging at 2 AM.


1. File & Directory Operations

Core Navigation

pwd                        # print working directory (absolute path)
cd -                       # go back to previous directory (uses $OLDPWD)
cd ~username               # jump to another user's home
ls -lahF --color=auto      # long, all files, human-readable sizes, classify
ls -lrt                    # sort by modification time, oldest first — great for logs
ls -li                     # show inode numbers — use when debugging hard links

Senior tip: ls -lrt /var/log/ is your first move when something broke and you don’t know which log changed recently. Newest files appear at the bottom.

mkdir / rmdir

mkdir -p /opt/app/{config,logs,data}   # create nested dirs + brace expansion
mkdir -m 750 /opt/secrets              # create with specific permissions
rmdir dir/                             # fails if dir is not empty (safe)
rm -rf dir/                            # recursive force delete — no confirmation

Gotcha: rm -rf /path/to/dir / — that trailing space before / deletes root. Always double-check paths with a space. Some distros protect / but don’t count on it.

cp / mv

cp -a /src /dst           # archive mode: preserves permissions, timestamps, symlinks
cp -u src dst             # copy only if src is newer than dst (rsync-lite)
cp --reflink=auto src dst # CoW copy on btrfs/xfs — near-instant, no extra disk
mv -n src dst             # no-clobber: don't overwrite dst if it exists
mv -i src dst             # interactive: prompt before overwrite

Why -a over -r: -r copies recursively but drops ownership, timestamps, and symlinks. -a is equivalent to -dR --preserve=all — what you want when cloning config trees or backups.

find — the real workhorse

# Basic patterns
find /var/log -name "*.log" -mtime -1           # modified in last 24h
find /home -name "*.sh" -perm /111              # files with any execute bit set
find /tmp -type f -atime +7 -delete             # delete files not accessed in 7 days
find / -xdev -name "*.conf" 2>/dev/null         # stay on same filesystem (skip /proc, /sys)

# Size-based
find /var -type f -size +100M                   # files over 100MB
find . -size 0 -delete                          # remove empty files

# Owner/permission audits
find / -nouser -o -nogroup 2>/dev/null          # orphaned files (user deleted but files remain)
find / -perm -4000 -type f 2>/dev/null          # SUID files — security audit
find / -perm -2000 -type f 2>/dev/null          # SGID files

# Exec pattern
find /etc -name "*.conf" -exec grep -l "password" {} \;   # find conf files with passwords
find . -name "*.py" -exec chmod 644 {} +                  # +  batches args (faster than \;)

Senior tip: -exec cmd {} + batches multiple files into one invocation. -exec cmd {} \; forks a new process per file. For 10,000 files, + is 100x faster.

FlagMeaning
-mtime -1modified < 1 day ago
-mtime +7modified > 7 days ago
-mmin -30modified < 30 minutes ago
-newer filenewer than reference file
-xdevdon’t cross filesystem boundaries
-maxdepth 2recurse at most 2 levels
-prunedon’t descend into matched dir
ln target linkname          # hard link: same inode, same data
ln -s /path/to/target link  # symlink: pointer to path (can cross filesystems)
ln -sf new_target existing  # force: update existing symlink
readlink -f symlink         # resolve symlink to absolute real path

Hard link internals: A hard link is just a directory entry pointing to the same inode. The file’s data is not deleted until all hard link count reaches zero (ls -li shows inode + link count). Hard links cannot span filesystems or point to directories (kernel restriction to prevent cycles).

Soft link internals: A symlink is a special file containing a path string. When the kernel resolves it, it substitutes the path. If the target moves or is deleted, the symlink is “dangling” — it still exists but points nowhere.

# Detect dangling symlinks
find /usr/local/bin -type l ! -exec test -e {} \; -print

stat / file

stat /etc/passwd            # full metadata: inode, permissions, atime/mtime/ctime, blocks
file /usr/bin/python3       # determine file type by magic bytes, not extension
file -b /some/binary        # brief output (no filename prefix)

Gotcha: atime (access time) is often disabled via noatime mount option for performance. Don’t rely on it for auditing access without verifying mount options in /proc/mounts.

tree

tree -L 2 /etc/nginx        # depth 2 only
tree -I "*.pyc|__pycache__" # ignore patterns
tree -sh                    # show sizes, human-readable
tree -d                     # directories only

2. Text Processing

cat / less / head / tail

cat -n file.txt             # show line numbers
cat -A file.txt             # show non-printing chars (^ for control, $ for EOL) — debug Windows CRLF
less -S file.log            # don't wrap long lines (horizontal scroll with arrow keys)
less +F file.log            # tail-follow mode inside less (press F to activate, ctrl+c to stop)
head -n 50 file.log         # first 50 lines
tail -n 100 -f /var/log/syslog  # follow: print new lines as written
tail -c 1M large.log        # last 1 megabyte

Senior tip: less +F is better than tail -f for log analysis — you can press ctrl+c to stop following and scroll back through history, then press F to resume following.

grep

grep -rn "error" /var/log/          # recursive, show line numbers
grep -i "fail" /var/log/auth.log    # case insensitive
grep -v "DEBUG" app.log             # invert match (exclude lines)
grep -E "error|warn|crit" app.log   # extended regex (alternation)
grep -o 'ip=[0-9.]*' access.log     # print only matched part
grep -c "ERROR" app.log             # count matching lines
grep -A 3 -B 3 "Exception" app.log  # 3 lines after and before match (context)
grep -l "password" /etc/*.conf      # print only filenames, not matches
grep -w "user" /etc/passwd          # whole word match
grep --color=always "ERROR" log | less -R  # colorized grep piped to less

# Practical log analysis
grep "$(date +%b\ %d)" /var/log/syslog     # today's entries
grep -P '\d{3}-\d{4}' contacts.txt         # Perl regex (more powerful)

egrep vs grep -E: egrep is just grep -E. Use grep -Eegrep is deprecated in newer coreutils.

sed — Stream EDitor

# Substitution
sed 's/foo/bar/' file               # replace first occurrence per line
sed 's/foo/bar/g' file              # replace all occurrences
sed 's/foo/bar/gi' file             # case-insensitive, global
sed -i 's/old/new/g' config.txt     # in-place edit
sed -i.bak 's/old/new/g' file       # in-place with backup (safer)

# Line operations
sed -n '10,20p' file                # print lines 10-20 (-n suppresses default output)
sed '5d' file                       # delete line 5
sed '/^#/d' file                    # delete comment lines
sed '/^$/d' file                    # delete empty lines
sed -n '/START/,/END/p' file        # print between patterns

# Real-world usage
sed -i "s/APP_VERSION=.*/APP_VERSION=${VERSION}/" .env   # update version in .env
sed 's/\r//' file.txt > unix_file.txt                    # remove Windows CR characters

Senior tip: Always use -i.bak for in-place edits in scripts. If sed crashes mid-write, you still have the original. Only drop the backup when you’re confident in the pattern.

awk — Pattern Scanning and Processing

# Field processing (default delimiter: whitespace)
awk '{print $1, $3}' file           # print fields 1 and 3
awk -F: '{print $1}' /etc/passwd    # custom delimiter: print usernames
awk 'NR==5' file                    # print line 5 (NR = line number)
awk 'NR>=5 && NR<=10' file          # print lines 5-10

# Conditionals and patterns
awk '/ERROR/ {print $0}' app.log    # print lines matching pattern
awk '$3 > 100 {print $1, $3}' data  # conditional: field 3 > 100
awk 'END {print NR}' file           # count total lines

# Aggregation
awk '{sum += $4} END {print sum}' metrics.log         # sum column 4
awk '{count[$1]++} END {for (k in count) print k, count[k]}' access.log  # frequency count

# Real-world: extract HTTP status codes from nginx log
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# Print 4th field of lines where 2nd field equals "failed"
awk '$2 == "failed" {print $4}' auth.log
VariableMeaning
$0entire line
$1..$NFfields
NRcurrent line number
NFnumber of fields in current line
FSfield separator (set with -F)
OFSoutput field separator
RSrecord separator

cut / sort / uniq / wc

cut -d: -f1,3 /etc/passwd          # delimiter ':', fields 1 and 3
cut -c1-10 file                    # characters 1 to 10

sort -k2 -n file                   # sort by field 2, numerically
sort -k2 -rn file                  # reverse numeric sort
sort -u file                       # sort + deduplicate
sort -t: -k3 -n /etc/passwd        # sort passwd by UID

uniq -c sorted_file                # count consecutive duplicates (must be sorted first)
uniq -d file                       # show only duplicate lines
uniq -u file                       # show only unique lines

wc -l file                         # line count
wc -w file                         # word count
wc -c file                         # byte count

Gotcha: uniq only removes adjacent duplicates. Always pipe through sort first: sort file | uniq -c | sort -rn — the standard frequency-count idiom.

tr / paste / column / tee / diff

tr 'a-z' 'A-Z' < file              # translate lowercase to uppercase
tr -d '\r' < windows.txt           # delete carriage returns
tr -s ' ' < file                   # squeeze repeated spaces into one

paste file1 file2                  # merge files side by side (tab-delimited)
paste -d, file1 file2              # use comma as delimiter

column -t -s, data.csv             # pretty-print CSV as aligned columns
column -t /etc/fstab               # align any whitespace-delimited file

tee output.log | grep ERROR        # write to file AND stdout simultaneously
command 2>&1 | tee -a debug.log    # append stderr+stdout to file while displaying

diff file1 file2                   # compare files line by line
diff -u file1 file2                # unified diff (what patch expects)
diff -r dir1/ dir2/                # recursive directory diff

Senior tip: tee -a in deployment scripts lets you stream output to console while capturing to a log file. Combine with timestamps: command | ts '[%Y-%m-%d %H:%M:%S]' | tee -a deploy.log (requires moreutils).


3. File Permissions & Ownership

Permission Fundamentals

Every file has three permission sets: owner, group, others. Each set has read (r=4), write (w=2), execute (x=1).

-rwxr-x--- 1 deploy app 1234 May 1 10:00 deploy.sh
 ^^^ ^^^ ^^^
 |   |   others: no permissions (---)
 |   group 'app': r-x (read, execute)
 owner 'deploy': rwx (read, write, execute)

chmod — Change Mode

# Symbolic mode
chmod u+x script.sh          # add execute for owner
chmod go-w file.txt          # remove write from group and others
chmod a+r file.txt           # add read for all
chmod u=rwx,g=rx,o= file    # set exact permissions

# Numeric (octal) mode
chmod 755 script.sh          # rwxr-xr-x (typical executable)
chmod 644 config.txt         # rw-r--r-- (typical config file)
chmod 600 ~/.ssh/id_rsa      # rw------- (private key — SSH refuses if looser)
chmod 700 ~/.ssh/            # rwx------ (SSH directory)

# Recursive
chmod -R 755 /var/www/html   # recursive
chmod -R u=rwX,go=rX dir/    # capital X: set execute only if already executable or directory
Common OctalSymbolicUse Case
777rwxrwxrwxNever (security risk)
755rwxr-xr-xDirectories, public scripts
644rw-r–r–Regular config files
640rw-r—–Config with sensitive data
600rw——-Private keys, credentials
700rwx——Private directories
664rw-rw-r–Shared write in same group

Senior tip: Use capital X with -R instead of lowercase x. chmod -R 755 sets execute on files too, which is usually wrong for web content. chmod -R u=rwX,go=rX only sets execute on directories.

chown / chgrp / umask

chown user:group file          # change owner and group
chown -R nginx:nginx /var/www  # recursive
chown --reference=ref file     # copy ownership from reference file
chgrp docker /var/run/docker.sock  # change group only

umask                          # show current umask (e.g., 022)
umask 027                      # new files: rw-r----- new dirs: rwxr-x---
umask -S                       # symbolic display: u=rwx,g=rx,o=

How umask works: umask is a subtraction mask applied to default permissions. Default for files is 666, for directories 777. With umask 022: files get 666 - 022 = 644, dirs get 777 - 022 = 755.

Special Bits

# SUID (Set User ID) — execute as file owner, not caller
chmod u+s /usr/bin/passwd      # passwd needs root access even when run by normal user
chmod 4755 program             # octal: 4 = SUID

# SGID (Set Group ID) — execute as file's group
chmod g+s /usr/bin/wall        # run as group 'tty'
chmod 2755 program
# On directories: new files inherit directory's group
chmod g+s /shared/project/     # all files created here get group 'project'

# Sticky bit — only owner can delete files in directory
chmod +t /tmp                  # anyone can write to /tmp, only owner can delete own files
chmod 1777 /tmp
ls -ld /tmp                    # drwxrwxrwt (note the 't')

Gotcha: SUID on scripts is ignored by the Linux kernel for security reasons. It only works on compiled binaries. If you need privilege escalation for scripts, use sudo.

ACLs — Access Control Lists

Standard Unix permissions are limited to one owner, one group. ACLs add per-user/per-group entries.

getfacl /shared/file           # view ACL
setfacl -m u:jenkins:rw file   # grant jenkins user read+write
setfacl -m g:developers:rx dir # grant developers group read+execute
setfacl -x u:jenkins file      # remove ACL entry for jenkins
setfacl -b file                # remove all ACLs
setfacl -d -m g:devs:rwx dir   # set default ACL (applied to new files in dir)
setfacl -R -m u:deploy:rx /var/app  # recursive

# Check if ACL is in effect: ls shows '+' after permission string
ls -la file                    # -rw-r--r--+ (the + means ACL present)

Senior tip: Default ACLs (-d) on directories are essential in CI/CD shared environments. Set them once: setfacl -d -m g:deploy:rwx /opt/releases/ and every new file inherits the ACL automatically.


4. Process Management

Viewing Processes

ps aux                         # all processes, BSD format (no dash)
ps -ef                         # all processes, UNIX format
ps -eo pid,ppid,pcpu,pmem,comm # custom output columns
ps --sort=-%cpu | head -10     # top 10 CPU-consuming processes
ps -u nginx                    # processes for user nginx
ps -fp $(pgrep java)           # full details for java processes

pgrep -la nginx                # find PIDs by name, show command
pgrep -u root sshd             # PID of sshd owned by root

top / htop

top -b -n 1 -o %CPU            # batch mode (for scripts), sort by CPU
top -p 1234,5678               # monitor specific PIDs

# top interactive keys:
# k - kill process    M - sort by memory    P - sort by CPU
# u - filter by user  f - field management  1 - per-CPU view
# q - quit            z - color mode        c - show full command

Senior tip: In top, press 1 to see per-CPU utilization. A process pegging one of 32 CPUs won’t look alarming in aggregate but is obvious per-core. Critical for diagnosing single-threaded bottlenecks.

kill / killall / pkill

kill -l                        # list all signal numbers and names
kill -15 1234                  # SIGTERM: polite shutdown (default)
kill -9 1234                   # SIGKILL: immediate kill (unblockable)
kill -HUP 1234                 # SIGHUP: reload config (nginx, sshd)
kill -STOP 1234                # pause process
kill -CONT 1234                # resume paused process

killall nginx                  # kill all processes named 'nginx'
killall -HUP sshd              # send HUP to all sshd processes
pkill -9 -u baduser            # kill all processes by user
pkill -f "python worker.py"    # match against full command line

kill -0 1234                   # test if process exists (no signal sent, returns 0/1)
SignalNumberDefault ActionWhen to Use
SIGHUP1TerminateReload config (daemons)
SIGINT2TerminateCtrl+C equivalent
SIGTERM15TerminateGraceful shutdown
SIGKILL9KillForce kill — last resort
SIGSTOP19StopPause process
SIGCONT18ContinueResume paused

Gotcha: SIGKILL cannot be caught, blocked, or ignored by the process. But it also means the process has no chance to flush buffers, close connections, or write state. Data loss is possible. Always try SIGTERM first and wait.

nice / renice — Process Priority

nice -n 19 backup.sh           # start with lowest priority (19=lowest, -20=highest)
nice -n -10 critical_job       # higher priority (requires root for negative values)
renice -n 10 -p 1234           # change priority of running process
renice -n 5 -u nginx           # change priority of all nginx processes

Priority range: -20 (highest) to 19 (lowest). Default is 0.

Background Jobs & Job Control

command &                      # run in background
jobs -l                        # list background jobs with PIDs
fg %1                          # bring job 1 to foreground
bg %2                          # resume job 2 in background
ctrl+z                         # suspend foreground process (sends SIGSTOP)
disown %1                      # detach job from shell (won't be killed on logout)
disown -a                      # disown all jobs

nohup long-job.sh > output.log 2>&1 &   # immune to SIGHUP (survives logout)

Senior tip: disown vs nohup: nohup redirects signals from terminal hangup at startup. disown removes the job from the shell’s job table after it’s running, preventing SIGHUP on shell exit. Use nohup for jobs you expect the terminal to close. Use disown when you forgot nohup.

systemctl / journalctl

# Service management
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl reload nginx          # reload config without dropping connections
systemctl enable nginx          # enable at boot
systemctl disable nginx
systemctl status nginx          # detailed status + recent logs
systemctl is-active nginx       # exit 0 if active (use in scripts)
systemctl is-enabled nginx

# List and query
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service
systemctl list-dependencies nginx

# journalctl log viewing
journalctl -u nginx             # logs for nginx unit
journalctl -u nginx -f          # follow logs
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --since "2026-01-01" --until "2026-01-02"
journalctl -p err -b            # errors since last boot
journalctl -b -1 -p crit        # critical messages from previous boot
journalctl --disk-usage         # journal disk consumption
journalctl --vacuum-size=500M   # trim journal to 500MB

5. Disk & Storage

df / du

df -hT                         # human-readable, show filesystem type
df -i                          # show inode usage (can fill up before disk is "full")
df -h /var                     # space for specific mount point

du -sh /var/log                # total size of directory
du -h --max-depth=1 /var       # size of each subdirectory, one level
du -h --exclude='*.log' /app   # exclude pattern
du -sh * | sort -h | tail -20  # find 20 largest items in current dir
ncdu /var                      # interactive ncurses du (install separately)

Gotcha: Disk showing full but du says it’s not? Deleted files held open by a running process still occupy space. Find them: lsof | grep deleted — the file will show (deleted) but the process still has the fd open. Restart the process to reclaim space.

lsblk / mount / umount

lsblk                          # tree view of block devices
lsblk -f                       # show filesystem type and UUID
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,UUID

mount | column -t              # show mounted filesystems, formatted
mount -t ext4 /dev/sdb1 /mnt/data          # mount with explicit type
mount -o remount,ro /          # remount root read-only (emergency)
mount -o loop image.iso /mnt/iso           # mount ISO file
mount -t nfs server:/share /mnt/nfs

umount /mnt/data               # unmount
umount -l /mnt/data            # lazy umount (detach when no longer busy)
fuser -m /mnt/data             # show which processes use the mount
lsof +D /mnt/data              # alternative: list open files on mount

fdisk / parted / mkfs / fsck

fdisk -l                       # list all disks and partitions
fdisk /dev/sdb                 # interactive partition editor (MBR, up to 2TB)
parted /dev/sdb print          # show partition table (works with GPT too)
parted -s /dev/sdb mklabel gpt # create GPT partition table
parted -s /dev/sdb mkpart primary ext4 0% 100%

mkfs.ext4 -L data /dev/sdb1   # format as ext4 with label
mkfs.xfs /dev/sdb1
mkfs.btrfs /dev/sdb1

# fsck — do NOT run on mounted filesystems
fsck -n /dev/sdb1              # dry run check (read-only)
fsck -y /dev/sdb1              # auto-repair
e2fsck -p /dev/sdb1            # ext4 auto-repair

dd — Disk Duplication

# Disk imaging
dd if=/dev/sda of=/mnt/backup/sda.img bs=4M status=progress

# Wipe disk (overwrite with zeros)
dd if=/dev/zero of=/dev/sdb bs=4M status=progress

# Create a test file of exact size
dd if=/dev/zero of=testfile bs=1M count=1024   # 1GB file

# Bootable USB
dd if=ubuntu.iso of=/dev/sdc bs=4M status=progress oflag=sync

Gotcha: dd has no guard against if= and of= being swapped. dd if=/dev/sdb of=/dev/sda destroys sda. Triple-check your source and destination. Consider ddrescue for recovery tasks — it’s safer and handles I/O errors better.

Swap Management

swapon -s                      # show swap usage
free -h                        # show RAM and swap
cat /proc/swaps                # swap devices

# Create swap file
fallocate -l 4G /swapfile      # fast allocation
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# Add to /etc/fstab: /swapfile swap swap defaults 0 0

swapoff -a                     # disable all swap (before removing swapfile)
cat /proc/sys/vm/swappiness    # current swappiness (default 60)
sysctl vm.swappiness=10        # reduce swap tendency (better for servers)

6. Networking

ip — Modern Network Tool (replaces ifconfig/route)

ip addr show                   # show all interfaces and IPs
ip addr show eth0              # specific interface
ip addr add 192.168.1.10/24 dev eth0   # add IP
ip addr del 192.168.1.10/24 dev eth0   # remove IP
ip link set eth0 up            # bring interface up
ip link set eth0 down          # bring interface down
ip link show                   # show link layer (MAC, MTU, state)

ip route show                  # routing table
ip route add default via 192.168.1.1   # add default gateway
ip route add 10.0.0.0/8 via 172.16.0.1  # static route
ip route del 10.0.0.0/8        # remove route
ip route get 8.8.8.8           # show which route is used for a destination

ip neigh show                  # ARP table
ip -s link show eth0           # interface statistics (packets, errors, drops)

ping / traceroute / dig / nslookup

ping -c 4 8.8.8.8              # 4 packets, then stop
ping -i 0.2 -c 100 host        # fast ping (200ms interval)
ping -M do -s 1472 router      # don't fragment, 1472 byte payload (MTU discovery)

traceroute -n 8.8.8.8          # numeric only (faster — no DNS lookups)
traceroute -T -p 443 host      # TCP traceroute on port 443 (bypasses UDP blocks)
mtr -n --report host           # combined ping+traceroute, report mode

dig google.com                 # DNS lookup
dig google.com MX              # MX records
dig @8.8.8.8 google.com        # query specific DNS server
dig +short google.com          # just the IP
dig -x 8.8.8.8                 # reverse DNS lookup
dig google.com +trace          # trace DNS resolution from root
dig ANY google.com +noall +answer

nslookup google.com
host -t MX google.com          # simple, fast DNS query

Senior tip: dig +short in scripts. nslookup is interactive and harder to parse. For cert expiry and DNS validation in CI pipelines, combine: dig +short TXT _dmarc.example.com

netstat / ss — Socket Statistics

# ss is the modern replacement for netstat
ss -tlnp                       # TCP, listening, numeric, show process
ss -tulnp                      # TCP + UDP
ss -s                          # socket summary statistics
ss -tnp state established      # established TCP connections
ss -tnp dst 192.168.1.1        # connections to specific destination
ss -tnp src :443               # connections from port 443

# legacy netstat (may not be installed)
netstat -tlnp                  # same as ss -tlnp
netstat -an | grep ESTABLISHED | wc -l   # count established connections
netstat -rn                    # routing table

curl / wget

# curl
curl -I https://example.com              # headers only
curl -o file.tar.gz https://url/file     # save to file
curl -L -o file url                      # follow redirects
curl -u user:pass https://api/endpoint   # basic auth
curl -H "Authorization: Bearer $TOKEN" https://api/v1/data
curl -X POST -H "Content-Type: application/json" -d '{"key":"val"}' https://api/
curl -k https://self-signed.host         # skip TLS verification (dev only)
curl --max-time 10 --retry 3 https://url # timeout + retries
curl -w "@curl-format.txt" -o /dev/null -s https://url  # timing breakdown
curl --resolve example.com:443:1.2.3.4 https://example.com  # force IP (CDN testing)

# wget
wget -q -O - https://url | tar xz       # download and extract in one pipe
wget --mirror -p --no-parent https://site/  # mirror website
wget -c https://large-file.tar.gz       # resume download

Senior tip: curl -w with a format file reveals DNS time, TCP connect time, TLS handshake time separately. Essential for diagnosing “slow site” — tells you if slowness is DNS, network, or server response.

nc (netcat) — Network Swiss Army Knife

nc -zv host 22                 # port scan (verbose)
nc -zv host 20-80              # scan port range
nc -l 8080                     # listen on port 8080 (simple server)
nc host 80                     # connect to port 80 (type HTTP manually)
nc -l 1234 > received.txt      # receive file
nc host 1234 < file.txt        # send file
echo "test" | nc -w 1 host 25  # test SMTP connectivity

scp / rsync

scp file user@host:/remote/path          # copy file to remote
scp -r dir/ user@host:/remote/           # recursive
scp -P 2222 file user@host:/path         # custom port

rsync -avz /src/ user@host:/dst/         # archive, verbose, compress
rsync -avz --delete /src/ /dst/          # sync: delete files in dst not in src
rsync -avz --exclude='*.log' /src/ /dst/
rsync -avzn /src/ /dst/                  # dry run (-n)
rsync -avz --progress /large/ /backup/  # show progress per file
rsync -e "ssh -p 2222 -i key.pem" /src/ user@host:/dst/  # custom SSH

Senior tip: Always include trailing slash on source /src/ vs /src — the difference is whether the directory itself is copied or just its contents. /src/ means “copy contents of src into dst”. /src means “copy src directory into dst”. Many engineers get bitten by this.

tcpdump — Packet Capture

tcpdump -i eth0                          # capture on eth0
tcpdump -i any port 80                   # any interface, port 80
tcpdump -i eth0 'host 10.0.0.1'         # traffic to/from host
tcpdump -i eth0 'port 443 and host api.example.com'
tcpdump -i eth0 -w capture.pcap         # write to file (for Wireshark)
tcpdump -r capture.pcap                 # read from file
tcpdump -i eth0 -n -nn                  # no name resolution (faster)
tcpdump -i eth0 tcp[tcpflags] & tcp-syn != 0  # SYN packets only (new connections)

iptables / nftables Basics

iptables -L -n -v                        # list rules with packet/byte counts
iptables -A INPUT -p tcp --dport 22 -j ACCEPT    # allow SSH
iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT       # allow from subnet
iptables -A INPUT -j DROP                # default deny (add last)
iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT  # insert at position 1
iptables-save > /etc/iptables/rules.v4   # persist rules
iptables-restore < /etc/iptables/rules.v4

# nftables (modern replacement)
nft list ruleset
nft add rule ip filter input tcp dport 22 accept

7. User & Group Management

User Operations

useradd -m -s /bin/bash -G sudo,docker username   # create user with home, shell, groups
useradd -r -s /sbin/nologin serviceaccount        # system account (no login)
usermod -aG docker username          # add to group (without -a, replaces groups)
usermod -s /bin/zsh username         # change shell
usermod -L username                  # lock account
usermod -U username                  # unlock account
usermod -e 2026-12-31 username       # set expiry date
userdel -r username                  # delete user and home directory

# Change password
passwd username                      # interactive
echo "user:newpass" | chpasswd       # non-interactive (use in scripts carefully)
passwd -e username                   # force password change at next login
passwd -l username                   # lock (same as usermod -L)

# Query
id username                          # show uid, gid, groups
groups username                      # show groups
finger username                      # detailed user info (if installed)
getent passwd username               # query NSS (works for LDAP users too)

/etc/passwd, /etc/shadow, /etc/group Structure

/etc/passwd: username:x:uid:gid:comment:home:shell
root:x:0:0:root:/root:/bin/bash
nginx:x:101:103:nginx web server,,,:/var/lib/nginx:/sbin/nologin

/etc/shadow: username:hashed_password:lastchg:min:max:warn:inactive:expire
root:$6$salt$hash:19000:0:99999:7:::

/etc/group: groupname:x:gid:member1,member2
docker:x:998:deploy,jenkins

Gotcha: usermod -G group user (without -a) replaces the user’s group list with just group. The -a flag appends. Forgetting -a has locked people out of sudo access in production.

sudo / visudo

sudo command                    # run as root
sudo -u postgres psql          # run as specific user
sudo -i                        # interactive root shell (loads root environment)
sudo -l                        # list allowed sudo commands for current user
sudo -k                        # expire sudo timestamp (require password next time)

visudo                         # safely edit /etc/sudoers (syntax validation)
# In /etc/sudoers:
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx   # passwordless specific command
%developers ALL=(ALL) NOPASSWD: ALL                           # group with full access
jenkins ALL=(ALL) NOPASSWD: /usr/bin/docker                   # CI/CD common pattern

Senior tip: Never edit /etc/sudoers directly. Use visudo — it validates syntax before saving. A syntax error in sudoers can completely lock you out of sudo. visudo -c checks syntax without editing.


8. Package Management

apt (Debian/Ubuntu)

apt update                            # refresh package index
apt upgrade                           # upgrade installed packages
apt full-upgrade                      # upgrade + handle dependency changes
apt install nginx postgresql          # install packages
apt install -y nginx                  # non-interactive (yes to all)
apt remove nginx                      # remove (keep config)
apt purge nginx                       # remove + delete config
apt autoremove                        # remove orphaned packages
apt search "web server"               # search packages
apt show nginx                        # package details
apt list --installed                  # list installed packages
apt list --upgradable                 # show available upgrades
apt-cache policy nginx                # show installed and candidate versions

# Hold/unhold packages (prevent upgrades)
apt-mark hold nginx
apt-mark unhold nginx
apt-mark showhold

dpkg (low-level)

dpkg -i package.deb                   # install local .deb file
dpkg -l nginx                         # show installed package info
dpkg -L nginx                         # list files installed by package
dpkg -S /usr/sbin/nginx               # which package owns a file
dpkg -r nginx                         # remove package
dpkg --configure -a                   # fix broken installs

yum / dnf (RHEL/CentOS/Fedora)

dnf install nginx                     # install
dnf update                            # update all
dnf update nginx                      # update specific
dnf remove nginx
dnf search nginx
dnf info nginx
dnf list installed
dnf history                           # transaction history
dnf history undo 15                   # undo transaction 15
dnf autoremove                        # remove unused dependencies

# Repos
dnf repolist
dnf config-manager --enable epel
dnf install epel-release              # enable EPEL repo

rpm (low-level)

rpm -ivh package.rpm                  # install verbose with progress
rpm -qa                               # list all installed RPMs
rpm -ql nginx                         # list files from package
rpm -qf /usr/sbin/nginx               # which package owns file
rpm -qi nginx                         # package info
rpm --checksig package.rpm            # verify GPG signature

9. Archiving & Compression

tar

# Create archives
tar -czf archive.tar.gz dir/          # create gzip-compressed
tar -cjf archive.tar.bz2 dir/         # bzip2 (slower, better compression)
tar -cJf archive.tar.xz dir/          # xz (best compression, slowest)
tar -cf archive.tar dir/              # no compression

# Extract
tar -xzf archive.tar.gz               # extract gzip
tar -xzf archive.tar.gz -C /target/  # extract to specific directory
tar -xzf archive.tar.gz file.txt      # extract single file
tar -tzf archive.tar.gz               # list contents without extracting

# Useful combinations
tar -czf - /data | ssh host "tar -xzf - -C /backup"  # remote backup over SSH
tar -czf - /src | pv | tar -xzf - -C /dst            # copy with progress (pv)
tar --exclude='*.log' --exclude='.git' -czf app.tar.gz app/

Memory aid: czf = Create + gZip + File. xzf = eXtract + gZip + File. The verb (c/x/t) always comes first.

gzip / bzip2 / xz

gzip file.txt                  # compress (replaces original with file.txt.gz)
gzip -d file.txt.gz            # decompress
gzip -k file.txt               # keep original
gzip -9 file.txt               # max compression
zcat file.txt.gz               # view without decompressing
zgrep "error" file.gz          # grep inside gzipped file

bzip2 file.txt                 # .bz2 — better compression than gzip
bunzip2 file.txt.bz2

xz file.txt                    # .xz — best compression ratio
xz -d file.txt.xz

# Compression speed vs ratio (approximately)
# gzip:  fastest, ~50% reduction
# bzip2: 2x slower than gzip, ~55% reduction
# xz:    10x slower than gzip, ~65% reduction

zip / unzip

zip -r archive.zip dir/        # create zip recursively
zip -e secrets.zip file.txt    # encrypt with password
unzip archive.zip              # extract
unzip archive.zip -d /target/  # extract to directory
unzip -l archive.zip           # list contents
unzip -p archive.zip file.txt  # extract single file to stdout

10. System Information & Monitoring

uname / hostname / uptime / free

uname -a                       # all: kernel, hostname, arch
uname -r                       # kernel version only
uname -m                       # machine hardware (x86_64, aarch64)

hostname                       # current hostname
hostname -f                    # FQDN
hostnamectl set-hostname new-name  # set hostname (systemd)

uptime                         # load averages: 1, 5, 15 minute
w                              # who is logged in + load average
who                            # logged-in users

free -h                        # RAM and swap, human-readable
free -h -s 2                   # refresh every 2 seconds
cat /proc/meminfo              # detailed memory breakdown

Load average interpretation: Load average represents average active (running + waiting) processes. On a 4-core system, load of 4.0 means full utilization. Load of 8.0 means processes are waiting. Load of 1.0 on a 4-core system = 25% utilization.

vmstat / iostat / sar

vmstat 2 10                    # 2-second intervals, 10 samples
# Output: procs, memory, swap, io, system, cpu
# bi/bo: blocks in/out per second (disk I/O)
# wa: CPU waiting for I/O (high wa = I/O bottleneck)
# si/so: swap in/out (non-zero = memory pressure)

iostat -x 2                    # extended disk stats, 2-second intervals
# %util: disk utilization (near 100% = saturated)
# await: average I/O wait time in ms
# r_await/w_await: separate read/write wait

sar -u 2 5                     # CPU utilization, 2s interval, 5 samples
sar -r 2 5                     # memory utilization
sar -d 2 5                     # disk I/O
sar -n DEV 2 5                 # network interface stats
sar -f /var/log/sysstat/sa20   # historical data from specific day

Senior tip: When investigating a production slowdown, run vmstat 1 and watch wa (I/O wait) and si/so (swap activity) simultaneously. High wa + low si/so = disk bottleneck. High si/so = memory pressure causing swapping.

dmesg / lsof / strace

dmesg                          # kernel ring buffer (boot messages, hardware events)
dmesg -T                       # human-readable timestamps
dmesg -w                       # follow (like tail -f)
dmesg | grep -i error          # filter errors
dmesg | grep -i "oom"          # out-of-memory events

lsof                           # list open files (all processes)
lsof -p 1234                   # files opened by PID 1234
lsof -u nginx                  # files opened by user nginx
lsof -i :80                    # processes using port 80
lsof -i TCP:443                # TCP connections on 443
lsof +D /var/log               # files open in directory
lsof | grep deleted            # deleted files still held open

strace -p 1234                 # trace syscalls of running process
strace -f -p 1234              # trace including forked children
strace -c command              # count syscalls (summary)
strace -e trace=open,read,write command   # trace specific syscalls
strace -o strace.log -ff command          # write to file

Senior tip: lsof | grep deleted is your first move when df shows high usage but du doesn’t explain it. Deleted log files held open by a process don’t free space until the process closes them or is restarted.

/proc Filesystem

cat /proc/cpuinfo              # CPU details (model, cores, flags)
cat /proc/meminfo              # memory stats
cat /proc/loadavg              # load averages + running/total processes
cat /proc/uptime               # seconds since boot
cat /proc/version              # kernel version string
cat /proc/mounts               # currently mounted filesystems
cat /proc/net/tcp              # TCP socket table (hex addresses)
ls /proc/1234/                 # directory for PID 1234
cat /proc/1234/cmdline         # full command line of process
cat /proc/1234/status          # process status (memory, threads)
ls -la /proc/1234/fd/          # file descriptors opened by process
cat /proc/1234/net/tcp         # process's network connections
cat /proc/sys/net/ipv4/ip_forward   # check IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward  # enable IP forwarding (temporary)

11. Shell & Scripting Essentials

Pipes and Redirection

command > file            # stdout to file (overwrite)
command >> file           # stdout to file (append)
command 2> errors.log     # stderr to file
command 2>&1              # redirect stderr to same place as stdout
command &> file           # both stdout and stderr to file (bash 4+)
command > /dev/null 2>&1  # silence all output

# Pipes
ls | grep ".sh" | wc -l
ps aux | sort -k3 -rn | head -10    # top 10 by CPU
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head   # top IPs

# Here document
cat > /etc/motd << 'EOF'
Welcome to production server
Unauthorized access is prohibited
EOF
# single-quoted 'EOF' prevents variable expansion

# Process substitution
diff <(ls dir1) <(ls dir2)          # diff output of two commands
comm <(sort file1) <(sort file2)    # compare sorted files

xargs

find /tmp -name "*.tmp" | xargs rm -f       # delete found files
find . -name "*.py" | xargs grep "TODO"     # grep in found files
cat servers.txt | xargs -I{} ssh {} uptime  # -I{}: placeholder
cat urls.txt | xargs -P 8 -I{} curl -o /dev/null {}  # 8 parallel
echo "a b c" | xargs -n1 echo              # one arg per line
find . -name "*.log" | xargs -r gzip        # -r: don't run if no input

Senior tip: xargs -P N runs N processes in parallel. Combine with find -print0 and xargs -0 when filenames might contain spaces: find . -name "*.txt" -print0 | xargs -0 grep "pattern". Without -0, filenames with spaces break everything.

Variables and Loops

# Variables
VAR="value"
echo "$VAR"                         # always quote variables
echo "${VAR}_suffix"                # braces for disambiguation
readonly CONST="immutable"          # prevent modification
unset VAR                           # delete variable

# Default values
echo "${VAR:-default}"              # use 'default' if VAR is unset/empty
echo "${VAR:=default}"              # assign and use 'default' if unset
VAR="${1:?Error: argument required}" # exit with error if unset

# For loop
for host in web1 web2 web3; do
    ssh "$host" "systemctl status nginx"
done

for file in *.log; do
    gzip "$file"
done

for i in $(seq 1 10); do
    echo "Step $i"
done

# While loop
while read -r line; do
    echo "Processing: $line"
done < input_file

while true; do
    if systemctl is-active --quiet nginx; then break; fi
    sleep 5
done

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

Conditionals

# File tests
if [ -f file.txt ]; then echo "regular file"; fi
if [ -d /var/log ]; then echo "directory exists"; fi
if [ -e file ]; then echo "exists (any type)"; fi
if [ -r file ]; then echo "readable"; fi
if [ -x script.sh ]; then echo "executable"; fi
if [ -s file ]; then echo "non-empty"; fi
if [ -L symlink ]; then echo "is symlink"; fi

# String tests
if [ -z "$VAR" ]; then echo "empty string"; fi
if [ -n "$VAR" ]; then echo "non-empty string"; fi
if [ "$A" = "$B" ]; then echo "equal"; fi
if [[ "$string" =~ ^[0-9]+$ ]]; then echo "numeric"; fi

# Numeric tests
if [ "$count" -gt 10 ]; then echo "greater than 10"; fi
if [ "$count" -eq 0 ]; then echo "zero"; fi

# Logical
if [ -f f1 ] && [ -f f2 ]; then echo "both exist"; fi
if [[ -f f1 && -f f2 ]]; then echo "both exist (bash)"; fi

[ vs [[: [[ is bash-specific but safer — handles spaces in strings without quoting issues, supports regex with =~, and doesn’t word-split. Use [[ in bash scripts, [ for POSIX portability.

Safe Script Patterns

#!/usr/bin/env bash
set -euo pipefail
# -e: exit on error
# -u: treat unset variables as errors
# -o pipefail: pipe fails if any command fails (not just last)

# Trap for cleanup
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT SIGINT SIGTERM

# Trap for debugging
trap 'echo "Error on line $LINENO"' ERR

# Logging
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2; }
log "Starting deployment"

# Lock file (prevent concurrent runs)
LOCKFILE="/tmp/my-script.lock"
exec 200>$LOCKFILE
flock -n 200 || { log "Already running"; exit 1; }

Senior tip: set -euo pipefail is the mandatory header for all production scripts. Without -o pipefail, false | true returns exit code 0. Without -u, referencing $UNSET_VAR silently expands to empty string — a common source of bugs like rm -rf "$DIR/" when DIR is unset.


12. SSH & Remote

ssh Basics

ssh user@host                          # connect
ssh -p 2222 user@host                  # custom port
ssh -i ~/.ssh/deploy_key user@host     # specify key
ssh -v user@host                       # verbose (debug connection)
ssh -A user@host                       # forward SSH agent (enables jump hosts)
ssh -X user@host                       # X11 forwarding
ssh user@host "command"                # run remote command
ssh user@host 'bash -s' < script.sh   # run local script remotely

ssh-keygen / ssh-agent

ssh-keygen -t ed25519 -C "user@company.com"       # generate key (Ed25519 recommended)
ssh-keygen -t rsa -b 4096 -C "comment"            # RSA 4096 (legacy compat)
ssh-keygen -p -f ~/.ssh/id_ed25519                # change passphrase
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host    # deploy public key

eval $(ssh-agent -s)                   # start agent in current shell
ssh-add ~/.ssh/id_ed25519              # add key to agent
ssh-add -l                             # list keys in agent
ssh-add -t 3600 key                    # add key with 1-hour expiry

SSH Config (~/.ssh/config)

Host bastion
    HostName 203.0.113.1
    User ec2-user
    IdentityFile ~/.ssh/prod_key
    StrictHostKeyChecking no
    ServerAliveInterval 60

Host prod-*
    User ubuntu
    IdentityFile ~/.ssh/prod_key
    ProxyJump bastion

Host prod-web-01
    HostName 10.0.1.10

Host prod-db-01
    HostName 10.0.2.10

After this config: ssh prod-web-01 auto-jumps through bastion.

Port Forwarding

# Local port forwarding: access remote service locally
ssh -L 5432:db.internal:5432 user@bastion
# Now: psql -h localhost -p 5432 connects to db.internal through bastion

# Remote port forwarding: expose local service to remote
ssh -R 8080:localhost:3000 user@public-server
# Now: public-server:8080 forwards to your local port 3000

# Dynamic (SOCKS proxy)
ssh -D 1080 user@bastion
# Now: configure browser to use localhost:1080 as SOCKS5 proxy
# Browse as if you're on the bastion's network

# Background tunnel with persistent reconnect
ssh -fNL 5432:db:5432 -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes user@bastion
autossh -M 0 -fNL 5432:db:5432 user@bastion  # auto-reconnect (requires autossh)

Senior tip: ProxyJump (or -J flag) replaced ProxyCommand for jump hosts. It’s cleaner and properly chains SSH agent forwarding. Use -J bastion user@internal-host on command line or ProxyJump bastion in config.

sshfs

sshfs user@host:/remote/path /local/mountpoint   # mount remote filesystem
sshfs -o reconnect,ServerAliveInterval=15 user@host:/data /mnt/data
fusermount -u /local/mountpoint                  # unmount (Linux)
umount /local/mountpoint                         # alternative

13. Cron & Scheduling

crontab

crontab -e                     # edit crontab for current user
crontab -l                     # list
crontab -r                     # remove all (careful — no confirmation)
crontab -u username -l         # list for specific user

Cron syntax:

# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-7, 0 and 7 are Sunday)
# │ │ │ │ │
# * * * * *  command

0 2 * * *     /opt/scripts/backup.sh              # daily at 2 AM
0 */6 * * *   /opt/scripts/sync.sh                # every 6 hours
0 9 * * 1-5   /opt/scripts/report.sh              # weekdays at 9 AM
*/5 * * * *   /opt/scripts/healthcheck.sh         # every 5 minutes
@reboot       /opt/scripts/startup.sh             # on boot
@daily        /opt/scripts/daily.sh               # midnight daily

Cron gotchas:

  • PATH is minimal in cron (/usr/bin:/bin). Use full paths or set PATH at top of crontab.
  • No interactive terminal — redirect output: command >> /var/log/cron.log 2>&1
  • Environment variables in shell profile (~/.bashrc) are NOT loaded.
# Good cron job pattern
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@company.com

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

at — One-time Scheduling

at 2:30pm tomorrow             # schedule for tomorrow afternoon
at now + 2 hours               # 2 hours from now
at 10:00 2026-06-01            # specific date
atq                            # list pending jobs
atrm 3                         # remove job 3

systemd Timers (modern cron replacement)

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

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true                # run if missed (e.g., system was off)
Unit=backup.service

[Install]
WantedBy=timers.target

# /etc/systemd/system/backup.service
[Unit]
Description=Backup Service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
systemctl enable --now backup.timer
systemctl list-timers                  # show all timers + next run time
systemctl list-timers --all            # include inactive
journalctl -u backup.service           # view execution logs

Senior tip: Systemd timers have Persistent=true which retriggers missed runs after downtime. Standard cron silently skips missed runs. For critical batch jobs, systemd timers are more reliable.


14. Log Management

/var/log Structure

PathContents
/var/log/syslog or /var/log/messagesGeneral system messages
/var/log/auth.log or /var/log/secureAuthentication, sudo, SSH
/var/log/kern.logKernel messages
/var/log/dmesgBoot + hardware messages
/var/log/nginx/access.logNginx access
/var/log/nginx/error.logNginx errors
/var/log/postgresql/PostgreSQL logs
/var/log/apt/APT package actions

journalctl — systemd Journal

journalctl -n 100                       # last 100 lines
journalctl -f                           # follow (like tail -f)
journalctl -u nginx -f                  # follow specific service
journalctl -p err                       # errors only (emerg,alert,crit,err)
journalctl -p warning..err             # warning through error
journalctl -b                           # since current boot
journalctl -b -1                        # previous boot (crashed system)
journalctl --since "2026-01-01 00:00" --until "2026-01-01 23:59"
journalctl --since "1 hour ago"
journalctl -u nginx --since today -o json-pretty  # JSON output
journalctl --no-pager | grep "Connection refused" | wc -l  # count occurrences

# Space management
journalctl --disk-usage
journalctl --vacuum-time=7d             # delete entries older than 7 days
journalctl --vacuum-size=1G             # trim to 1GB

Log Analysis Patterns

# Real-time monitoring with filtering
tail -f /var/log/nginx/access.log | grep --line-buffered "500"

# Count HTTP status codes
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# Top 10 IPs
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# Slowest requests (from access log with request time)
awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head -20

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

# OOM kills
grep -i "killed process" /var/log/kern.log
dmesg | grep -i "oom\|out of memory"

# Find errors in last 10 minutes
journalctl -p err --since "10 minutes ago" --no-pager

logrotate

logrotate -d /etc/logrotate.conf        # dry run (show what would happen)
logrotate -f /etc/logrotate.conf        # force rotation now
cat /etc/logrotate.d/nginx              # per-app config

Example logrotate config:

/var/log/myapp/*.log {
    daily
    rotate 14              # keep 14 rotations
    compress               # gzip old logs
    delaycompress          # don't compress most recent rotation (process may still write)
    missingok              # don't error if log missing
    notifempty             # skip if empty
    postrotate
        nginx -s reopen    # signal app to reopen log files
    endscript
}

Gotcha: After logrotate moves a log file, the application still writes to the old file descriptor (the moved file). You must send the app a signal to reopen its log file handles, or use copytruncate (copy then truncate in place — causes small data loss window). Always use postrotate/prerotate for proper rotation with running services.


Quick Reference: One-Liners for Production

# What process is listening on port X?
ss -tlnp | grep :8080
lsof -i :8080

# Disk space used by largest directories
du -h --max-depth=2 / 2>/dev/null | sort -h | tail -20

# Find recently changed files (last 10 minutes)
find /etc -mmin -10 -type f

# Kill all processes matching a name
pkill -9 -f "stale_worker"

# Watch a command output refresh every 2s
watch -n 2 'ss -s'

# Show environment variables of a running process
cat /proc/$(pgrep nginx | head -1)/environ | tr '\0' '\n'

# Follow multiple log files simultaneously
tail -f /var/log/nginx/error.log /var/log/app/error.log

# Benchmark disk write speed
dd if=/dev/zero of=/tmp/test bs=4k count=100000 oflag=direct 2>&1

# Check if a remote port is open (pure bash, no nc needed)
timeout 3 bash -c "cat < /dev/null > /dev/tcp/host/22" && echo open

# Capture all traffic to/from a host for 30 seconds
timeout 30 tcpdump -i eth0 -w /tmp/capture.pcap host 10.0.0.50

# Real-time memory pressure
watch -n 1 'free -h; echo; vmstat 1 1'

# Find which service is causing most disk writes
iotop -obP -d 2

# Quick cert expiry check
echo | openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates

# List all open ports in one line
ss -tlnp | awk 'NR>1 {print $4}' | sort -u