Tools & Reviews 20 min read

Free Website Backup: Tools and Methods That Work

Free website backup methods rechecked against vendor documentation on 13 August 2026: rsync and mysqldump written correctly, restic snapshots on $0.015/GB object storage, which WordPress free tiers really schedule off-site, and what cPanel will not restore for you.

ST
Scraping.Pro Team
Data collection for business needs
Published: 22 January 2026

In May 2024 a misconfiguration inside Google Cloud deleted UniSuper's private cloud subscription. Not a disk and not a virtual machine: the subscription itself, replicated across two zones, and everything inside it. The Australian pension fund, which administers retirement savings for more than half a million members, came back over the following week for exactly one reason. It kept a copy of its data with a second provider, outside the account that disappeared. Google Cloud and UniSuper said so in a joint statement.

Three years earlier, in March 2021, fire destroyed OVHcloud's SBG2 building in Strasbourg. Customers who had bought the backup option and let it write to the same campus lost the site and the backup in the same hour. In August 2023 the Danish host CloudNordic told its customers that a ransomware attack had encrypted production and backup systems together and that the data was unrecoverable. Most of those customers never got their sites back.

None of these people had skipped backups. They had skipped the part where one copy sits somewhere the disaster cannot reach, and the part where somebody has actually restored it. That gap is what this guide is about. Every method below is free, and every version and price here was read from the vendor's own page on 13 August 2026.

Two numbers to write down before you pick a tool

Every backup decision falls out of two numbers, and almost nobody writes them down.

How much data can you afford to lose? A nightly job at 02:30 means that a failure at 02:00 costs you a full day of work. For a shop taking 200 orders a day, that is roughly eight orders an hour of exposure, and eight orders is a support conversation, not a catastrophe. For a busy forum, a day is unacceptable and you need the database more often than the files.

How long can you be down? This one is arithmetic, and it is usually worse than people guess. Forty gigabytes over a 100 Mbit/s link is 53 minutes of pure transfer, before you unpack anything or import a single row. Pull the same archive down a 20 Mbit/s home upload and you are at four and a half hours. Restoring a large SQL dump is slower than creating it, because the server rebuilds every index on the way in.

What a complete backup contains

For a static site, the files are the whole story. Plain HTML, or a built Jekyll, Hugo or Astro site, has nothing behind it.

For a dynamic site there are two halves, and half a backup restores to nothing. The files are HTML, PHP, JS, CSS, images, uploads, themes, plugins and config. The database is where the content actually lives, and a dump without files gives you rows with nothing to render them.

There is a third part, and it is the one that turns a two-hour restore into a two-day one. Call it the environment, and write it down rather than back it up:

  • Database users and their grants. A dump restores tables, not the account your application connects with.
  • Cron entries and systemd timers. They live outside the web root and vanish silently.
  • TLS certificates and the renewal configuration. A new server with old DNS gets a browser warning within minutes.
  • DNS records, and the TTL on them. A 24-hour TTL turns a 20-minute restore into a day of half-broken traffic.
  • PHP version and extensions, plus any php.ini overrides your code depends on.
  • Web server config: .htaccess, nginx server blocks, redirects, rewrite rules.
  • File ownership and permissions. Restoring as root is a classic way to make a working site return 500.
  • Secrets: wp-config.php salts, .env values, SMTP and payment API keys.

A crawler copy made with HTTrack or wget is a fourth thing again. It saves the rendered public pages, not your source code and not your database: right for archiving a site you do not control, wrong for protecting one you own.

Method 1: the host's own backups, and the sentence cPanel buries

Check what your host already does before you build anything. The cheapest backup is the one somebody else already runs.

cPanel. The Backup Wizard produces a full account archive containing your home directory, databases, email forwarders and filters. It lands in your home directory or goes out to FTP or SCP. Then comes the line most write-ups leave out, from cPanel's own Backup Wizard documentation: "You cannot use a full backup file to restore your files through the cPanel interface. Contact your hosting provider for assistance." Partial backups are different. A home-directory tarball, a .sql dump or an email-filter file can each be restored by you, from the Restore screen, without a ticket. If self-service recovery matters, take partial backups.

Plesk. The Backup Manager schedules full or incremental backups to local or remote storage, though what is switched on depends on the licence your provider bought.

Managed WordPress hosts typically snapshot daily and restore from a button. Read the retention: 14 days will not help with a defacement you noticed after a month.

VPS and cloud. These are metered, and it is worth knowing the meter. DigitalOcean's backup pricing is 20% of the monthly Droplet cost for weekly backups and 30% for daily ones. On a $24 Droplet, daily backups add $7.20 a month and run whether or not you remember them.

