rsync over SSH

Chapter 8 — rsync over SSH

Everything from Chapter 7 applies here — the flags, the trailing-slash rule, the dry-run habit. The only difference is the destination format. Instead of a local path, you provide user@host:/path and rsync tunnels the transfer through SSH automatically. No extra configuration needed if you can already SSH into the machine.

Remote Path Syntax

rsync -avz ~/projects/website/ philip@server.example.com:/var/www/html/ flags local source user hostname or IP remote destination
Config aliases work here too. If server is defined in ~/.ssh/config, you can write rsync -av src/ server:/var/www/html/ and rsync picks up the port, user, and key file from your config automatically.

Push vs Pull

Push — local → remote
rsync -avz src/ server:/dest/
Run on your local machine. Sends files to the server. Used for deploys — pushing a built website or updated config to a server.
Pull — remote → local
rsync -avz server:/src/ dest/
Run on your local machine. Fetches files from the server. Used for backups — pulling logs, databases, or a site snapshot to your machine.

Basic Push and Pull

philip@laptop — push to server
# Push local website build to the server's web root philip@laptop:~$ rsync -avz ~/projects/website/ server:/var/www/html/ sending incremental file list index.html css/style.css js/app.js sent 52,480 bytes received 131 bytes 21,044.40 bytes/sec total size is 247,820 speedup is 4.71 # Pull server logs back to your machine for analysis philip@laptop:~$ rsync -avz server:/var/log/nginx/ ~/logs/nginx/ sending incremental file list access.log error.log sent 1,823 bytes received 284,910 bytes 82,209.43 bytes/sec

Non-Standard Port — the --rsh Flag

When your SSH server runs on a port other than 22, you can't just add -P — that means something else to rsync. Instead, use -e (short for --rsh, remote shell) to pass the port to SSH directly:

# -e tells rsync which shell command to use for the connection rsync -avz -e "ssh -p 2222" src/ server:/dest/ # With a specific key file too rsync -avz -e "ssh -p 2222 -i ~/.ssh/id_work" src/ server:/dest/ # Or just use a config alias — zero extra flags needed rsync -avz src/ work:/dest/ ← port and key come from ~/.ssh/config
The config alias approach is almost always cleaner. Define the port, user, and key in ~/.ssh/config once, and every tool that uses SSH — including rsync, sftp, and scp — inherits those settings from the alias.

Practical Scenarios

Deploy a website (with delete)
Mirror local build to server — removes files deleted from source
rsync -avz --delete \ --exclude='.git' \ --exclude='node_modules/' \ ~/projects/site/dist/ \ server:/var/www/html/
Pull a full server backup
Download home directory from server; skip large cache dirs
rsync -avz \ --exclude='.cache/' \ --exclude='tmp/' \ server:/home/philip/ \ ~/backups/server-home/
Sync a Raspberry Pi project
Push code to the Pi, skip virtual environments
rsync -avz \ --exclude='venv/' \ --exclude='__pycache__/' \ --exclude='*.pyc' \ ~/projects/myapp/ \ pi:~/myapp/
Backup MySQL dumps
Pull latest database dumps from server to local machine
rsync -avz \ server:/var/backups/mysql/ \ ~/backups/mysql/
Preview before deploying
Dry-run to confirm exactly what will change — always a good habit
rsync -avzn --delete \ ~/projects/site/dist/ \ server:/var/www/html/
Sync between two remote servers
Run from your local machine; route through it as relay
rsync -avz \ server1:/var/www/html/ \ server2:/var/www/html/

Bandwidth Limiting

On a shared or metered connection, rsync can be told to cap its transfer rate so it doesn't swamp the link — useful for background backups running during working hours:

philip@laptop — capping transfer speed
# --bwlimit takes kilobytes per second (KB/s) philip@laptop:~$ rsync -avz --bwlimit=5000 server:/home/philip/ ~/backups/server/ sending incremental file list large-archive.tar.gz 512,000,000 100% 4.88MB/s 0:01:44 ← capped near 5 MB/s # Common values # --bwlimit=1000 ≈ 1 MB/s (cautious background transfer) # --bwlimit=5000 ≈ 5 MB/s (moderate) # --bwlimit=50000 ≈ 50 MB/s (fast LAN, still capped)

Resuming Interrupted Transfers

If a large transfer is interrupted (connection drop, power cut), rsync can pick up where it left off rather than starting from scratch — but only for files that were partially transferred:

