Back to All Cheatsheet Libraries cheatsheets

Linux

Core commands, filesystem and permissions reference, and everyday sysadmin tasks for the Linux CLI.

Total Items: 0
Category Environment / Scope Name / Utility Syntax / Combo Common Flags / Options Real Usage Example Description & Functionality
8. Services & Packages Debian/Ubuntu apt / dpkg apt update && apt install update, upgrade, install, purge, dpkg -i sudo apt update && sudo apt install -y htop Package management suite for Debian and Ubuntu based distributions.
8. Services & Packages POSIX / All at / nohup at [time] / nohup cmd & nohup.out redirection nohup python3 script.py > app.log 2>&1 & Executes one-time delayed jobs (at) or runs tasks detached from terminal SIGHUP disconnects (nohup).
3. Text Processing POSIX / All awk awk 'program' file -F [delimiter], print $N, BEGIN/END blocks awk -F':' '{print $1, $7}' /etc/passwd Pattern scanning language optimized for parsing columnated log files and structured text.
5. Process & Memory POSIX / All bg / fg / jobs bg [%job] / fg [%job] / jobs -l (show PIDs) jobs -l && fg %1 Lists suspended jobs and manages foreground/background process execution in current shell.
3. Text Processing POSIX / All cat cat [options] file -n (line numbers), -A (show non-printing) cat -n /etc/hosts Concatenates file contents and prints directly to standard output.
2. File & Directory POSIX / All cd cd [dir] - (previous dir), ~ (home) cd /etc/nginx && cd - Changes active directory location. cd - toggles back to previous path.
4. Permissions & Users POSIX / All chgrp chgrp group file -R (recursive) chgrp -R developers /opt/project Changes group ownership of target files and directories.
4. Permissions & Users POSIX / All chmod chmod [mode] file 755, 644, +x, -R (recursive) chmod +x deploy.sh && chmod 600 id_rsa Modifies file mode bits for Owner, Group, and Others using octal (755) or symbolic (+x) notation.
4. Permissions & Users POSIX / All chown chown owner:group file -R (recursive), --reference chown -R www-data:www-data /var/www Changes target file or directory system user ownership and group assignment.
3. Text Processing POSIX / All cmp cmp [options] file1 file2 -l (list byte numbers), -s (silent mode) cmp -s file1.bin file2.bin Compares two files byte by byte to check for identical binary data.
2. File & Directory POSIX / All cp cp [options] src dst -r (recursive), -p (preserve permissions), -a (archive) cp -a /etc/nginx /etc/nginx.bak Copies files and directories. Option -a preserves ownership, mode, and timestamps.
8. Services & Packages POSIX / All crontab crontab [options] -e (edit), -l (list), -r (remove), -u [user] crontab -e / crontab -l Schedules automated recurring background tasks using cron timing syntax.
1. Terminal Keys Shortcut Cut Line / Kill Buffer Ctrl + U / Ctrl + K Readline Cut Buffer Ctrl + U (Cut to start) Cuts line from cursor to beginning (Ctrl+U) or cursor to end (Ctrl+K). Paste with Ctrl+Y.
7. Networking POSIX / All curl / wget curl [opts] URL / wget [opts] URL -I (headers), -X (POST/PUT), -O, -c (continue download) curl -Is https://api.github.com Downloads web assets and interacts with REST APIs over HTTP/HTTPS/FTP.
3. Text Processing POSIX / All cut cut [options] file -d [delimiter], -f [fields], -c [characters] cut -d',' -f1,3 data.csv Extracts specific character ranges or delimited fields from lines in text files.
6. System & Storage POSIX / All df df [options] -h (human-readable), -T (fs type), -i (inodes) df -hT Displays total disk space usage and free capacity for all mounted filesystems.
3. Text Processing POSIX / All diff diff [options] file1 file2 -u (unified output), -y (side-by-side), -r (recursive) diff -u config.orig config.new Compares two text files line by line and outputs differences (used to generate patch files).
7. Networking POSIX / All dig / nslookup dig [domain] [type] / nslookup +short, @server, MX, TXT, AAAA dig example.com MX +short Queries DNS servers for record resolution (A, CNAME, MX, TXT) and troubleshooting.
6. System & Storage POSIX / All dmesg dmesg [options] -T (human timestamps), -l (level: err,warn) dmesg -T -l err,crit Prints kernel ring buffer messages to diagnose boot errors, drive failures, and hardware faults.
8. Services & Packages RHEL/Fedora dnf / rpm dnf install [pkg] install, check-update, remove, rpm -qa sudo dnf install -y nginx Package management suite for Red Hat Enterprise Linux, Fedora, and Rocky Linux.
2. File & Directory POSIX / All du du [options] [path] -s (summary), -h (human readable), -d [depth] du -sh /var/log/* | sort -hr Estimates and prints disk space consumption for specified directory paths.
6. System & Storage POSIX / All fdisk / parted fdisk [device] / parted -l (list partitions) sudo fdisk -l /dev/sda Low-level partition manipulation utilities for creating and resizing MBR and GPT disk tables.
2. File & Directory POSIX / All file file [options] filename -b (brief mode), -i (mime type string) file -i archive.tar.gz Inspects file magic headers to determine true file type (binary, text, encoding, archive).
2. File & Directory POSIX / All find find [path] [expression] -name, -type (f/d), -mtime, -size, -exec find /var/log -name "*.log" -mtime +30 -delete Recursively searches directory trees based on name, age, size, permissions, or executes actions.
5. Process & Memory POSIX / All free free [options] -h (human-readable), -m (MB), -g (GB), -s [delay] free -h Displays total, used, free, shared, buffer/cache, and available RAM memory and Swap space.
6. System & Storage POSIX / All fsck fsck [options] device -y (auto repair), -f (force check) sudo fsck -f -y /dev/sda1 Checks and repairs corrupted filesystem blocks and inode structure errors. Drive must be unmounted.
3. Text Processing POSIX / All grep grep [options] pattern file -i (ignorecase), -r (recursive), -n (line num), -v (invert) grep -rnw "Failed password" /var/log/auth.log Searches files or stdin for lines matching regular expression patterns.
4. Permissions & Users POSIX / All groupadd groupadd [options] groupname -g (custom GID), -r (system group) groupadd -r sysadmins Creates new user group definitions inside `/etc/group`.
3. Text Processing POSIX / All head head [options] file -n [lines], -c [bytes] head -n 20 /var/log/auth.log Prints top N lines (default 10) of input files or pipeline stream.
6. System & Storage POSIX / All hostname / hostnamectl hostnamectl [cmd] status, set-hostname sudo hostnamectl set-hostname node-01 Queries and sets machine network hostname and system metadata.
1. Terminal Keys Shortcut Insert Last Argument Alt + . or Esc + . Readline Argument mkdir my_dir -> cd [Alt+.] Cycles and pastes the final argument from previous commands. Saves typing long paths.
1. Terminal Keys Shortcut Interrupt Signal Ctrl + C SIGINT Signal Ctrl + C Sends SIGINT to immediately stop and terminate the foreground process.
7. Networking POSIX / All ip ip [object] [command] addr, link, route, -c (color) ip -c addr show eth0 / ip route Modern utility to view/configure network interfaces, IP addresses, and routing tables (replaces ifconfig).
8. Services & Packages Systemd journalctl journalctl [options] -u [unit], -f (follow), -p [err], --since "1 hour ago" journalctl -u docker -f --since "10 min ago" Queries and filters systemd binary journal log files.
5. Process & Memory POSIX / All kill / killall kill [signal] PID / killall name -15 (SIGTERM), -9 (SIGKILL), -1 (SIGHUP) kill -15 1234 / killall -9 nginx Sends specified termination signals to processes by process PID or binary name string.
3. Text Processing POSIX / All less less [options] file -N (line numbers), -S (chop long lines), +F (live tail) less -N /var/log/syslog Interactive file pager supporting backward navigation and keyword searching.
1. Terminal Keys Shortcut Line Start / End Jump Ctrl + A / Ctrl + E Readline Shortcut Ctrl + A (Start) / Ctrl + E (End) Moves cursor instantly to beginning (Ctrl+A) or end (Ctrl+E) of command line.
2. File & Directory POSIX / All ln ln [options] target link -s (symbolic link), -f (force override) ln -s /etc/nginx/sites-avail/app /etc/nginx/sites-enabled/ Creates soft (symbolic) or hard filesystem links to target files/directories.
2. File & Directory POSIX / All locate locate [options] pattern -i (ignore case), -r (regex search) locate nginx.conf Fast indexed search using internal mlocate database. Requires running updatedb to refresh.
2. File & Directory POSIX / All ls ls [options] [path] -l (long), -a (all), -h (human), -t (time sort) ls -la --color=auto /var/log Lists directory contents including permissions, owner, size, and mod dates.
6. System & Storage POSIX / All lsblk / blkid lsblk [opts] / blkid [dev] -f (filesystems), -p (full path) lsblk -f && sudo blkid Displays block devices tree hierarchy, drive partition layout, filesystems, and device UUIDs.
6. System & Storage POSIX / All lscpu / lsusb / lspci lscpu / lsusb / lspci -v (verbose), -t (tree mode) lscpu | grep "Model name" Enumerates hardware details for CPU sockets/cores, USB controller buses, and PCI express cards.
7. Networking POSIX / All lsof lsof [options] -i:[port], -u [user], -p [PID], +D [path] sudo lsof -i :80 Lists all open files, directory locks, and associated network sockets held by processes.
2. File & Directory POSIX / All mkdir mkdir [options] dir -p (parents), -m (mode) mkdir -p /var/www/site/public Creates directories. Option -p creates parent subdirectories recursively if missing.
6. System & Storage POSIX / All mkfs mkfs.[type] [device] -t (type: ext4, xfs, btrfs), -L (label) sudo mkfs.ext4 -L "DATA" /dev/sdb1 Formats specified disk partition with a Linux filesystem (ext4, xfs, btrfs, vfat).
6. System & Storage POSIX / All mount / umount mount [dev] [dir] / umount -a (all fstab), -t (type), -o (options: ro,rw) sudo mount -o ro /dev/sdb1 /mnt/data Attaches or unmounts storage device filesystems into designated mount directories.
2. File & Directory POSIX / All mv mv [options] src dst -i (interactive prompt), -n (no clobber), -v (verbose) mv config.tmp config.yaml Moves or renames files and directories within or across filesystems.
7. Networking POSIX / All nc (netcat) nc [options] host port -z (zero-I/O port scan), -v (verbose), -l (listen) nc -zv 192.168.1.50 22-80 Swiss-army knife for TCP/UDP socket testing, banner grabbing, and port connectivity verification.
5. Process & Memory POSIX / All nice / renice nice -n [val] cmd / renice -n PID Priority range -20 (highest) to +19 (lowest) nice -n 19 tar -czf backup.tar.gz /data Sets or alters process CPU scheduling priority to balance resource usage against foreground tasks.
7. Networking POSIX / All nmap nmap [options] target -sS (SYN scan), -p [ports], -A (aggressive/OS detection) nmap -sV -p 22,80,443 192.168.1.0/24 Network exploration security scanner for host discovery, open port detection, and OS fingerprinting.
4. Permissions & Users POSIX / All passwd passwd [options] [username] -l (lock), -u (unlock), -e (expire password) sudo passwd devuser Updates local user account password authentication credentials.
5. Process & Memory POSIX / All pgrep / pkill pgrep [opts] pattern / pkill -f (full command line), -u (user), -9 (SIGKILL) pkill -9 -f "python3 server.py" Looks up processes by regex name (pgrep) or sends termination signals matching full command string.
7. Networking POSIX / All ping ping [options] host -c [count], -i [interval], -4 (IPv4), -6 (IPv6) ping -c 4 1.1.1.1 Sends ICMP ECHO_REQUEST packets to verify reachability and measure round-trip latency.
5. Process & Memory POSIX / All ps ps [options] aux, -ef, --sort=-%cpu, --sort=-%mem ps aux --sort=-%mem | head -n 10 Takes a static snapshot of active system processes, showing PID, CPU/RAM usage, and TTY.
2. File & Directory POSIX / All pwd pwd [options] -P (physical), -L (logical) pwd -P Prints current working directory absolute path, resolving symlinks with -P.
1. Terminal Keys Shortcut Reverse History Search Ctrl + R Bash / Zsh History (reverse-i-search)`ssh': ... Interactively searches command history backwards as you type characters.
2. File & Directory POSIX / All rm rm [options] path -r (recursive), -f (force), -i (interactive) rm -rf /tmp/cache_* Deletes files or directories recursively. Use extreme caution with -rf.
2. File & Directory POSIX / All rmdir rmdir [options] dir -p (parents), --ignore-fail-on-non-empty rmdir -p old_dir/sub_dir Removes empty directories from the filesystem hierarchy.
7. Networking POSIX / All scp / rsync rsync [opts] src user@host:dst -a (archive), -v (verbose), -z (compress), -P (progress) rsync -avzP ./build/ user@remote:/var/www/html/ Transfers and synchronizes files over SSH, updating only modified block deltas.
3. Text Processing POSIX / All sed sed [options] 'script' file -i (in-place write), -e (expression), s/pattern/replace/g sed -i 's/Port 22/Port 2222/g' /etc/ssh/sshd_config Stream editor for inline text parsing, string search and replace, and line filtering.
8. Services & Packages POSIX / All shutdown / reboot shutdown [opts] [time] -h (halt now), -r (reboot), +N (delay minutes) sudo shutdown -h +15 "Server maintenance" Safely signals processes and shuts down or reboots the OS kernel.
3. Text Processing POSIX / All sort sort [options] file -n (numeric), -r (reverse), -k [column], -u (unique) sort -t: -k3 -n /etc/passwd Sorts lines of text files alphabetically or numerically across designated key columns.
7. Networking POSIX / All ss / netstat ss [options] / netstat [options] -t (tcp), -u (udp), -l (listen), -p (process), -n (numeric) ss -tulpn | grep :80 Displays active listening sockets, open network connections, and associated PIDs.
7. Networking POSIX / All ssh ssh [options] user@host -i [key], -p [port], -L (local tunnel), -D (SOCKS proxy) ssh -i ~/.ssh/id_ed25519 user@10.0.0.5 -p 2222 Connects securely to remote host command lines over encrypted channels.
2. File & Directory POSIX / All stat stat [options] file -f (filesystem), -c (format specification) stat /etc/passwd Displays detailed inode metadata, file size, access permissions, owner UID, and timestamps.
4. Permissions & Users POSIX / All su / sudo su - [user] / sudo [cmd] -i (login shell), -u (user target), -v (validate) sudo -i / su - postgres Switches target user shell environment or elevates command privileges using sudo policies.
1. Terminal Keys Shortcut Suspend Job Ctrl + Z SIGTSTP Signal Ctrl + Z (Then run 'bg') Suspends active job and pushes it to background. Resume with fg or bg.
8. Services & Packages Systemd systemctl systemctl [cmd] [service] status, start, stop, restart, enable, disable, reload sudo systemctl restart nginx Controls systemd init services, daemons, and target runlevels.
3. Text Processing POSIX / All tail tail [options] file -f (follow live stream), -n [lines], --retry tail -100f /var/log/nginx/error.log Outputs trailing lines of files. Option -f monitors file growth in real-time.
8. Services & Packages POSIX / All tar tar [options] archive.tar.gz -c (create), -x (extract), -z (gzip), -v, -f (file) tar -czvf backup.tar.gz /var/www Bundles files into tape archives and compresses/extracts using gzip or bzip2.
7. Networking POSIX / All tcpdump tcpdump [options] [filter] -i [interface], -nn (no resolve), -w [file.pcap], port [N] sudo tcpdump -i eth0 -nn port 80 -w http.pcap Command-line packet analyzer that captures network traffic for Wireshark inspection.
3. Text Processing POSIX / All tee tee [options] file -a (append mode), -i (ignore signals) echo "data" | sudo tee -a /etc/config Reads standard input and writes simultaneously to standard output and disk files.
5. Process & Memory POSIX / All top / htop top / htop -u [user], -p [PID], -d [delay] htop -u www-data Dynamic real-time monitoring interface for CPU utilization, RAM consumption, and process trees.
2. File & Directory POSIX / All touch touch [options] file -a (access time), -m (mod time), -c (no create) touch /var/www/app/index.php Creates empty file if missing or updates file access and modification timestamps.
3. Text Processing POSIX / All tr tr [options] set1 [set2] -d (delete), -s (squeeze repeats) cat file.txt | tr 'a-z' 'A-Z' Translates, squeezes, or deletes characters from standard input stream.
7. Networking POSIX / All traceroute / mtr traceroute host / mtr host -n (numeric IP), -T (TCP packets) mtr --report 8.8.8.8 Traces network packet hop paths and pinpoints latency bottlenecks or packet loss.
2. File & Directory POSIX / All tree tree [options] [path] -L [depth], -d (dirs only), -a (all files) tree -L 2 /var/www Displays directory tree structure visually in depth-indented format.
7. Networking POSIX / All ufw / iptables ufw status / iptables -L allow [port], deny, -t nat, -A INPUT sudo ufw allow 22/tcp && sudo ufw enable Manages Linux kernel netfilter packet filtering rules and firewall policies.
4. Permissions & Users POSIX / All umask umask [mask] -S (symbolic output) umask 022 Sets default access permission masking mode for newly created files and directories.
6. System & Storage POSIX / All uname uname [options] -a (all details), -r (kernel release), -m (arch) uname -a Prints kernel name, hostname, OS version, architecture (x86_64/aarch64), and build details.
3. Text Processing POSIX / All uniq uniq [options] [input] -c (count), -d (duplicates only), -u (unique only) sort access.log | uniq -c | sort -nr Filters adjacent duplicate lines from sorted text streams and reports occurrence counts.
2. File & Directory POSIX / All updatedb updatedb [options] --output, --prunepaths sudo updatedb Rebuilds mlocate search index database across mounted local filesystems.
5. Process & Memory POSIX / All uptime uptime [options] -p (pretty format), -s (since timestamp) uptime -p Shows running duration, active users, and 1, 5, 15 minute system load averages.
4. Permissions & Users POSIX / All useradd useradd [options] username -m (create home), -s (shell), -g (group), -G (groups) useradd -m -s /bin/bash -G sudo devuser Creates a new user account, setting up home directories and initial group memberships.
4. Permissions & Users POSIX / All userdel userdel [options] username -r (remove home directory and mail spool) userdel -r olduser Deletes user account and optionally wipes home directory files with -r.
4. Permissions & Users POSIX / All usermod usermod [options] username -aG (append groups), -s (change shell), -L (lock) usermod -aG docker devuser Modifies user account parameters, secondary group affiliations, or login shells.
3. Text Processing POSIX / All vim / nano vim [file] / nano [file] +line, -R (read-only mode) sudo vim /etc/fstab Terminal text editors for modifying configuration files directly on headless servers.
4. Permissions & Users POSIX / All visudo visudo [options] -c (check syntax), -f [file] sudo visudo Safely opens `/etc/sudoers` in an editor with syntax validation checks to prevent lockouts.
5. Process & Memory POSIX / All vmstat vmstat [delay] [count] -s (event counters), -d (disk stats) vmstat 2 5 Reports virtual memory, swap-in/swap-out paging, process queues, I/O blocks, and CPU states.
4. Permissions & Users POSIX / All w / who / last w / who / last -n [lines], -f [file] w && last -n 10 Inspects active SSH sessions, logged-in terminal users, system load, and recent boot history.
2. File & Directory POSIX / All wc wc [options] [file] -l (lines), -w (words), -c (bytes) wc -l /etc/passwd Prints line, word, character, and byte count totals from input files or pipes.
4. Permissions & Users POSIX / All whoami / id whoami / id [user] -u (UID), -g (GID), -G (all groups) id -u root Prints effective username (whoami) or full numeric UID/GID and group assignments (id).
8. Services & Packages POSIX / All zip / unzip zip archive.zip file / unzip -r (recursive), -q (quiet), -d (destination) unzip archive.zip -d /tmp/extracted Compresses and extracts standard zip archive files.

Command Structure & Syntax Anatomy

$ command -flags --long-option [argument_source] [argument_target] | pipeline_next

1. Binary / Alias

The executable file located in $PATH (e.g. /usr/bin/rsync) or custom shell alias.

2. Short / Long Flags

Short options use single dash (-a -v or combined -av). Long options use double dashes (--archive).

3. Arguments

Operands passed to the program, such as file targets, string regex patterns, or network host IP addresses.

4. Redirection / Pipe

Chains stdout output into stdin of the next process using | or redirects streams to disk with >, >>.

Standard I/O Streams

stdin (0) stdout (1) stderr (2) 2>&1 /dev/null

File Mode Bits (Octal)

4 = Read (r) 2 = Write (w) 1 = Execute (x) 755 (rwxr-xr-x) 644 (rw-r--r--)

Process Signals

SIGHUP (1) SIGINT (2) SIGTERM (15) SIGKILL (9)

System Administrator Pro-Tips

Re-run Last Command as Root
Forgot to prepend sudo to a privileged system command? Use history substitution: sudo !! Executes the exact last command line with elevated privileges.
Fast Directory Navigation
Quickly toggle back to your previous working directory without typing full paths: cd - Swaps instantly between your current path and previous $OLDPWD.
Monitor Active Connections Live
Combine process and network socket monitors with continuous update interval tracking: watch -n 1 "ss -tulpn" Refreshes open listening port status output every 1.0 second.

System Administration Golden Rules

Safety First CLI Principles
  • Dry-Run Verification: Always run non-destructive flags first (e.g., rsync --dry-run or rm -i) before executing bulk operations.
  • Avoid Blind chmod 777: Never grant world-writable permissions to fix permission errors; use exact group grants (e.g., 755 or 644).
  • Use Absolute Paths in Scripts: Cron jobs and systemd service units do not inherit your user shell's $PATH environment. Always specify full binary paths (e.g. /usr/bin/python3).
Production Log Extraction Pipeline Example

High-efficiency pipeline combining process filtering, sorting, and field selection:

ps aux --sort=-%mem | grep -v "grep" | head -n 10 | awk '{print $2, $4, $11}'

htpasswd — Protect a Folder Behind a Username & Password

HTTP Basic Auth: the twenty-year-old tool that's still the right answer

When you need a staging site, an internal dashboard, or one directory gated behind a login — without building any user system at all — htpasswd plus your web server's Basic Auth is still the fastest correct answer. One file of user:hash lines, three lines of server config, done.

Know what it is and isn't: it's a gate, not a user-management system. Perfect for "keep the public out of this folder"; wrong for anything needing sessions, roles, or password resets.

Task Command Notes
Install the tool sudo apt install apache2-utils Debian/Ubuntu. On RHEL/Fedora it's sudo dnf install httpd-tools. The tool comes from Apache but the file it makes works with nginx too.
Create file + first user sudo htpasswd -c /etc/nginx/.htpasswd tyler Prompts for the password twice — double entry catches the silent typo that blind single entry invites. -c is for the first user only.
Add another user sudo htpasswd /etc/nginx/.htpasswd sarah No -c. Re-using -c here wipes the whole file and replaces it with just this user — the classic htpasswd mistake.
Change a password sudo htpasswd /etc/nginx/.htpasswd tyler Same as adding — an existing user is simply re-prompted and updated.
Verify a password htpasswd -v /etc/nginx/.htpasswd tyler Checks a typed password against the stored hash — the "did I set what I think I set?" test, worth running before you blame the server.
Delete a user sudo htpasswd -D /etc/nginx/.htpasswd sarah Removes just that one line from the file.
Stronger hashing sudo htpasswd -B -C 12 /etc/nginx/.htpasswd tyler -B uses bcrypt instead of the aging MD5-based default. See the nginx caveat below before using it there.

Wiring it up: nginx

Two directives inside the location (or server) block you want gated, then reload:

# /etc/nginx/sites-available/mysite (or your server block) location /private/ { auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; } # Gate an entire site instead: put the same two lines # directly in the server { } block. sudo nginx -t && sudo systemctl reload nginx

Wiring it up: Apache

Either in the vhost config (preferred) or a .htaccess file in the folder itself (needs AllowOverride AuthConfig enabled):

# In the vhost, or in /var/www/private/.htaccess <Directory "/var/www/private"> AuthType Basic AuthName "Restricted" AuthUserFile /etc/apache2/.htpasswd Require valid-user </Directory> sudo apachectl configtest && sudo systemctl reload apache2
The gotchas that actually bite
  • HTTPS is not optional. Basic Auth sends the username and password base64-encoded — that is encoding, not encryption — on every single request. Over plain HTTP, anyone on the path reads the credentials. Behind TLS it's fine.
  • Keep the file out of the webroot. /etc/nginx/.htpasswd, never /var/www/html/.htpasswd — a file inside the served directory can end up downloadable.
  • -c destroys. It means "create the file", and it recreates it happily over an existing one, silently dropping every other user. First user only.
  • Never use -b (password on the command line) interactively — it lands in your shell history in plaintext. It exists for scripts that pipe from a secrets store, not for typing.
  • bcrypt (-B) + nginx needs one check. nginx delegates hash checking to the system's crypt(), and older glibc builds don't speak bcrypt — logins then fail with a correct password. Modern distros (libxcrypt) are fine; if valid logins bounce after switching to -B, recreate the entry with the default hash and it will work everywhere.
  • No lockout, no rate limit. Basic Auth will happily let a bot guess forever. For anything internet-facing, pair it with fail2ban or nginx's limit_req on the gated location.
  • Permissions: the file should be readable by the web server user and nobody else — sudo chown root:www-data /etc/nginx/.htpasswd && sudo chmod 640 /etc/nginx/.htpasswd.
  • Browsers cache the credentials for the session. "Logging out" isn't really a thing — closing the browser or using a private window is how you test with a second account.

scp & rsync — Moving Files Over SSH

The two tools a web designer or developer actually uses to get a site from a laptop onto a client's server and back. scp is the quick copy; rsync is the deploy tool. Both ride on the SSH login you already have, so if ssh user@host works, everything below works too.

Five minutes once, then every transfer is one short command

Do this on your laptop for each client server. The payoff: no password prompts, no remembering ports or IPs, and rsync -avz dist/ client:/var/www/site/ instead of a 90-character command you paste from a note.

1

Make a key pair (once per laptop)

Ed25519 is the modern default: short, fast, and supported by every server you'll meet. Give it a passphrase; the agent in step 4 means you type it once per login, not per transfer.

ssh-keygen -t ed25519 -C "tyler@laptop" -f ~/.ssh/id_ed25519
2

Put the public key on the server

ssh-copy-id does the whole dance (creates ~/.ssh, appends to authorized_keys, fixes permissions). It asks for the account password one last time.

ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 deploy@203.0.113.10

No ssh-copy-id (older macOS)? Same result by hand:

cat ~/.ssh/id_ed25519.pub | ssh deploy@203.0.113.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
3

Give the server a short name in ~/.ssh/config

This is the step most people skip and then regret. Every tool that uses SSH — ssh, scp, rsync, git, VS Code Remote — reads this file, so the port, user, and key live in one place.

# ~/.ssh/config (chmod 600 ~/.ssh/config) Host acme HostName 203.0.113.10 User deploy Port 2222 IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes ServerAliveInterval 60 Host acme-staging HostName staging.acme-client.com User deploy IdentityFile ~/.ssh/id_ed25519 ProxyJump bastion.acme-client.com

From now on: ssh acme, scp file.css acme:/var/www/site/css/, rsync -avz dist/ acme:/var/www/site/.

4

Load the key into the agent (macOS remembers it)

With --apple-use-keychain the passphrase goes in the Keychain, and UseKeychain yes + AddKeysToAgent yes in the config make new terminals pick it up automatically.

ssh-add --apple-use-keychain ~/.ssh/id_ed25519 # macOS ssh-add ~/.ssh/id_ed25519 # Linux / WSL ssh-add -l # list what's loaded
5

Test it, then test a transfer

If the first command logs in without a password prompt, keys work. The second proves the path on the server is writable by that user — the number-one deploy failure.

ssh acme "whoami && hostname && ls -ld /var/www/site" echo ok > /tmp/probe.txt && scp /tmp/probe.txt acme:/var/www/site/ && ssh acme "rm /var/www/site/probe.txt"
~/.ssh/config directive Example Why a web dev wants it
HostHost acme acme.*The alias you'll type. Patterns work: Host *.acme-client.com applies to every subdomain.
HostNameHostName 203.0.113.10The real IP or DNS name. Change it here when a client migrates hosts and nothing else has to change.
UserUser deployStops the user@ prefix. Use a non-root deploy user where you can; see --rsync-path in the rsync tab for the sudo trick.
PortPort 2222Many hosts move SSH off 22. Set once here and scp's -P / rsync's -e "ssh -p" become unnecessary.
IdentityFileIdentityFile ~/.ssh/acme_ed25519Per-client keys. Lets you revoke one client's access without touching the others.
IdentitiesOnlyIdentitiesOnly yesOnly offer the key named above. Cures "Too many authentication failures" when the agent holds five keys and the server gives up after three.
ProxyJumpProxyJump bastion.acme.comTunnels through a jump/bastion host transparently. scp and rsync then work as if the target were directly reachable.
ServerAliveIntervalServerAliveInterval 60Keeps long transfers alive through NAT/firewall idle timeouts. Pair with ServerAliveCountMax 3.
AddKeysToAgent / UseKeychainAddKeysToAgent yes
UseKeychain yes
macOS: passphrase typed once, stored in Keychain, auto-loaded in every new terminal. Put these under Host *.
ControlMaster / ControlPathControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
Reuses one SSH connection for a burst of commands — a deploy script that runs rsync then three ssh calls feels instant instead of paying the handshake four times.
CompressionCompression yesCompress the SSH stream. Helps text/CSS/JS on slow links; pointless for already-compressed images and zips.

scp: cp with a hostname in the path

Best for one-off copies: a hotfixed style.css, a client's logo, a database dump you want back on your laptop. Anything with a host: prefix is remote; the argument order is always source → destination, same as cp. Since OpenSSH 9.0 it moves data over SFTP under the hood, but the commands haven't changed.

Know its limits before you lean on it: no resume (a dropped 2 GB copy starts from zero), no skip-unchanged (it re-sends every byte every time), overwrites silently, and no delete. The moment you're copying a folder for the second time, switch to the rsync tab.

$ scp [-r] [-P port] [-i key] [-p] [-C] [-l kbit/s] SOURCE… [user@]host:DESTINATION

Upload

Local path first, host:path last. A relative remote path is relative to that user's home.

Download

host:path first, local path last. . means "right here".

Capital -P

Port is -P in scp (lowercase -p means preserve times). ssh uses lowercase. Everyone gets this wrong once.

Quote the remote side

Spaces and globs on the remote path need quoting twice: acme:"'/var/www/My Site/'".

Showing 0 recipes
Task Command Notes
Upload one filescp style.css acme:/var/www/site/css/Trailing slash on the destination folder = "put it inside". Without it, if the folder doesn't exist you've just created a file named css.
Upload and renamescp index.local.html acme:/var/www/site/index.htmlDestination is a file path, so it lands under that name.
Download one filescp acme:/var/www/site/wp-config.php ./backup/Remote first, local last. The local folder must already exist.
Upload a whole folderscp -r ./dist acme:/var/www/site/Creates /var/www/site/dist/. To copy the contents into site/ use scp -r ./dist/. acme:/var/www/site/ — or just use rsync.
Download a folderscp -r acme:/var/www/site/wp-content/themes/acme ./themes/Pulls the theme folder down as ./themes/acme.
Non-standard portscp -P 2222 logo.svg deploy@203.0.113.10:/var/www/site/img/Capital P. Unnecessary once the port is in ~/.ssh/config.
Specific keyscp -i ~/.ssh/acme_ed25519 file.zip deploy@203.0.113.10:~/~/ or a bare host: both mean the remote home directory.
Several files at oncescp index.html style.css app.js acme:/var/www/site/Any number of sources; the last argument is always the destination.
Local globscp img/*.webp acme:/var/www/site/img/Your shell expands the * before scp sees it, so this just works locally.
Remote globscp 'acme:/var/log/nginx/*.log' ./logs/Quote it so your shell doesn't try to expand it (zsh otherwise errors with "no matches found"); the remote shell expands it instead.
Spaces in a pathscp "Brand Guide.pdf" acme:"'/var/www/site/Client Files/'"Local side: normal quotes. Remote side: quotes inside quotes, because the path is parsed twice. Or rename the folder and never think about it again.
Preserve timestamps & modesscp -rp ./assets acme:/var/www/site/Lowercase -p. Keeps mtimes so "what changed?" comparisons on the server still mean something.
Compress in flightscp -C site-dump.sql acme:~/Great for SQL, HTML, CSS, JS. Useless for images/zips/videos (already compressed) — it just burns CPU.
Cap bandwidthscp -l 8000 video.mp4 acme:/var/www/site/media/Kilobits per second (8000 ≈ 1 MB/s). Keeps a big upload from killing your Zoom call or the client's live site.
Server-to-serverscp -3 old-host:/var/www/site/uploads.tar.gz acme:/var/www/site/-3 routes through your laptop so the two servers don't need keys for each other. Slower, but zero setup. For a real migration, run rsync on one of the servers instead.
Through a bastionscp -J bastion.acme.com file.zip deploy@10.0.1.20:~/Same as ProxyJump in the config. Private-network app servers behind a jump box.
Quiet (for scripts)scp -q build.zip acme:~/ && echo "uploaded"No progress meter. Exit code is still non-zero on failure, so && chains stay safe.
Debug a failing copyscp -v file.txt acme:~/ 2>&1 | grep -i -E "auth|denied|offer"Shows which keys were offered and why they were rejected. Add -vvv for the full firehose.
Use a different config filescp -F ~/clients/acme/ssh_config file.txt acme:~/Handy when a client's SSH details live in their project repo rather than your global config.
Legacy scp protocolscp -O file.txt acme:~/Forces the old protocol if a very old server has no SFTP subsystem ("subsystem request failed").
Copy a folder fast without scptar czf - dist | ssh acme "tar xzf - -C /var/www/site"Thousands of tiny files (node_modules, image sprites) copy far faster as one stream than as thousands of scp round-trips.
Need resume?rsync -avP big-file.zip acme:/var/www/site/scp can't. rsync's -P (--partial --progress) picks up where the last attempt died.
scp things that bite
  • It overwrites without asking. There is no -i/interactive mode. Uploading wp-config.php over the live one is instant and unrecoverable. Download the server's copy first if in doubt.
  • -r follows symlinks and copies the target. A theme folder with a symlink to ../shared/ arrives as a full copy of shared/. rsync's -a keeps it as a link.
  • Destination folder missing = wrong result, not an error. scp file.css acme:/var/www/site/css with no css/ folder creates a file named css. ssh acme "mkdir -p /var/www/site/css" first.
  • Ownership is whoever you logged in as. Files uploaded as deploy are owned by deploy; PHP running as www-data may not be able to write next to them (uploads, caches). Fix with chown after, or rsync's --chown.
  • Port is -P, preserve is -p. Lowercase when you meant uppercase silently connects to port 22 and hangs.
  • Every run sends everything. A 300 MB site re-uploaded for a one-line CSS fix. That's the whole reason rsync exists.

rsync: make there look like here, sending only what changed

rsync compares both sides first (size + modification time by default) and transfers only the files that differ — and for large files, only the changed blocks. Run it twice and the second run does nothing. That makes it the right tool for deploys, backups, pulling a client's uploads folder, and every "sync this folder" job. It also resumes, can delete stale files, and can fix ownership on the way in.

Two things to internalize and you're 90% there: -avz is the standard opening, and the trailing slash on the source decides whether you get the folder or its contents.

$ rsync -avz --delete --dry-run [--exclude PATTERN]… SOURCE/ [user@]host:DESTINATION/

-a archive

= -rlptgoD: recursive, keep symlinks, permissions, times, group, owner, devices. "Copy it faithfully." Always.

-v verbose

Print each file transferred. Add -h for human sizes, --stats for a summary.

-z compress

Compress in transit. Big win for HTML/CSS/JS/SQL over the internet; skip it on a LAN or for image-heavy folders.

--delete

Remove files on the destination that no longer exist in the source. What turns "copy" into "mirror". Always dry-run first.

The trailing-slash rule

dist/ → site/ = contents of dist land in site/ dist → site/ = site/dist/ is created destination slash never matters

How "changed" is decided

default: size + mtime -c checksum (slow, exact) --size-only -u skip newer on dest --ignore-existing

Filter order

first matching rule wins --include before --exclude /pattern = anchored to source root pattern/ = directories only
Showing 0 entries
Flag / Task Example What it does & when you want it
-a, --archiversync -a ./dist/ acme:/var/www/site/The baseline. Recursive + preserves symlinks, perms, times, owner, group. Everything below assumes it.
-n, --dry-runrsync -avzn --delete ./dist/ acme:/var/www/site/Show what would happen, touch nothing. Non-negotiable before any --delete, and the fastest way to check your trailing slash.
-v / -h / --statsrsync -avh --stats ./dist/ acme:/var/www/site/List files, human-readable sizes, totals at the end ("Number of regular files transferred: 3").
-z, --compressrsync -avz ./src/ acme:/var/www/site/Compress in transit. Add --skip-compress=jpg/png/webp/mp4/zip/gz if a folder mixes code and media (recent versions do this by default).
-P (= --partial --progress)rsync -avP big-export.zip acme:/var/www/site/dl/Per-file progress bar and keep partial files so a dropped connection resumes instead of restarting. Use for anything over ~50 MB.
--info=progress2rsync -a --info=progress2 ./media/ acme:/var/www/site/media/One overall progress line for the whole job instead of one per file. Nicer for folders with thousands of images.
--deletersync -avz --delete ./dist/ acme:/var/www/site/Mirror: remove files on the server that aren't in dist/. Renamed about.html → about-us.html locally? Old one is gone on the server too. Never use it on a folder the client writes to (uploads) without excludes.
--delete-excludedrsync -av --delete --delete-excluded --exclude 'cache/' ./site/ acme:/var/www/site/Also delete files on the destination that match an exclude. Dangerous with --exclude uploads/ — it would wipe uploads. Know the difference.
--excludersync -avz --exclude '.git' --exclude 'node_modules' --exclude '.env' --exclude '.DS_Store' ./ acme:/var/www/site/Repeatable. A bare name matches at any depth; /node_modules only at the source root; *.map globs; cache/ only directories. Quote patterns so the shell doesn't expand them.
--exclude-fromrsync -avz --delete --exclude-from '.rsyncignore' ./ acme:/var/www/site/One pattern per line, # comments allowed. Commit .rsyncignore to the project so every deploy uses the same list. (See Deploy Workflows for a starter file.)
--include + --excludersync -avm --include '*/' --include '*.css' --exclude '*' ./src/ acme:/var/www/site/"Only CSS files, keep the folder structure." */ lets rsync descend into directories, -m prunes empty ones. First match wins, so includes go first.
-e "ssh …"rsync -avz -e "ssh -p 2222 -i ~/.ssh/acme_ed25519" ./dist/ deploy@203.0.113.10:/var/www/site/Custom port / key / jump host when you haven't set up ~/.ssh/config. Note ssh's port is lowercase -p here.
--chownrsync -avz --chown=www-data:www-data ./dist/ acme:/var/www/site/Set owner:group on arrival so the web server can read/write. Needs root (or the sudo trick below) on the receiving side; requires rsync ≥ 3.1.
--chmodrsync -avz --chmod=D755,F644 ./dist/ acme:/var/www/site/Directories 755, files 644 regardless of what they were on your Mac. Fixes the "everything is 777 / 600" mess from a bad export.
--rsync-path="sudo rsync"rsync -avz --rsync-path="sudo rsync" --chown=www-data:www-data ./dist/ acme:/var/www/site/Log in as a normal user but run the receiving rsync as root — writes into root-owned /var/www and applies --chown. Needs passwordless sudo for rsync on the server (deploy ALL=(root) NOPASSWD: /usr/bin/rsync).
--no-o --no-g --no-prsync -rlvz --no-o --no-g --no-p ./dist/ shared-host:public_html/Shared hosting where you can't change ownership: drop owner/group/perms from -a (equivalently -rltvz) and the "failed to set permissions/times" noise disappears.
-u, --updatersync -avzu acme:/var/www/site/wp-content/uploads/ ./uploads/Skip files that are newer on the destination. Safe two-way-ish pulls of a folder the client edits.
--ignore-existingrsync -avz --ignore-existing ./uploads/ acme:/var/www/site/wp-content/uploads/Add new files only, never touch what's already there. The polite way to top up a media library.
--existingrsync -avz --existing ./dist/ acme:/var/www/site/Opposite: update files that already exist, create nothing new. Push fixes to a site without adding your local junk.
-c, --checksumrsync -avzc ./dist/ acme:/var/www/site/Compare by content hash, not size+time. Slower (reads every file on both ends) but right after a build tool that rewrites timestamps on every run.
--size-onlyrsync -avz --size-only ./media/ acme:/var/www/site/media/Ignore timestamps entirely. Useful after a copy that lost mtimes (FAT drives, some FTP clients) so you don't re-send 4 GB of unchanged images.
--bwlimitrsync -avP --bwlimit=2m ./media/ acme:/var/www/site/media/Kilobytes/s (or 2m). Keep a bulk media push from starving the client's live traffic — or your own connection.
-i, --itemize-changesrsync -ain --delete ./dist/ acme:/var/www/site/A change-code per file (>f.st.... = file, size & time differ; *deleting). The most precise dry-run readout.
--list-onlyrsync --list-only acme:/var/www/site/A remote ls -l through rsync. Handy to confirm a path before you point --delete at it.
-L / -l / --safe-linksrsync -avL ./dist/ acme:/var/www/site/-l (in -a) copies symlinks as symlinks; -L replaces them with the real files (what you want when the server won't have the link target); --safe-links drops links pointing outside the tree.
--files-fromgit diff --name-only main | rsync -avz --files-from=- ./ acme:/var/www/site/Send exactly the listed paths (relative to the source root). Feeding it git diff gives a "deploy only what I changed" one-liner.
--backup --backup-dirrsync -avz --delete --backup --backup-dir=/var/backups/site/$(date +%F) ./dist/ acme:/var/www/site/Anything overwritten or deleted on the server is moved into a dated folder first. Cheap undo for a bad deploy. (Backup dir must be an absolute path on the destination.)
--link-destrsync -avz --delete --link-dest=../latest acme:/var/www/site/ ~/Backups/acme/$(date +%F)/Time Machine-style snapshots: unchanged files are hard-linked to the previous snapshot, so 30 daily backups cost about one copy of disk. Repoint a latest symlink after each run.
--remove-source-filesrsync -av --remove-source-files acme:/var/www/site/exports/ ./exports/A "move" — delete each source file after it's safely copied. Folders are left behind (empty). Pull-and-clear job for a client's export drop.
--max-size / --min-sizersync -avz --max-size=20m acme:/var/www/site/wp-content/uploads/ ./uploads/Pull the uploads folder for local dev but skip the 500 MB videos.
-x, --one-file-systemrsync -avx / /mnt/backup/root/Don't cross mount points. Full-server backups that would otherwise happily recurse into /proc, /mnt, and the backup drive itself.
--inplace / --append-verifyrsync -avP --append-verify huge-backup.tar acme:/backups/For one giant file that grows (a log, a tar being written): append the new tail and verify. --inplace writes directly instead of via a temp file (saves disk, breaks atomicity — don't use it for live web assets).
--timeout / --contimeoutrsync -avz --timeout=60 --contimeout=15 ./dist/ acme:/var/www/site/Fail fast in a cron job rather than hanging forever on a dead connection.
--log-filersync -avz --log-file=~/deploys/acme-$(date +%F).log ./dist/ acme:/var/www/site/A written record of what each deploy changed. Worth its weight the day a client asks "what changed on Tuesday?"
-H, --hard-linksrsync -avH /srv/snapshots/ /mnt/backup/snapshots/Preserve hard links (needed when copying a --link-dest backup set elsewhere, or they balloon to full size).
Local → localrsync -avh --progress ~/Sites/acme/ /Volumes/Archive/acme/No host at all: rsync is also the best cp -r — resumable, skips unchanged, shows progress. Copying to an external drive, this is what you want.
Remote → remotessh old-host "rsync -avz /var/www/site/ deploy@new-host:/var/www/site/"rsync itself refuses two remote paths. Run it on one server (needs a key from old → new). Site migrations go server-to-server, not through your laptop.
macOS: get a modern rsyncbrew install rsync && rsync --version | head -1Older macOS shipped rsync 2.6.9 (2006): no --chown, no --info=progress2, protocol warnings against modern Linux. Newer macOS bundles openrsync, which also lacks some flags. Homebrew fixes both.

The jobs you actually do for clients, as copy-paste commands

Every recipe assumes the Setup tab is done (a Host acme alias). Replace acme and the paths. Each one has been run in anger; the comments say what to check before you hit Enter.

1

Deploy a static / built site (the standard deploy)

Build locally, mirror the output folder, never touch anything that isn't yours. Dry-run first, then the real thing.

npm run build rsync -avzn --delete --exclude '.DS_Store' ./dist/ acme:/var/www/acme.com/ # dry run — read the list rsync -avz --delete --exclude '.DS_Store' ./dist/ acme:/var/www/acme.com/ # for real
2

Push a whole project but keep secrets and junk at home

Commit a .rsyncignore so every deploy — yours, a teammate's, the CI job — uses the same list.

# .rsyncignore .git/ .github/ node_modules/ .env .env.* .DS_Store *.map /src/ # anchored: only the top-level src, not theme/src /README.md /package*.json
rsync -avz --delete --exclude-from='.rsyncignore' ./ acme:/var/www/acme.com/
3

WordPress: push the theme, never the uploads

Only the theme folder is yours. --delete is safe here because it's scoped to that folder; the uploads/plugins the client manages are never in the path.

rsync -avz --delete --exclude 'node_modules' --exclude 'src' \ ./wp-content/themes/acme/ acme:/var/www/acme.com/wp-content/themes/acme/ # then bust the cache if a plugin caches ssh acme "cd /var/www/acme.com && wp cache flush"
4

Pull a client site down to work on it locally

Code first, then the media (which is big — cap file size), then the database. No --delete on a pull: you don't want rsync deleting your local work.

rsync -avz --exclude 'wp-content/uploads' --exclude 'wp-content/cache' \ acme:/var/www/acme.com/ ~/Sites/acme/ rsync -avz --max-size=20m --info=progress2 \ acme:/var/www/acme.com/wp-content/uploads/ ~/Sites/acme/wp-content/uploads/ ssh acme "mysqldump --single-transaction acme_db | gzip" > ~/Sites/acme/db-$(date +%F).sql.gz
5

Deploy as a normal user, land files owned by the web server

You log in as deploy; files should belong to www-data with sane modes. One-time on the server: echo 'deploy ALL=(root) NOPASSWD: /usr/bin/rsync' | sudo tee /etc/sudoers.d/deploy-rsync.

rsync -avz --delete --rsync-path="sudo rsync" \ --chown=www-data:www-data --chmod=D755,F644 \ ./dist/ acme:/var/www/acme.com/
6

One-file hotfix

Fix a typo in production without a full deploy. scp is fine for this; the second line is the same thing with rsync (resumable, skips if identical).

scp css/style.css acme:/var/www/acme.com/css/ rsync -avz css/style.css acme:/var/www/acme.com/css/
7

Deploy only what changed in git

List the files that differ from main (or between two tags) and send exactly those. Deleted files still need a manual ssh acme rm — this can't delete.

git diff --name-only --diff-filter=ACMR v1.4.0 v1.5.0 | rsync -avz --files-from=- ./ acme:/var/www/acme.com/
8

A deploy.sh you can hand to the next developer

Build, dry-run with a confirmation, deploy with a dated backup of anything overwritten, reload the server. set -e stops at the first failure so a broken build never half-deploys.

#!/usr/bin/env bash set -euo pipefail HOST=acme DEST=/var/www/acme.com/ STAMP=$(date +%F_%H%M) npm run build echo "--- dry run ---" rsync -avzn --delete --exclude-from=.rsyncignore ./dist/ "$HOST:$DEST" read -r -p "Deploy for real? [y/N] " ok; [[ "$ok" == "y" ]] || exit 0 rsync -avz --delete --exclude-from=.rsyncignore \ --backup --backup-dir="/var/backups/acme/$STAMP" \ --log-file="deploys/$STAMP.log" \ ./dist/ "$HOST:$DEST" ssh "$HOST" "sudo systemctl reload nginx" echo "deployed $STAMP"
9

Nightly server backup to your laptop / NAS, with snapshots

Each night is a full-looking folder that only stores the changed files. Restore = copy a folder back. Run from cron or a launchd job on the machine that stores the backups.

#!/usr/bin/env bash set -euo pipefail BASE=~/Backups/acme TODAY=$BASE/$(date +%F) rsync -avz --delete --link-dest="$BASE/latest" \ --exclude 'wp-content/cache' \ acme:/var/www/acme.com/ "$TODAY/" ssh acme "mysqldump --single-transaction acme_db | gzip" > "$TODAY/db.sql.gz" ln -sfn "$TODAY" "$BASE/latest" find "$BASE" -maxdepth 1 -type d -name '20*' -mtime +30 -exec rm -rf {} + # keep 30 days
# crontab -e (2:30 every night) 30 2 * * * /Users/tyler/bin/backup-acme.sh >> /Users/tyler/Backups/acme/cron.log 2>&1
10

Ghost theme deploy (this site's own routine)

Mirror the theme folder from the repo into Ghost's content/themes/, then restart so Handlebars templates re-load. Excludes keep dev-only files out.

rsync -avz --delete --exclude '.DS_Store' --exclude 'node_modules' --exclude '*.md' \ themes/SanPedro/ ty1er:/www/wwwroot/build.ty1er.com/content/themes/SanPedro/ ssh ty1er "systemctl restart ghost_build-ty1er-com"
11

Migrate a site between two servers

Run rsync on the old server pointing at the new one (after putting a key from old → new in place). Then a final --delete pass at cutover to catch the last hour of uploads.

# on OLD server rsync -avz --info=progress2 /var/www/acme.com/ deploy@new.acme.com:/var/www/acme.com/ mysqldump --single-transaction acme_db | gzip | ssh deploy@new.acme.com "gunzip | mysql acme_db" # at cutover, once DNS is about to flip rsync -avz --delete /var/www/acme.com/ deploy@new.acme.com:/var/www/acme.com/
The five ways people wreck a client site with rsync
  • Missing trailing slash + --delete. rsync -av --delete dist acme:/var/www/site/ creates site/dist/ and deletes everything else in site/ — because from rsync's point of view, only dist should exist there. Dry-run shows this instantly as a wall of deleting … lines.
  • Pointing --delete at a folder the client writes to. Uploads, exports, user avatars. Your local copy is a week old; rsync faithfully deletes the week. Scope the deploy to the theme/build folder, or --exclude 'uploads/' (and never add --delete-excluded).
  • Deploying .env / wp-config.php from local. Overwrites production database credentials with your local ones. Site down. Exclude them, always, in a committed .rsyncignore.
  • Source and destination swapped. A pull written as a push. rsync happily makes the server look like your stale laptop. Say it out loud: source, then destination. Dry-run.
  • Running as root with -a onto a shared host. Ownership arrives as your local UID (501) and PHP can no longer write. Use --chown or --no-o --no-g.
Error / symptom What it means Fix
Permission denied (publickey)The server only accepts keys and none of yours matched (wrong key, wrong user, key not in authorized_keys, or bad permissions on the server's ~/.ssh).ssh -v acme and read which key was offered. Check User. On the server: chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys.
Too many authentication failuresYour agent offered 5+ keys and the server cut you off before reaching the right one.IdentitiesOnly yes + IdentityFile in the config for that host.
REMOTE HOST IDENTIFICATION HAS CHANGED!The server's host key differs from what's saved — usually because the client re-provisioned the server (or moved the IP to a new box). Rarely, an actual attack.Confirm with the client the server was rebuilt, then ssh-keygen -R 203.0.113.10 (and -R hostname) and reconnect.
Connection timed out / refusedWrong port, firewall, or SSH moved off 22.Ask for the port. nc -zv host 2222 to test. Remember scp is -P, rsync is -e "ssh -p 2222", config is Port.
bash: rsync: command not found (remote)rsync isn't installed on the server (minimal Docker images, some fresh VPSes).ssh acme "sudo apt install -y rsync" (or dnf). Until then, the tar | ssh trick from the scp tab.
rsync: --chown: unknown option / protocol version mismatchOne side is running an ancient rsync (macOS 2.6.9, or openrsync).brew install rsync on the Mac; make sure /opt/homebrew/bin is first in PATH. rsync --version on both ends.
rsync error: some files/attrs were not transferred (code 23)Partial success: most files went, a few failed — typically permission denied on a file, or "failed to set times/permissions" on a host you don't own.Scroll up for the actual per-file error. Shared host: add --no-o --no-g --no-p. Root-owned folder: --rsync-path="sudo rsync".
rsync: failed to set times on … Operation not permittedYou don't own the destination file/dir, so its mtime can't be changed. Data still copied.--no-times / -O (omit dir times), or fix ownership on the server once.
skipping non-regular file / symlink has no referentA symlink was copied as a link but its target doesn't exist on the server.Use -L to copy the real files instead of links, or exclude the link.
Broken pipe / connection unexpectedly closed (large transfers)Idle NAT/firewall timeout, flaky Wi-Fi, or the server's SSH was restarted mid-copy.ServerAliveInterval 60 in the config; re-run the same rsync with -P and it resumes.
zsh: no matches found: acme:/var/log/*.logzsh tried to expand the remote glob locally and found nothing.Quote the remote path: 'acme:/var/log/*.log'.
My excludes aren't excludingPattern anchoring. --exclude 'src' matches any src at any depth; --exclude '/src' only the top-level one; --exclude 'src/' only directories. Patterns are relative to the source root, not your cwd.Test with -n -i and look for the files. Order matters: the first matching include/exclude wins.
Everything re-uploads every deployYour build tool rewrites every file's mtime, so size+time says "changed" even when content didn't. Or a FAT/exFAT drive with 2-second timestamps.-c (checksum) for exactness, --modify-window=2 for FAT, or --size-only for media.
Site shows old content after deployThe files are there — a cache in front of them isn't (nginx/Varnish/Cloudflare/WordPress plugin/browser).ssh acme "ls -l /var/www/site/css/" to prove the file landed, then purge the cache. Ghost, WordPress, and Cloudflare all have a purge command/button.
subsystem request failed on channel 0 (scp)New scp speaks SFTP; this old server has no SFTP subsystem enabled.scp -O for the legacy protocol, or just use rsync.
Windows: where do I run these?PowerShell has ssh/scp built in (OpenSSH) but no rsync.Use WSL (Ubuntu) for rsync; your Windows files are at /mnt/c/Users/…. Or Git Bash + a rsync package. Keys live in ~/.ssh inside WSL.
scp vs rsync vs sftp — which one?
scp: one file, right now, no fuss. rsync: any folder, anything you'll do twice, anything with --delete, anything big. sftp (or a GUI like Cyberduck/FileZilla in SFTP mode): browsing around when you don't know the path yet. If you're typing -r after scp, you probably want rsync.
The two-command habit
Run every deploy first with -n added, read the list, then press ↑ and delete the n. It takes four seconds and it has saved more client sites than any backup ever has. rsync -avzn --delete ./dist/ acme:/var/www/site/
Undo a bad deploy
If you used --backup --backup-dir, restore by rsyncing that dated folder back over the site. If you didn't, this is why the nightly --link-dest snapshots exist. rsync -av acme:/var/backups/acme/2026-09-05_1420/ acme:/var/www/acme.com/ Two remote paths won't work from your laptop — run that one on the server over ssh.