Web Development Tutorials

Server Administration

Deploy a Site With rsync

Deploy a site with rsync and every update becomes one repeatable command that copies only what changed. The first run pushes the whole directory; after that, rsync compares files and sends the difference, so a deploy takes seconds. First, this tutorial deploys a small site over SSH with -avz. Next, it protects secrets and user uploads with --exclude. Finally, it adds --delete — and shows why that flag always deserves a dry run first.

Requirements to deploy a site with rsync:

  • rsync on both ends (tested with rsync 3.2.7 on Ubuntu 24.04.4 LTS). Linux and macOS ship it; on Windows it comes with Git Bash or WSL.
  • SSH access to the server. Key-based login, as set up in connecting to a remote server using SSH keys, makes repeated deploys painless.
  • A site directory to publish. The examples use a small static site and the target /tmp/a67/site, so nothing important is at risk.

How To Deploy a Site With rsync.

The objective is a one-line deploy you can re-run after every edit — the same routine that maintains the small server we tuned in the first part of this series. The local site looks like this:

site/index.html
site/css/style.css
site/js/app.js
site/config.php
site/uploads/photo.jpg

config.php holds local settings and must never reach the server; uploads/ belongs to the live site, not to the copy on your machine. Both are exclude cases, and rsync handles them cleanly.

Step 1.

First, rehearse. The -n flag makes rsync print what it would transfer and touch nothing.

rsync -avzn --exclude 'uploads/' --exclude 'config.php' \
  -e "ssh -i ~/.ssh/mykey.pem" \
  site/ ubuntu@203.0.113.10:/tmp/a67/site/
sending incremental file list
created directory /tmp/a67/site
./
index.html
css/
css/style.css
js/
js/app.js

sent 192 bytes  received 72 bytes  58.67 bytes/sec
total size is 343  speedup is 1.30 (DRY RUN)

The flags: -a (archive) keeps permissions and timestamps, -v lists the files, -z compresses in transit, and -e names the SSH command. Notice that config.php and uploads/ are absent from the list — the excludes work.

Step 2.

Next, run it for real by dropping the -n.

rsync -avz --exclude 'uploads/' --exclude 'config.php' \
  -e "ssh -i ~/.ssh/mykey.pem" \
  site/ ubuntu@203.0.113.10:/tmp/a67/site/
sending incremental file list
created directory /tmp/a67/site
./
index.html
css/
css/style.css
js/
js/app.js

sent 655 bytes  received 124 bytes  173.11 bytes/sec
total size is 343  speedup is 0.44

The trailing slashes matter. site/ means “the contents of site”; without the slash, rsync would create site/site/ on the server. Also, rsync creates only the last path segment — if the parent directory does not exist yet, create it first with ssh ubuntu@203.0.113.10 'mkdir -p /tmp/a67' or add --mkpath.

Step 3.

Then, confirm the copy from the server side.

ssh -i ~/.ssh/mykey.pem ubuntu@203.0.113.10 'ls -R /tmp/a67/site'
/tmp/a67/site:
css
index.html
js

/tmp/a67/site/css:
style.css

/tmp/a67/site/js:
app.js

Step 4.

Now edit one file locally — a new paragraph in index.html — and deploy again. This re-run is the everyday command, so it is worth watching closely.

sending incremental file list
./
index.html

sent 500 bytes  received 46 bytes  121.33 bytes/sec
total size is 372  speedup is 0.68

Only index.html travels. As a result, deploying a thousand-file site after a one-file edit costs almost nothing — this delta transfer is the whole reason rsync beats scp for deploys.

Step 5.

Finally, handle deletions. By default rsync never removes anything, so a file you delete locally lives on at the server forever. --delete fixes that — but it will happily wipe the whole target if you mistype a path. Therefore, always pair it with -n first.

rsync -avzn --delete --exclude 'uploads/' --exclude 'config.php' \
  -e "ssh -i ~/.ssh/mykey.pem" \
  site/ ubuntu@203.0.113.10:/tmp/a67/site/
sending incremental file list
deleting js/app.js
js/

sent 202 bytes  received 37 bytes  68.29 bytes/sec
total size is 339  speedup is 1.42 (DRY RUN)

The plan says it will delete only js/app.js — the file removed locally. That is exactly right, so run the same command without -n to apply it. Excluded paths are safe here too: --exclude also shields uploads/ from --delete.

The result when you deploy a site with rsync.

The everyday deploy now reads: edit locally, dry-run, deploy. Each run lists precisely what changed and transfers nothing else.

sending incremental file list
deleting js/app.js
js/

sent 202 bytes  received 37 bytes  68.29 bytes/sec
total size is 339  speedup is 1.42

deploy a site with rsync: the incremental re-run transfers one changed file and the --delete run removes the deleted script

Notes when you deploy a site with rsync:

  • Turn the command into a one-line script (deploy.sh) so every deploy uses identical flags. A forgotten --exclude is how a config.php ends up published.
  • rsync copies your local ownership and modes. After the first deploy, check that the web server can actually read the files — setting file and directory permissions with chmod covers the fix.
  • --delete plus a wrong target path equals data loss. The dry run costs one command; recovering a wiped directory costs an afternoon.
  • For many small files rsync is dramatically faster than scp, because it skips everything unchanged instead of re-sending the tree.

References:

//

Series: Maintain a Server

You are reading part 2 of 5 — follow the parts in order.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
//

Featured tutorial

Leave a comment

Your email address will not be published. Required fields are marked *