One catch applies to all four. Host backups live on the same infrastructure as the site, in the same account, behind the same stolen password. UniSuper survived because one copy was somewhere else.

Method 2: rsync and mysqldump, written the way they should be

The old routine was FTP plus a manual phpMyAdmin export, with no history and no schedule. Two commands replace it.

Files. rsync over SSH moves only what changed:

bash
# Dry run first. Always. --delete is not reversible.
rsync -avz --dry-run --delete user@yourserver.com:/var/www/yoursite/ ./backup/files/
rsync -avz --delete --link-dest=../previous \
      user@yourserver.com:/var/www/yoursite/ ./backup/current/

This breaks when you mistype a path. --delete makes the destination match the source. Point it at an empty or wrong directory and rsync will faithfully empty your backup to match, quickly and without asking. The --dry-run line above is not decoration. --link-dest is the other half of the answer: it hard-links unchanged files against the previous run, so ten dated snapshots of a 5 GB site cost about 5 GB plus the deltas, and each one still looks like a complete tree.

Patch rsync while you are in there. Version 3.5.0 landed on 13 August 2026 and is described by the project as a major security release fixing 33 issues, including a critical PROXY-protocol source-address spoof and a command-injection flaw; 3.4.3 in May 2026 fixed six more, and 3.4.0 in January 2025 fixed six, the worst of which let a malicious server write outside the destination directory on a client pulling from it. The rsync security page lists them. If you expose an rsync daemon, this is your week.

Database. The command in most tutorials, including the earlier version of this one, is incomplete:

bash
# ~/.my.cnf holds the credentials, chmod 600. A password on the command
# line is visible to every user on the box via ps.
mysqldump --defaults-extra-file=~/.my.cnf \
          --single-transaction --routines --events --triggers \
          --databases yourdb | gzip > backup/db-$(date +%F).sql.gz

Three corrections to the short version. --routines and --events both default to false, so stored procedures, functions and scheduled events are missing from a plain dump; --triggers defaults to true. --single-transaction gives a consistent snapshot only for transactional tables, which in practice means InnoDB, and MariaDB's mariadb-dump documentation is blunt about the other half: during such a dump "no other connection should use the following statements: ALTER TABLE, CREATE TABLE, DROP TABLE, RENAME TABLE, or TRUNCATE TABLE." A plugin update that adds a column at 02:31 can corrupt the dump that started at 02:30, and nothing will warn you. And MySQL's manual says plainly that "Specifying a password on the command line should be considered insecure."

On MariaDB the binary is now mariadb-dump. The mysqldump symlink still exists on Linux but is deprecated from MariaDB 11.0 and already absent from the official MariaDB Docker image, so a script written against the old name will fail inside a container with a confusing "command not found".

Restoring is the reverse: rsync the tree back, then gunzip < dump.sql.gz | mysql -u dbuser -p yourdb. On WordPress, WP-CLI 2.12.0 does the database half with wp db export and can search-replace the site URL during a move. For SQL Server rather than MySQL the mechanics differ, and are covered in backing up an MS SQL Server database.

One version note that catches people mid-restore. Oracle's end-of-life notice put MySQL 8.0 under Sustaining Support on 21 April 2026, and MariaDB 10.6 reached end of life on 6 July 2026, while WordPress still recommends "MySQL version 8.0 or MariaDB version 10.11 or greater" on its download page. Restore a 2024 dump onto a 2026 server and you may cross a major version boundary you did not plan for. Put the source version in the filename.

Method 3: encrypted, deduplicated snapshots for the price of a coffee

Dated .sql.gz files and rsync trees work, but they are neither encrypted nor deduplicated, and the bill grows linearly with retention.

restic is a single binary with no server component, and the project's own site lists 0.19.1 as the current release, announced on 5 July 2026. It encrypts client-side, deduplicates at the chunk level, and speaks S3-compatible object storage directly:

bash
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/my-bucket"
export RESTIC_PASSWORD_FILE="/root/.restic-pass"
restic init
restic backup /var/www/yoursite /var/backups/db
restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --prune

The economics are the argument. Backblaze B2 starts at $6.95 per TB per month with the first 10 GB free, free egress up to three times your stored volume and $0.01 per GB after that. Cloudflare R2 charges $0.015 per GB-month with 10 GB-month free, one million Class A and ten million Class B operations free, and no egress charge at all. Twenty gigabytes of deduplicated snapshots therefore costs about $0.07 a month on B2 and about $0.15 on R2, and the encryption key never leaves your machine.

