How to create rsync snapshot backups on Linux

Hard-linked rsync snapshots let a Linux backup operator keep several browseable points in time without storing another copy of every unchanged file. Each dated directory looks like a complete tree, while matching regular files can share one inode on the backup filesystem.

The --link-dest option hard-links a file from an earlier tree only when its content and preserved attributes match. Its relative ../2026-07-10 value selects the previous sibling from the new destination, so both dated trees must share the same backup filesystem. The --checksum check reads same-size files instead of trusting only their modification time and size, which costs more disk I/O but prevents an undetected same-size content change from being linked as unchanged.

The publish and inode checks target Linux and use GNU-compatible mv and stat options that differ from the default macOS and BSD forms. Keep source writes paused through the whole-tree comparison, resume the workload afterward, and leave completed snapshot trees unchanged because later writes through shared inodes can alter more than one date.

Steps to create rsync snapshot backups on Linux:

  1. Pause applications or jobs that write to /srv/projects/, then confirm that writes have stopped.

    Use the application's native backup, checkpoint, or snapshot method for databases and other write-heavy data. A filesystem copy taken while related files are changing may not restore to one consistent point in time.

  2. List the completed snapshots and choose a new date.
    $ ls -1 /snapshots
    2026-07-10

    Replace the sample dates and paths consistently. Only one backup process should write to this snapshot root at a time.

  3. Create an empty staging directory for the new snapshot.
    $ mkdir /snapshots/.2026-07-11.in-progress

    mkdir must fail if this staging name already exists. Do not reuse an interrupted or completed snapshot as the writable destination; inspect and remove only the exact abandoned staging tree before starting again.

  4. Copy the settled source into the staging directory with the previous snapshot as the link source.
    $ rsync --archive --checksum --itemize-changes --link-dest=../2026-07-10 /srv/projects/ /snapshots/.2026-07-11.in-progress/
    .d..t...... ./
    >fcst...... workload.log
    >fc........ releases/current.txt
    >f+++++++++ releases/release-notes.txt

    Do not add --inplace or point this command at /snapshots/2026-07-10/. In-place writes can change data through shared hard links, and writing into a completed snapshot can damage historical data.

  5. Publish the snapshot after rsync exits with status 0.
    $ mv --no-clobber --verbose --no-target-directory /snapshots/.2026-07-11.in-progress /snapshots/2026-07-11
    renamed '/snapshots/.2026-07-11.in-progress' -> '/snapshots/2026-07-11'

    The --no-clobber and --no-target-directory options prevent an existing dated directory from becoming an accidental parent. Continue only when mv prints the renamed line.

  6. Compare the settled source with the published snapshot while source writes remain paused.
    $ rsync --archive --checksum --dry-run --delete --itemize-changes /srv/projects/ /snapshots/2026-07-11/

    The command must print nothing and exit with status 0. An itemized line identifies a content, path, or archived-attribute mismatch; --delete reports destination-only paths without removing them because --dry-run is active.

  7. Resume the paused applications or jobs and run their normal health check to confirm that /srv/projects/ accepts writes again.

    The live source may change after this point. Run later restore checks against the completed snapshot rather than comparing them with the changing source.

  8. Check one unchanged file across both snapshot dates.
    $ stat --format='%i %h %n' /snapshots/2026-07-10/policy.txt /snapshots/2026-07-11/policy.txt
    366911 2 /snapshots/2026-07-10/policy.txt
    366911 2 /snapshots/2026-07-11/policy.txt

    The matching inode number and link count of 2 show that the two paths share one unchanged file on disk.

  9. Create an empty directory for a restore test.
    $ mkdir /tmp/rsync-restore-test
  10. Restore the published snapshot into the test directory.
    $ rsync --archive --itemize-changes /snapshots/2026-07-11/ /tmp/rsync-restore-test/
    .d..t...... ./
    >f+++++++++ policy.txt
    >f+++++++++ workload.log
    cd+++++++++ releases/
    >f+++++++++ releases/current.txt
    >f+++++++++ releases/release-notes.txt
  11. Compare the complete restored tree with the published snapshot.
    $ rsync --archive --checksum --dry-run --delete --itemize-changes /snapshots/2026-07-11/ /tmp/rsync-restore-test/

    No output with status 0 confirms that every regular file matches by checksum, the archived attributes match, and neither tree has an extra path.

  12. Remove the temporary restore directory after the comparison passes.
    $ rm -r /tmp/rsync-restore-test
  13. Read a known file from the retained snapshot.
    $ cat /snapshots/2026-07-11/releases/current.txt
    release-2026.07.11