# --partial: keep partially transferred files at the destination # so the next run can resume from where it stopped rsync -avz --partial server:/backups/large-archive.tar.gz ~/backups/ # --partial-dir: store partial files in a hidden staging directory # cleaner than leaving half-transferred files in the destination rsync -avz --partial-dir=.rsync-partial server:/backups/ ~/backups/ # -P is shorthand for --partial --progress together rsync -avzP server:/backups/large-archive.tar.gz ~/backups/
-P is the flag to remember for large remote transfers — it gives you a progress bar and enables resume on interruption in a single letter.

Preserving Permissions Across Users

When pushing files to a server where Apache or Nginx serves them, you may need to set permissions correctly rather than copying your local user's permissions. Two approaches:

# --no-perms --no-owner --no-group: don't copy metadata — let the # server's umask and ownership apply (simplest for web deploys) rsync -rltvz --no-perms --no-owner --no-group \ ~/projects/site/dist/ server:/var/www/html/ # --chmod: force specific permissions regardless of source rsync -avz --chmod=D755,F644 \ ~/projects/site/dist/ server:/var/www/html/ # D755 = directories get 755, F644 = files get 644 # --chown: force ownership (requires root or sudo on the server) rsync -avz --chown=www-data:www-data \ ~/projects/site/dist/ server:/var/www/html/

A Complete Deploy Script

Putting it all together — a simple shell script that builds and deploys a website, with a dry-run mode and a production mode:

#!/bin/bash # deploy.sh — build and rsync to server # Usage: ./deploy.sh (dry run) # ./deploy.sh --live (real deploy) SERVER="server" # ssh config alias REMOTE_DIR="/var/www/html/" LOCAL_DIR="$HOME/projects/website/dist/" RSYNC_OPTS="-avz --delete --exclude='.git' --exclude='node_modules/' --exclude='*.map'" # Build first echo "Building..." npm run build || { echo "Build failed"; exit 1; } # Deploy if [[ "$1" == "--live" ]]; then echo "Deploying to $SERVER..." rsync $RSYNC_OPTS "$LOCAL_DIR" "$SERVER:$REMOTE_DIR" echo "Done." else echo "Dry run (pass --live to deploy):" rsync $RSYNC_OPTS --dry-run "$LOCAL_DIR" "$SERVER:$REMOTE_DIR" fi
philip@laptop — using the deploy script
# Preview what would be deployed philip@laptop:~$ ./deploy.sh Building... ✓ Built in 3.4s Dry run (pass --live to deploy): index.html css/style.css (DRY RUN) # Happy with the preview — deploy for real philip@laptop:~$ ./deploy.sh --live Building... ✓ Built in 3.4s Deploying to server... index.html css/style.css Done.

Common Errors and Fixes

troubleshooting rsync over SSH
# Error: rsync: failed to connect — wrong host or SSH not listening rsync: [Receiver] failed to connect to server.example.com: Connection refused → check: ssh server (does plain SSH work?) # Error: permission denied on the remote path rsync: [sender] change_dir "/var/www/html" failed: Permission denied (13) → fix: ensure your user owns or has write access to the remote path ssh server "sudo chown philip:philip /var/www/html" # Error: rsync not found on the remote machine bash: rsync: command not found rsync: connection unexpectedly closed → fix: install rsync on the server ssh server "sudo apt install rsync" # Warning: skipping non-regular file — symlink in source skipping non-regular file "current" → add -l to preserve symlinks, or --copy-links to follow them # Error: port 22 blocked by firewall at work → use -e to connect via port 443 if sshd listens there rsync -avz -e "ssh -p 443" src/ server:/dest/

Quick Reference

Command / flagWhat it does
rsync -avz src/ server:/dest/Push local to remote (archive + verbose + compress)
rsync -avz server:/src/ dest/Pull remote to local
rsync -avzn src/ server:/dest/Dry run — preview without transferring
rsync -avz --delete src/ server:/dest/Mirror — delete remote files that no longer exist locally
-e "ssh -p 2222"Use non-standard SSH port
-e "ssh -p 22 -i ~/.ssh/id_work"Specify SSH key inline
--bwlimit=5000Cap transfer at 5 MB/s (value in KB/s)
-PProgress bar + resume partial transfers
--partial-dir=.rsync-partialStore partial files in a staging directory
--chmod=D755,F644Force directory/file permissions at destination
--no-perms --no-owner --no-groupDon't copy metadata — let server's umask apply
--exclude='node_modules/'Skip a directory by name
--exclude-from=.rsyncignoreLoad exclude patterns from a file
Next — Chapter 9: Automating rsync. Running rsync by hand is fine for one-off transfers. For backups and deploys you want them to happen automatically — on a schedule, without prompts, and with some kind of rotation so you don't fill your disk. Chapter 9 covers cron jobs, unattended key-based auth, backup rotation strategies, and building a reliable home-server backup routine.