If you want a plain mirror rather than a snapshot history, rclone 1.75.0 speaks to the same backends. Whichever you pick, turn on object lock for the bucket. A backup an attacker can delete with your credentials is a second target, not an off-site copy.

Method 4: WordPress plugins, and which free tier actually schedules off-site

Plugins are the least technical route, and the free tiers differ far more than the roundups admit. Figures below are from each plugin's wordpress.org page on 13 August 2026; the current WordPress release is 7.0.4.

Plugin Version / installs Free tier does Paid tier adds
All-in-One WP Migration 7.109 / 5m+ one-click export and import of the whole site Unlimited Extension for large imports, cloud destinations, multisite
UpdraftPlus 1.26.6 / 3m+ scheduled files and database to Drive, Dropbox, S3, FTP, email; restore from the dashboard incremental backups, one-click migration, database encryption, multisite; from $70/yr for two sites
BackWPup 5.7.5 / 400k+ scheduled hourly to monthly, to Dropbox, S3, Azure, FTP, several at once direct restore from cloud storage, Google Drive and OneDrive, encryption, migration
Duplicator 1.5.16.1 / 1m+ manual full-site archive, single site only scheduled backups, every cloud destination, multi-threading for large sites; from $79/yr

Two corrections against the usual lists, this article's earlier version included. UpdraftPlus is routinely called the most popular WordPress backup plugin; wordpress.org's own counter puts All-in-One WP Migration and Backup at 5 million or more active installs against UpdraftPlus at 3 million or more. And Duplicator gets recommended for automated backups when Duplicator Lite has neither. Its plugin page puts "Scheduled backups" and "Cloud Storage to Dropbox Backups, Google Drive Backups, Microsoft OneDrive Backups, Amazon S3 Backups and FTP/SFTP Backups" in the Pro column. Duplicator Lite makes an excellent migration package on demand. It will not run unattended, and it will not push anything off your server.

If the requirement is free, scheduled and off-site, the honest shortlist is UpdraftPlus and BackWPup.

Where plugin backups break. They run inside PHP, under your host's max_execution_time and memory limit. A 6 GB uploads directory on shared hosting will hit that wall, and the failure mode is a job that stops halfway and reports nothing useful. Verify the first backup by downloading the archive and opening it. Check again in a month: a plugin update or a PHP upgrade can quietly break a schedule that has been green for a year.

Method 5: HTTrack and wget copy pages, not sites

To download an entire website as a browsable static copy, for offline reading or for archiving a site before it changes, you want a site copier. These are polite web crawlers: fetch a page, find its links, follow them, rewrite the URLs so the copy works locally.

HTTrack is not dead, whatever the roundups say. The claim travels because httrack.com still offers Windows installers labelled 3.49.2 and dated 20 May 2017, and reviewers stop reading there. The same download page currently serves a Linux source tarball, httrack-3.49.20.tar.gz, dated 10 August 2026, a Windows beta 3.50-beta-3 dated 8 August 2026, and a macOS arm64 alpha build. The GitHub repository tagged 3.49.21 on 12 August 2026. The accurate statement is narrower: the stable Windows binary most people download is nine years old, and the source line is still moving.

bash
httrack "https://example.com" -O ./example-backup "+example.com/*" -v

wget is not already installed, and that matters. macOS ships curl, not wget; you install it from Homebrew or MacPorts. Fedora went further and replaced the original package with wget2, and Fedora's own change proposal states the limit in one sentence: "Except for WARC and FTP, Wget2 is a drop-in replacement for Wget in most cases." If your wget is wget2, the archival options in the next section are not there. Check with wget --version before you trust a script. GNU wget 1.25.0 dates from 10 November 2024 and wget2 2.2.1 from 30 December 2025, both from the GNU mirror.

bash
wget --mirror --convert-links --adjust-extension \
     --page-requisites --no-parent \
     --wait=1 --random-wait --limit-rate=500k \
     https://example.com/
  • --mirror is documented in the wget manual as "equivalent to -r -N -l inf --no-remove-listing". Without it, plain -r stops at depth 5 and you get a partial copy that looks complete.
  • --convert-links rewrites links so the offline copy navigates.
  • --adjust-extension adds .html where the server did not.
  • --page-requisites pulls CSS, JS and images.
  • --no-parent keeps the crawl from climbing above the start path.
  • --wait=1 --random-wait --limit-rate=500k is the difference between archiving a site and hammering it. The manual recommends --wait explicitly, "as it lightens the server load by making the requests less frequent". A mirror run flat out from one address is indistinguishable from an attack, and on a site behind a bot-management service it will be treated as one.

