# rsync Tutorial: Copy, Sync and Back Up Files over SSH with Incremental Snapshots

> Learn rsync with examples: copy and mirror folders, trailing slashes, excludes, SSH sync, dry runs, bandwidth limits and incremental backups with hard links.

- Source: https://www.itwonderlab.com/rsync-tutorial-backup-ssh/
- Published: 2026-08-29
- Updated: 2026-08-29
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## What is rsync

[rsync](https://www.itwonderlab.com/rsync/) copies files and sends only the parts that changed. The first run copies everything and the next ones take seconds, which makes it ideal for backups, mirroring a website or moving data between servers. It works locally and over [SSH](https://www.itwonderlab.com/ssh/), with no server software other than rsync itself and an SSH login.

```bash
sudo apt install -y rsync     # macOS: brew install rsync
rsync --version
```

Create some data to practice:

```bash
mkdir -p demo/src/docs && cd demo
echo one > src/a.txt && echo two > src/docs/b.txt && echo skip > src/debug.log
```

## The basic command

```bash
rsync -avh src/ dest/
```

- `-a` (archive): recursive, and keeps permissions, owners, times and symlinks. You almost always want it.
- `-v`: list the files. `-h`: human readable sizes.

Run it again and nothing is transferred. Change `a.txt` and run it once more: only that file is sent.

### The trailing slash

This is the most common mistake:

```bash
rsync -a src/ dest/    # copies the CONTENTS of src into dest:  dest/a.txt
rsync -a src dest/     # copies the folder itself:              dest/src/a.txt
```

The slash after the **source** decides it. The slash after the destination does not matter.

## Preview first with --dry-run

```bash
rsync -avhn --delete src/ dest/
```

`-n` (`--dry-run`) prints what would happen without touching anything. Make it a habit before you use `--delete`.

## Mirror with --delete

By default rsync never removes files from the destination. To make `dest` an exact copy of `src`:

```bash
rsync -avh --delete src/ dest/
```

> [!WARNING]
> `--delete` removes files at the destination. A wrong path or a missing trailing slash can delete real data. Run with `-n` first, and never point it at a folder you cannot afford to lose.

## Exclude and include

```bash
rsync -avh --exclude '*.log' --exclude 'node_modules/' --exclude '.git/' src/ dest/
rsync -avh --exclude-from=exclude.txt src/ dest/
rsync -avh --include '*.conf' --exclude '*' src/ dest/     # only .conf files
```

Patterns are checked in order, so put `--include` before the `--exclude '*'` that follows it.

## Copy over SSH

```bash
# push to a server
rsync -avzh src/ ubuntu@203.0.113.10:/backups/web/

# pull from a server
rsync -avzh ubuntu@203.0.113.10:/var/www/ ./www-copy/

# with a key and a different port
rsync -avzh -e "ssh -i ~/.ssh/backup_key -p 2222" src/ ubuntu@203.0.113.10:/backups/web/
```

`-z` compresses data on the wire, which helps on slow links and wastes CPU on already compressed files such as images and videos. The login uses [public key authentication](https://www.itwonderlab.com/public-key-authentication/). Between two remote servers, run rsync on one of them.

## Useful options

| Option | Use |
|---|---|
| `-P` | `--partial --progress`: keep partly transferred files and show progress |
| `--info=progress2` | One progress line for the whole transfer |
| `--bwlimit=5000` | Limit to about 5000 KiB/s |
| `-c` | Compare checksums instead of size and time (slow, thorough) |
| `-i` | Itemized list: explains why each file is copied |
| `--remove-source-files` | Move instead of copy |
| `--max-size=100m` | Skip big files |
| `-A -X` | Also copy ACLs and extended attributes |
| `--rsync-path="sudo rsync"` | Read root-owned files on the remote side |

For a large transfer over a bad connection, use `rsync -avP` and run it again after any failure: it continues where it stopped.

## Incremental backups with hard links

`--link-dest` creates a new folder for each backup in which unchanged files are **hard links** to the previous backup. Each snapshot looks like a full copy, but only the changed files use extra disk space.

```bash
#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail

SRC="/var/www/"
DEST="/backups/web"
NOW="$(date +%F_%H%M)"

mkdir -p "$DEST"
rsync -a --delete --link-dest="$DEST/latest" "$SRC" "$DEST/$NOW/"
ln -sfn "$DEST/$NOW" "$DEST/latest"

# keep the last 14 snapshots
ls -1dt "$DEST"/20* | tail -n +15 | xargs -r rm -rf
```

Deleting an old snapshot is safe: a file stays on disk while any snapshot still links to it. To store the backups on another machine, run the script there and pull from the source over SSH.

## Schedule it with cron

```text
30 2 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
```

This runs every night at 02:30 and never twice at the same time. See [cron expressions](https://www.itwonderlab.com/cron-expressions-examples/).

## Restore

Restoring is the same command in the other direction:

```bash
rsync -avh /backups/web/latest/ /var/www/
rsync -avh /backups/web/2026-10-01_0230/index.html /var/www/index.html
```

Test a restore regularly: a backup that was never restored is only a hope.

## rsync or something else

- **Databases** need a consistent dump first (see [PostgreSQL](https://www.itwonderlab.com/install-postgresql-docker/) and [MariaDB](https://www.itwonderlab.com/install-mariadb-docker/)); copying live data files may give a corrupted copy.
- **S3** has its own tool, `aws s3 sync` ([S3](https://www.itwonderlab.com/aws-s3/)).
- **Versioned and deduplicated backups** with encryption: tools such as restic or borg. rsync is simple and fast for plain copies.
- **Servers as code**: automate the copy with the Ansible `synchronize` module, which wraps rsync ([Ansible](https://www.itwonderlab.com/install-ansible-first-playbook/)).
