Cheat Sheets
Quick reference for everything in this guide.
Port Reference
| Port | Protocol | Service | Public? | Bind to |
|---|---|---|---|---|
| 22 | TCP | SSH | ✅ Yes | 0.0.0.0 |
| 25 | TCP | SMTP | ❌ | — |
| 53 | UDP/TCP | DNS | ❌ (outbound only) | 127.0.0.53 |
| 80 | TCP | HTTP | ✅ Yes (redirect + ACME) | 0.0.0.0 |
| 443 | TCP | HTTPS | ✅ Yes | 0.0.0.0 |
| 587 | TCP | SMTP submission | ❌ (outbound only) | — |
| 3000 | TCP | Nuxt SSR | ❌ Never | 127.0.0.1 |
| 3001 | TCP | NestJS API | ❌ Never | 127.0.0.1 |
| 5432 | TCP | PostgreSQL | ❌ Never | 127.0.0.1 |
| 6379 | TCP | Redis | ❌ Never | 127.0.0.1 |
| 9229 | TCP | Node inspector | ❌ Never in production | — |
| 19999 | TCP | Netdata | ❌ Never | 127.0.0.1 |
# The audit — should show ONLY 22, 80, 443
sudo ss -tulpn | grep -vE "127\.0\.0\.1|\[::1\]"Linux Directory Reference
| Path | Contents |
|---|---|
/etc | System configuration (text files only) |
/etc/nginx | Nginx config, sites-available/, sites-enabled/, snippets/ |
/etc/ssh/sshd_config.d/ | SSH config drop-ins (override the main file) |
/etc/postgresql/16/main/ | postgresql.conf, pg_hba.conf |
/etc/redis/redis.conf | Redis configuration |
/etc/letsencrypt/live/<domain>/ | fullchain.pem, privkey.pem |
/etc/fail2ban/jail.local | fail2ban jails (never edit jail.conf) |
/etc/sudoers.d/ | Scoped sudo rules (mode 440) |
/etc/systemd/system/ | Custom systemd units |
/var/log | All system and service logs |
/var/log/nginx/ | access.log, error.log, per-site logs |
/var/log/postgresql/ | PostgreSQL logs |
/var/log/auth.log | Authentication events |
/var/lib/postgresql/16/main/ | Database files |
/var/lib/docker/ | Images, containers, volumes |
/var/backups/ | Local backups |
/var/www | Conventional web root |
/home/deploy | Deploy user home (must be 755) |
/home/deploy/apps/ | Applications |
/home/deploy/.nvm/ | Node versions |
/home/deploy/.pm2/ | PM2 state, dump.pm2, logs |
/usr/local/bin | Manually-installed binaries and symlinks |
/tmp | Temporary, wiped on reboot |
/proc/<pid>/environ | A process's environment |
Useful Commands Cheat Sheet
# --- Navigation and files ---
pwd # where am I
ls -lah # detailed listing with hidden files
ls -ltr # sort by time, newest last
cd - # previous directory
mkdir -p a/b/c # create nested directories
cp -a src dst # copy preserving everything
mv old new # rename or move
rm -rf dir # ⚠️ permanent — `ls` the path first
ln -s target link # symlink (target first)
tar -czf out.tar.gz dir # create archive
tar -tzf out.tar.gz # list before extracting
tar -xzf out.tar.gz -C /dst # extract
# --- Reading ---
less file # pager: /search, F=follow, q=quit
tail -f -n 200 file # follow a log
head -n 50 file
grep -rn "text" ./src # recursive with line numbers
grep -A5 -B5 "Error" app.log # with context
grep -v "healthcheck" access.log
find /var/log -name "*.log" -mtime -1
find . -size +100M
# --- Processes ---
ps aux --sort=-%mem | head
pgrep -a node
kill 1234 # SIGTERM (polite)
kill -9 1234 # ⚠️ SIGKILL (last resort)
pkill -f "dist/main.js"
htop
# --- Resources ---
df -h # disk
df -i # inodes
du -h --max-depth=1 / | sort -rh | head -20
free -h # read `available`, not `free`
uptime # load average vs `nproc`
vmstat 1 5
sudo dmesg -T | grep -i "killed process"
# --- Network ---
sudo ss -tulpn # ⭐ listening sockets
ss -s # summary
nc -vz host 443 # is a port reachable?
curl -I https://example.com
curl -sv https://example.com 2>&1 | head -30
curl -w "\n%{time_total}s\n" -o /dev/null -s https://example.com
dig example.com +short
dig @1.1.1.1 example.com +short
# --- Permissions ---
chmod 600 .env # owner read/write only
chmod 755 dir # standard directory
chown -R deploy:deploy dir
namei -l /path/to/file # ⭐ permissions of every path component
sudo -u www-data stat /path # can Nginx read this?systemctl Cheat Sheet
sudo systemctl start <svc>
sudo systemctl stop <svc>
sudo systemctl restart <svc> # drops connections
sudo systemctl reload <svc> # ⭐ re-read config, no disconnection
sudo systemctl status <svc>
sudo systemctl enable <svc> # start on boot
sudo systemctl disable <svc>
sudo systemctl enable --now <svc> # enable and start
systemctl is-active nginx postgresql redis-server
systemctl is-enabled nginx
systemctl --failed # anything that failed to start
systemctl list-timers # scheduled units (certbot)
systemctl list-units --type=service --state=running
sudo systemctl daemon-reload # after editing a unit file
sudo systemctl edit <svc> # create an override drop-in
# Logs
journalctl -u nginx -f
journalctl -u nginx -n 100
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx -p err
journalctl -k # kernel
journalctl -b -1 # previous boot ⭐ after an unexplained reboot
journalctl --disk-usage
sudo journalctl --vacuum-size=500MSSH Cheat Sheet
# --- Connecting ---
ssh user@host
ssh -p 2222 user@host # non-standard port
ssh -i ~/.ssh/key user@host # specific key
ssh -v user@host # ⭐ verbose — first debugging step
ssh -o IdentitiesOnly=yes -i ~/.ssh/key user@host
ssh user@host "command" # ⚠️ non-interactive — no ~/.bashrc
# --- Keys ---
ssh-keygen -t ed25519 -C "me@laptop" -f ~/.ssh/id_ed25519
ssh-keygen -lf ~/.ssh/id_ed25519.pub # fingerprint
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
ssh-keyscan -p 22 host >> ~/.ssh/known_hosts
ssh-keygen -R host # remove a host key entry
# --- Agent ---
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l
ssh-add -D # forget all keys
# --- Tunnels ---
ssh -L 5433:127.0.0.1:5432 user@host # ⭐ reach PostgreSQL safely
ssh -fN -L 5433:127.0.0.1:5432 user@host # background
ssh -L 19999:127.0.0.1:19999 user@host # Netdata
# --- Files ---
scp file user@host:/tmp/
scp -P 2222 file user@host:/tmp/ # ⚠️ uppercase -P for scp
scp -r dir user@host:/tmp/
rsync -avz --progress ./dist/ user@host:/var/www/app/dist/
# --- Server side ---
sudo sshd -t # ⭐ validate config
sudo sshd -T # ⭐ EFFECTIVE config (resolves includes)
sudo systemctl reload ssh
sudo journalctl -u ssh -fRequired permissions
| Path | Mode |
|---|---|
~ (home) | 755 |
~/.ssh | 700 |
| private key | 600 |
authorized_keys | 600 |
~/.ssh/config | 600 |
UFW Cheat Sheet
sudo ufw status verbose
sudo ufw status numbered
sudo ufw show added # ⭐ staged rules before enabling
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # ⚠️ ALWAYS before `enable`
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow "Nginx Full"
sudo ufw limit ssh/tcp # rate-limit connections
sudo ufw allow from 198.51.100.42 to any port 22 proto tcp
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp
sudo ufw deny from 45.148.10.92
sudo ufw insert 1 deny from 45.148.10.92
sudo ufw delete 4 # by number (delete highest first)
sudo ufw delete allow 5432/tcp # by rule
sudo ufw enable
sudo ufw disable
sudo ufw reload
sudo ufw reset # ⚠️ deletes ALL rules and disables
sudo ufw logging on
sudo grep 'UFW BLOCK' /var/log/syslog | tail -20
sudo ufw app list
sudo ufw app info "Nginx Full"DOCKER BYPASSES UFW
A container published as -p 5432:5432 is publicly reachable regardless of UFW. Always use -p 127.0.0.1:5432:5432, or omit ports: entirely.
Nginx Cheat Sheet
sudo nginx -t # ⭐ validate — ALWAYS before reload
sudo nginx -T # ⭐ dump the full resolved config
sudo nginx -s reload
sudo systemctl reload nginx # ⭐ preferred — no dropped connections
sudo systemctl restart nginx # only after a package upgrade
sudo nginx -V # compiled modules
sudo ln -s /etc/nginx/sites-available/site /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
# Analysis
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
sudo awk '{print $(NF-1), $7}' /var/log/nginx/access.log | sort -rn | head -20
sudo grep -E ' 5[0-9][0-9] ' /var/log/nginx/access.log | tail -30Essential directives
server_name app.example.com; # matched against the Host header
listen 443 ssl; http2 on;
root /path/to/static;
try_files $uri $uri/ @fallback;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host; # ⭐ without this, redirects break
proxy_set_header X-Forwarded-Proto $scheme; # ⭐ without this, app thinks it's HTTP
proxy_http_version 1.1; # ⭐ required for WebSockets
proxy_read_timeout 7d; # ⭐ for /socket.io/
proxy_buffering off; # ⭐ for /socket.io/
client_max_body_size 20M; # default is 1M
limit_req zone=login burst=5 nodelay;
return 301 https://$host$request_uri;Location priority: = exact → ^~ prefix (stops regex) → ~/~* regex, in order → plain prefix, longest wins.
PM2 Cheat Sheet
pm2 start ecosystem.config.cjs
pm2 list
pm2 status
pm2 describe api
pm2 monit # live dashboard
pm2 reload api --update-env # ⭐ zero downtime (cluster mode only)
pm2 restart api --update-env # brief downtime
pm2 stop api
pm2 delete api
pm2 reload all
pm2 logs
pm2 logs api --lines 200
pm2 logs api --err --nostream
pm2 logs api --raw | jq # structured logs
pm2 flush
pm2 startup # print the boot-install command
pm2 save # ⭐ REQUIRED — persist the process list
pm2 resurrect # restore from the saved dump
pm2 unstartup systemd
pm2 env 0 # ⭐ verify environment after a reload
pm2 reset api # reset restart counters
pm2 prettylist | jq '.[] | {name, status, restart_time}'
pm2 ping # is the daemon alive?
pm2 update # after upgrading PM2 itself
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 14THE THREE PM2 GOTCHAS
pm2 restartwithout--update-envkeeps the old environment. Changed.env? Use--update-env.pm2 saveis separate frompm2 startup. Without it, a reboot starts nothing.reloadonly gives zero downtime in cluster mode with ≥2 instances. In fork mode it is just a restart.
PostgreSQL Cheat Sheet
# --- Connecting ---
sudo -u postgres psql
psql -U myapp -h 127.0.0.1 -d myapp_production
psql "postgresql://user:pass@127.0.0.1:5432/db"
docker compose exec postgres psql -U myapp -d myapp_production
# --- Service ---
sudo systemctl status postgresql
sudo systemctl reload postgresql # most config changes
sudo systemctl restart postgresql # shared_buffers, max_connections
sudo -u postgres pg_isready
sudo tail -f /var/log/postgresql/postgresql-16-main.log
# --- Backup / restore ---
pg_dump -U myapp -h 127.0.0.1 -Fc -f backup.dump myapp_production
pg_dump -U myapp -h 127.0.0.1 myapp_production | gzip > backup.sql.gz
sudo -u postgres pg_dumpall --globals-only -f globals.sql # ⭐ roles
pg_restore -U postgres -d target_db --no-owner --no-privileges backup.dump
pg_restore --list backup.dump # ⭐ verify a dump
gunzip -c backup.sql.gz | psql -U myapp -d myapp_production
# --- Admin ---
sudo -u postgres createdb mydb
sudo -u postgres createuser --pwprompt myuser
sudo -u postgres dropdb mydb
sudo -u postgres psql -c "SHOW config_file;"
sudo -u postgres psql -c "SHOW hba_file;"psql meta-commands
| Command | Shows |
|---|---|
\l | Databases |
\c db | Connect |
\dt+ | Tables with sizes |
\d table | Table structure |
\du | Roles |
\dp | Privileges |
\di | Indexes |
\x auto | Expanded output when needed |
\timing | Show query times |
\q | Quit |
Diagnostic queries
-- Running queries
SELECT pid, now()-query_start AS dur, state, left(query,80)
FROM pg_stat_activity WHERE state != 'idle' ORDER BY dur DESC;
-- Connections by state
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- Kill a query
SELECT pg_cancel_backend(1234); -- polite
SELECT pg_terminate_backend(1234); -- forceful
-- Table sizes
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS total
FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;
-- Slowest queries (needs pg_stat_statements)
SELECT substring(query,1,80), calls, round(mean_exec_time::numeric,2) AS avg_ms
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
-- Cache hit ratio (want > 0.99)
SELECT sum(heap_blks_hit)/nullif(sum(heap_blks_hit)+sum(heap_blks_read),0)
FROM pg_statio_user_tables;
-- Blocked locks
SELECT * FROM pg_locks WHERE NOT granted;
-- Unused indexes
SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE idx_scan < 50 ORDER BY pg_relation_size(indexrelid) DESC;Prisma
pnpm prisma generate # ⭐ required after every install
pnpm prisma migrate deploy # ⭐ the ONLY production migration command
pnpm prisma migrate status
pnpm prisma migrate dev # ⚠️ DEVELOPMENT ONLY — can reset the database
pnpm prisma db push # ⚠️ prototyping only — can drop columns
pnpm prisma studio # local GUIRedis Cheat Sheet
redis-cli # then AUTH <password>
REDISCLI_AUTH="$PASS" redis-cli ping # ⭐ password not in `ps`
redis-cli -n 1 # database 1
redis-cli INFO
redis-cli INFO memory
redis-cli INFO stats | grep evicted
redis-cli DBSIZE
redis-cli --stat # live stats
redis-cli --latency
redis-cli --bigkeys
redis-cli --scan --pattern 'sess:*' # ⭐ safe — never use KEYS
redis-cli SLOWLOG GET 10
redis-cli BGSAVE # ⭐ never plain SAVE
redis-cli BGREWRITEAOF
redis-cli MEMORY USAGE mykey
sudo systemctl status redis-server
sudo tail -f /var/log/redis/redis-server.logSET key value EX 60 GET key DEL key EXISTS key
TTL key EXPIRE key 60 INCR counter
HSET h f v HGETALL h
LPUSH q v RPOP q
SADD s v SMEMBERS s
ZADD z 100 member ZREVRANGE z 0 9 WITHSCORESNEVER RUN THESE ON PRODUCTION
KEYS * — blocks the single-threaded server while scanning every key. SAVE — blocks until the dump completes (use BGSAVE). FLUSHALL / FLUSHDB — deletes everything. MONITOR — significant performance cost; use SLOWLOG instead.
Docker Cheat Sheet
# --- Containers ---
docker ps
docker ps -a
docker logs -f --tail 100 <name>
docker exec -it <name> bash
docker stop <name>
docker start <name>
docker restart <name>
docker rm -f <name>
docker stats
docker inspect <name>
docker top <name>
# --- Images ---
docker images
docker pull postgres:16.4-alpine
docker rmi <image>
docker history <image>
docker build -t myapp:latest .
docker buildx build --platform linux/amd64 -t myapp:latest .
# --- Compose ---
docker compose up -d
docker compose up -d --build
docker compose down # ✅ volumes SAFE
docker compose down -v # ⚠️ DELETES VOLUMES
docker compose ps
docker compose logs -f api
docker compose exec api sh
docker compose run --rm api pnpm prisma migrate deploy
docker compose restart api
docker compose pull
docker compose config # ⭐ render the fully-resolved config
# --- Volumes and networks ---
docker volume ls
docker volume inspect <vol>
docker network ls
docker network inspect <net>
# --- Disk ---
docker system df
docker system df -v
docker image prune -af --filter "until=168h"
docker builder prune -af
docker container prune -f
docker system prune -a # ⚠️ NEVER add --volumes on a prod hostTHE THREE DOCKER PRODUCTION RULES
docker compose down -vdeletes your database. No confirmation, no undo.- Docker bypasses UFW. Always
127.0.0.1:PORT:PORT, or noports:at all. - Containers run as root by default. Add
USER nonrootto every Dockerfile.
Git Cheat Sheet
# --- Deployment ---
git fetch origin --prune
git reset --hard origin/production # ⭐ deploy: force-match the remote
git clean -fd # ⚠️ never -fdx (deletes .env)
git log -1 --oneline
git rev-parse --short HEAD
git status -sb
# --- Everyday ---
git clone git@github.com:org/repo.git
git checkout -b feature/x
git add -p # stage interactively
git commit -m "message"
git push -u origin feature/x
git pull --rebase
git log --oneline --graph --decorate -20
git diff HEAD~1
git show <sha>
git blame file.ts
# --- Undo ---
git restore file.ts # discard working-tree changes
git restore --staged file.ts # unstage
git reset --soft HEAD~1 # undo commit, keep changes staged
git revert <sha> # ⭐ safe undo on a shared branch
git reflog # ⭐ recover "lost" commits
# --- Remotes ---
git remote -v
git remote set-url origin git@github.com:org/repo.git
# --- Debugging ---
GIT_SSH_COMMAND="ssh -v" git fetch origin
ssh -T git@github.com
git config --list --show-originTHE TWO GIT DEPLOYMENT RULES
- Use
fetch+reset --hard, notpull. A merge conflict mid-deploy leaves the working tree half-updated. dubious ownershipmeans fix the ownership, not addsafe.directory.bashsudo chown -R deploy:deploy /home/deploy/apps/myapp
Certbot Cheat Sheet
sudo certbot --nginx -d app.example.com -d api.example.com --dry-run # ⭐ always first
sudo certbot --nginx -d app.example.com -d api.example.com \
--email you@example.com --agree-tos --no-eff-email --redirect
sudo certbot certificates
sudo certbot renew
sudo certbot renew --dry-run # ⭐ run after every Nginx/firewall change
sudo certbot renew --force-renewal
sudo certbot delete --cert-name old.example.com
systemctl status certbot.timer
sudo journalctl -u certbot --since "60 days ago"
# Verify externally
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
# Certificate and key must match
sudo openssl x509 -noout -modulus -in /etc/letsencrypt/live/D/fullchain.pem | openssl md5
sudo openssl rsa -noout -modulus -in /etc/letsencrypt/live/D/privkey.pem | openssl md5DNS Cheat Sheet
dig example.com +short
dig app.example.com A +short
dig app.example.com AAAA +short
dig example.com MX +short
dig example.com TXT +short
dig example.com NS +short # ⭐ who is authoritative?
dig -x 203.0.113.10 +short # reverse
dig @1.1.1.1 app.example.com +short # a public resolver
dig @ns1.provider.com app.example.com +short # ⭐ the authoritative source
dig +trace app.example.com # full resolution path
nslookup app.example.com
host -t MX example.com
# Test a server BEFORE cutting over DNS
curl -I --resolve app.example.com:443:203.0.113.10 https://app.example.com
# Flush local caches
sudo resolvectl flush-caches # Ubuntu
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder # macOSTHE THREE-QUERY PROPAGATION CHECK
dig @<authoritative-ns> name— is the record correct at the source?dig @1.1.1.1 name— has it propagated?dig name— what do I get locally?
Mismatch at 1 → fix the record. At 2 → wait for the TTL. At 3 → flush your cache or check /etc/hosts.
fail2ban Cheat Sheet
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 198.51.100.42
sudo fail2ban-client set sshd banip 45.148.10.92
sudo fail2ban-client unban --all
sudo fail2ban-client reload
sudo tail -f /var/log/fail2ban.logConfig: /etc/fail2ban/jail.local (never edit jail.conf).
Emergency triage — disk full
df -h && df -i
du -h --max-depth=1 / 2>/dev/null | sort -rh | head -20
sudo journalctl --vacuum-size=200M
pm2 flush
docker image prune -af # ⚠️ never --volumes
docker builder prune -af
sudo apt clean
sudo find /var/log -name "*.gz" -mtime +7 -delete
ls -1dt ~/apps/myapp/releases/* | tail -n +4 | xargs -r rm -rfEmergency triage — site down
# 1. Resources
uptime && free -h && df -h /
# 2. Services
systemctl is-active nginx postgresql redis-server
systemctl --failed
# 3. Application
pm2 list
curl -i http://127.0.0.1:3001/api/health # ⭐ the key test
pm2 logs --err --lines 50 --nostream
# 4. Proxy
sudo tail -30 /var/log/nginx/error.log
# 5. Kernel (OOM? disk errors?)
sudo dmesg -T | tail -20
# 6. External
curl -I https://app.example.com
dig app.example.com +short
sudo certbot certificatesThe five commands that matter most
sudo ss -tulpn | grep -vE "127.0.0.1|::1" # what is publicly exposed?
curl -i http://127.0.0.1:3001/api/health # is the app itself alive?
pm2 logs --err --lines 50 --nostream # why did it break?
sudo nginx -t && sudo systemctl reload nginx # safe config change
df -h && free -h # is the machine healthy?Back to: Overview & Table of Contents