The limitation to keep in front of you. Copiers save what the server sends. They capture rendered HTML and static assets, and they capture neither your PHP source, nor your database, nor anything a browser builds after JavaScript runs. Point wget at a modern single-page app and you get a near-empty shell plus a bundle, which is a description of the problem covered in scraping dynamic content. Use copiers for archives and offline reading, never as the primary backup of a dynamic site you own. Copy only what you have the right to copy, and honour the target's robots directives.

Archiving that survives: WARC instead of a folder of HTML

A --mirror copy is a folder of rewritten files. It has lost the HTTP status codes, the response headers, the redirect chain and the original URLs, which is exactly the material you need if the archive is ever evidence rather than reading matter. The web archiving world solved this with WARC, the ISO-standard container that stores requests and responses verbatim.

GNU wget writes it natively. The manual documents --warc-file as "Use file as the destination WARC file", --warc-cdx to "Write CDX index files", --warc-max-size to cap file size, and --warc-dedup to skip records already listed in a CDX index.

bash
wget --mirror --page-requisites --no-parent \
     --warc-file=example-2026-08-13 --warc-cdx \
     --wait=1 --random-wait \
     https://example.com/

That costs one flag and gives you a self-describing archive with the headers intact. For JavaScript-heavy sites where wget sees nothing, Browsertrix Crawler drives a real browser through the DevTools Protocol in a single Docker container and writes WACZ, a packaged WARC with an index. Either output opens in ReplayWeb.page, which renders archives in the browser with no server behind it. For a standing archive rather than one-off captures, ArchiveBox is MIT-licensed and saves each URL as HTML, PDF, PNG, WARC and SQLite at once.

One tool that is not a backup, despite being suggested as one constantly: the Wayback Machine. Its own Save Page Now documentation says the method "only saves a single page, not the whole site", that "it does not save any of the outlinks", and that it "can't be used to initiate a crawl of an entire web site". It pins one page to a permanent URL. It is not a site copy.

Method 6: Git for the code half

Anything code-shaped belongs in version control: themes, templates, application code, deployment scripts, server configuration. Push to a private repository on GitHub, GitLab or Bitbucket and you get history, an off-site copy, and the ability to undo one bad change instead of a whole night.

The limits are worth knowing before you meet them mid-push. GitHub's large files documentation warns above 50 MiB and blocks above 100 MiB per file, recommends repositories stay under 1 GB and states that "less than 5 GB is strongly recommended". Git LFS on Free and Pro includes 10 GiB of storage and 10 GiB of bandwidth; past that, with a $0 budget set, GitHub's billing docs say "Git LFS usage is blocked for the rest of the calendar month".

Three things people commit and regret. Uploads and media, which bloat the repository past every limit above and belong in object storage. wp-config.php and .env, because a secret pushed once lives in the history forever and deleting the file changes nothing; rotate the credential instead. And node_modules, for reasons that need no elaboration.

Git covers the code half only. It holds no database and no uploads, so it pairs with Method 2 or Method 4 rather than replacing them. A repository you never push is a backup on the disk you are trying to protect.

Method 7: schedule it so that failure is loud

A backup you have to remember is a backup you will forget. The interesting failure is not the one that never runs, it is the one that runs and quietly does nothing for eight months.

bash
#!/bin/bash
# /home/user/backup-site.sh
set -euo pipefail                       # die on the first error, not the last
export PATH=/usr/local/bin:/usr/bin:/bin
exec 9>/var/lock/backup.lock; flock -n 9 || exit 0   # never overlap runs

rsync -a --delete --link-dest=/backups/previous /var/www/site/ /backups/current/
mysqldump --defaults-extra-file=/root/.my.cnf --single-transaction \
          --routines --events --databases yourdb | gzip > /backups/db-$(date +%F).sql.gz
restic backup /backups/current /backups/db-$(date +%F).sql.gz
find /backups -name 'db-*.sql.gz' -mtime +30 -delete

curl -fsS -m 10 https://hc-ping.com/YOUR-UUID    # tell the watchdog it worked
bash
# crontab -e  — run it every night at 02:30
30 2 * * * /home/user/backup-site.sh >> /home/user/backup.log 2>&1

