Skip to content

Cheat Sheets ​

Quick reference for everything in this guide.

Port Reference ​

PortProtocolServicePublic?Bind to
22TCPSSH✅ Yes0.0.0.0
25TCPSMTP❌—
53UDP/TCPDNS❌ (outbound only)127.0.0.53
80TCPHTTP✅ Yes (redirect + ACME)0.0.0.0
443TCPHTTPS✅ Yes0.0.0.0
587TCPSMTP submission❌ (outbound only)—
3000TCPNuxt SSR❌ Never127.0.0.1
3001TCPNestJS API❌ Never127.0.0.1
5432TCPPostgreSQL❌ Never127.0.0.1
6379TCPRedis❌ Never127.0.0.1
9229TCPNode inspector❌ Never in production—
19999TCPNetdata❌ Never127.0.0.1
bash
# The audit — should show ONLY 22, 80, 443
sudo ss -tulpn | grep -vE "127\.0\.0\.1|\[::1\]"

Linux Directory Reference ​

PathContents
/etcSystem configuration (text files only)
/etc/nginxNginx 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.confRedis configuration
/etc/letsencrypt/live/<domain>/fullchain.pem, privkey.pem
/etc/fail2ban/jail.localfail2ban jails (never edit jail.conf)
/etc/sudoers.d/Scoped sudo rules (mode 440)
/etc/systemd/system/Custom systemd units
/var/logAll system and service logs
/var/log/nginx/access.log, error.log, per-site logs
/var/log/postgresql/PostgreSQL logs
/var/log/auth.logAuthentication events
/var/lib/postgresql/16/main/Database files
/var/lib/docker/Images, containers, volumes
/var/backups/Local backups
/var/wwwConventional web root
/home/deployDeploy user home (must be 755)
/home/deploy/apps/Applications
/home/deploy/.nvm/Node versions
/home/deploy/.pm2/PM2 state, dump.pm2, logs
/usr/local/binManually-installed binaries and symlinks
/tmpTemporary, wiped on reboot
/proc/<pid>/environA process's environment

Useful Commands Cheat Sheet ​

bash
# --- 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 ​

bash
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=500M

SSH Cheat Sheet ​

bash
# --- 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 -f

Required permissions

PathMode
~ (home)755
~/.ssh700
private key600
authorized_keys600
~/.ssh/config600

UFW Cheat Sheet ​

bash
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 ​

bash
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 -30

Essential directives

nginx
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 ​

bash
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 14

THE THREE PM2 GOTCHAS

  1. pm2 restart without --update-env keeps the old environment. Changed .env? Use --update-env.
  2. pm2 save is separate from pm2 startup. Without it, a reboot starts nothing.
  3. reload only gives zero downtime in cluster mode with ≥2 instances. In fork mode it is just a restart.

PostgreSQL Cheat Sheet ​

bash
# --- 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

CommandShows
\lDatabases
\c dbConnect
\dt+Tables with sizes
\d tableTable structure
\duRoles
\dpPrivileges
\diIndexes
\x autoExpanded output when needed
\timingShow query times
\qQuit

Diagnostic queries

sql
-- 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

bash
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 GUI

Redis Cheat Sheet ​

bash
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.log
SET 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 WITHSCORES

NEVER 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 ​

bash
# --- 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 host

THE THREE DOCKER PRODUCTION RULES

  1. docker compose down -v deletes your database. No confirmation, no undo.
  2. Docker bypasses UFW. Always 127.0.0.1:PORT:PORT, or no ports: at all.
  3. Containers run as root by default. Add USER nonroot to every Dockerfile.

Git Cheat Sheet ​

bash
# --- 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-origin

THE TWO GIT DEPLOYMENT RULES

  1. Use fetch + reset --hard, not pull. A merge conflict mid-deploy leaves the working tree half-updated.
  2. dubious ownership means fix the ownership, not add safe.directory.
    bash
    sudo chown -R deploy:deploy /home/deploy/apps/myapp

Certbot Cheat Sheet ​

bash
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 md5

DNS Cheat Sheet ​

bash
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   # macOS

THE THREE-QUERY PROPAGATION CHECK

  1. dig @<authoritative-ns> name — is the record correct at the source?
  2. dig @1.1.1.1 name — has it propagated?
  3. 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 ​

bash
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.log

Config: /etc/fail2ban/jail.local (never edit jail.conf).

Emergency triage — disk full ​

bash
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 -rf

Emergency triage — site down ​

bash
# 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 certificates

The five commands that matter most ​

bash
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