Four details do the work. set -euo pipefail stops the script at the first failure instead of pinging success anyway. The explicit PATH is there because cron's environment is nearly empty, and a script that works in your shell but fails under cron has usually just lost /usr/local/bin. flock keeps last night's slow run from colliding with tonight's. The final curl is a dead man's switch: Healthchecks.io monitors 20 jobs free with 100 log entries each, and mails you when the expected ping does not arrive. Without it a broken cron job is silent by design, because cron mails errors to a local mailbox nobody has read since 2009.

On a machine that is not always on, use a systemd timer with Persistent=true instead. Cron skips missed runs; the timer catches up at the next boot. More Linux scheduling habits that survive contact with a real server are in the Linux web scraper tips.

3-2-1, and the two digits ransomware added

The rule comes from photographer Peter Krogh's The DAM Book, where it was advice about protecting image libraries. The arithmetic held up:

  • 3 copies of the data,
  • on 2 different media or platforms,
  • with 1 copy off-site, at a different provider from your host.

Ransomware added two more digits, and the extended form is now written 3-2-1-1-0. The extra 1 is a copy that is offline or immutable, because an attacker with your credentials will look for the backups first, which is precisely what happened at CloudNordic. Object lock on a B2 or R2 bucket costs nothing extra and takes a minute. The 0 is zero errors on verification: restic check --read-data-subset=10% on a schedule, or a hash of the dump compared after transfer.

Off-site means a different provider, not a different directory. A snapshot in the same account as the server it protects shares that server's fate.

The restore rehearsal

An untested backup is a rumour. Rehearse it once, write down what broke, and the next one takes twenty minutes.

Restore to somewhere disposable: a local Docker stack, a $6 VPS, a subdomain. Import the database, unpack the files, point a hosts-file entry at the copy, and then walk the list that always bites:

  1. The database user and grants, which the dump did not contain.
  2. The site URL, stored in the database on WordPress and needing a search-replace after a move.
  3. Permalinks and rewrite rules, which is the usual cause of a working homepage and a 404 on everything else.
  4. File ownership and permissions, especially if you unpacked as root.
  5. The PHP version and extensions on the target.
  6. Cron jobs and timers, which are not in the web root and were never in the archive.

Time the whole thing. That number is your real recovery objective, and it is almost always larger than the one you assumed at the start. Repeat quarterly, and after any migration.

Where free stops working

Free methods have a ceiling. Better to know where yours is before an incident finds it.

Logical dumps do not scale. mysqldump is single-threaded and its output is a stream of INSERT statements; on a 40 GB database the dump takes hours and the restore takes longer, because every index is rebuilt on import. The free answer is a physical hot backup. Percona XtraBackup is open source and copies InnoDB files while the server runs, with the version caveat spelled out in its docs: the 8.4 series "does not support backups on MySQL 8.0 or 9.x servers", so match the tool to the server.

No binary logs, no point-in-time recovery. Nightly dumps let you restore to 02:30. Restoring to 14:07, just before somebody ran a bad UPDATE, needs binlogs kept alongside the dumps.

Restore bandwidth is a real cost. Egress is where free tiers stop being free. B2 gives three times your stored volume free each month, then charges $0.01 per GB, so pulling 200 GB back with 20 GB stored costs about $1.40. R2 charges nothing for egress at any volume, which suits a large restore despite the higher storage rate.

Free tiers carry no promises. No retention guarantee, no support queue at 3 a.m., no compliance attestations, no legal hold. If a lost day of data would cost more than a year of a paid plan, the free plan is not the cheap option.

Which method should you use?

Situation Best free method
Any site, least effort Host panel or managed backups; on cPanel take partial ones so you can restore them yourself (Method 1)
Custom PHP + MySQL app rsync with --link-dest plus mysqldump on cron, pushed to object storage with restic (Methods 2, 3, 7)
WordPress or CMS UpdraftPlus or BackWPup on a schedule to Drive, Dropbox or S3 (Method 4)
Static site Git, plus the built output mirrored anywhere (Methods 5, 6)
Archiving a site you do not own wget with --warc-file, or Browsertrix Crawler when the page needs JavaScript
Application code and config Git to a private repo, secrets excluded (Method 6)
Database over ~20 GB Percona XtraBackup with binlogs, not mysqldump

Most real sites use two of these together. A WordPress plugin sends files and database to Drive every night, Git holds the custom theme, and neither one alone would bring the site back.

Mirroring one site with wget is a weekend job. Mirroring two thousand of them on a schedule, rendering the JavaScript and delivering rows instead of folders is a different problem, and the one managed extraction exists to absorb. For your own site none of that applies. Write the script, push a copy somewhere your host cannot reach, and restore it once before you need to.