How to back up a PHP website on your own server, and test the restore
A disk can die, an update can go wrong, and a hacked site may need to be put back as it was yesterday. Then only one thing helps: a recent backup that you know works. Here is how to make one every night, and how to prove it works.
The examples use a site called shop, with its files in /var/www/shop and a MariaDB database also called shop. Use your own names. The commands were tested on Ubuntu 24.04 (restic 0.16, MariaDB 10.11) and Debian 13 (restic 0.18, MariaDB 11.8), backing up to a second server over SFTP.
What a good backup looks like
An old rule called 3-2-1 says it well: keep 3 copies of your data (the live site plus two backups), on 2 different kinds of storage, with 1 copy far away from the server. A backup on the same server is gone when the server is.
On top of that, a good backup is:
- Complete: the website files and the database. One without the other is not much use.
- Daily, and it runs by itself.
- Encrypted, because it holds customer data and passwords.
- More than one copy: if a site was hacked a week ago, last night's backup is hacked too.
- Tested: a backup you have never restored is only a hope.
1. Install restic
restic is a free backup tool. It is in the normal Ubuntu and Debian package lists:
sudo apt update
sudo apt install restic
restic version
2. Choose where the backups go
restic can send backups to many places. Two common ones:
- Another server you can reach with SSH (restic uses SFTP, which is part of SSH). Best at a different provider, or at least in a different data centre.
- An S3-compatible storage bucket. Many providers sell these, and they cost little for a small site.
Either way, restic encrypts the data on your server first. The other server or the storage provider only ever sees scrambled files.
3. Make a backup password and a private settings file
Make a folder that only root can open, and a long random password in it:
sudo install -d -m 700 /etc/restic
sudo sh -c 'umask 077; openssl rand -base64 32 > /etc/restic/password'
Save a copy of this password somewhere safe, away from the server (for example in your password manager). Without it, nobody can read the backups. Not an attacker, but also not you.
Now make the settings file /etc/restic/env (for example with sudo nano /etc/restic/env). It says where the backups go. For another server over SFTP:
RESTIC_REPOSITORY=sftp:backup-server:/home/backups/web1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
For an S3-compatible bucket, use the address and keys from your storage provider instead:
RESTIC_REPOSITORY=s3:https://s3.example.com/my-bucket/web1
AWS_ACCESS_KEY_ID=PASTE-ACCESS-KEY
AWS_SECRET_ACCESS_KEY=PASTE-SECRET-KEY
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
Then make sure only root can read it:
sudo chmod 600 /etc/restic/env
Keep passwords and keys in these two files only. Never write them into a script, a cron line or a file that other users or your website can read. If your provider lets you, make a key that can only use this one bucket.
web1 is a name for this server's backups. Give each server its own name.
4. Set up the backup server (SFTP only)
Skip this step if you use a bucket. For a backup server, we make a separate login that can only copy files, nothing else.
On the web server, make an SSH key just for backups and show its public half:
sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
sudo cat /root/.ssh/backup_key.pub
The key has no passphrase, because the backup runs at night with nobody there to type one. That is why it only gets file access on the other side.
On the backup server, make a user for the backups and its SSH folder:
sudo useradd -m -s /bin/bash backups
sudo install -d -m 700 -o backups -g backups /home/backups/.ssh
sudo nano /home/backups/.ssh/authorized_keys
Paste the public key as one line, with this in front of it:
restrict,command="internal-sftp" ssh-ed25519 AAAA...rest-of-the-key... root@web1
restrict turns off everything extra, and command="internal-sftp" means this key can only move files. If someone tries to log in with it for a shell, they get "This service allows sftp connections only." Then fix the owner and rights:
sudo chown backups:backups /home/backups/.ssh/authorized_keys
sudo chmod 600 /home/backups/.ssh/authorized_keys
Back on the web server, tell SSH how to reach the backup server. Add this to /root/.ssh/config (with sudo nano /root/.ssh/config), using your backup server's real name or IP address:
Host backup-server
HostName backup.example.com
User backups
IdentityFile /root/.ssh/backup_key
Connect once by hand. SSH shows the backup server's fingerprint. Check that it matches, type yes, then type ls and bye:
sudo sftp backup-server
5. Create the backup store
Open a root shell and load the settings. Run the rest of the commands in this guide in this window:
sudo -i
set -a; . /etc/restic/env; set +a
restic init
set -a loads the settings for this window only. They are not saved anywhere else. restic init answers with created restic repository ....
6. Write the backup script
Make the file /usr/local/sbin/site-backup:
#!/bin/bash
# Daily backup: dump the databases, back up files and dumps, then delete old copies.
set -euo pipefail
umask 077
# 1. Dump each database to a file (list your databases here).
mkdir -p /var/backups/db
for db in shop; do
mariadb-dump --single-transaction --routines --triggers "$db" > "/var/backups/db/$db.sql"
tail -n 1 "/var/backups/db/$db.sql" | grep -q 'Dump completed'
done
# 2. Back up the website files, the settings and the dumps.
restic backup --tag daily /var/www /etc /var/backups/db
# 3. Keep 7 daily, 4 weekly and 6 monthly backups, delete the rest.
restic forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
# 4. Check that the backup storage is healthy.
restic check
Only root may read or run it:
chmod 700 /usr/local/sbin/site-backup
What it does:
- Database first.
--single-transactiontakes a clean copy without stopping the site. Thetailline checks that the dump finished. If anything fails, the script stops and the run is marked as failed. On MySQL, usemysqldumpinstead ofmariadb-dump. Put all your databases in theforline, likefor db in shop blog; do. - Then files.
/var/wwwholds the sites,/etcholds the web server, PHP and certificate settings, and/var/backups/dbholds the fresh dumps. If your sites live somewhere else, add that folder. - The dumps are not zipped on purpose. restic compresses them itself and only sends the parts that changed since last night, so a daily backup is usually small and quick.
7. Run it every night
A systemd timer starts the script at night, and catches up after a reboot if it missed a run. Make two files. First /etc/systemd/system/site-backup.service:
[Unit]
Description=Back up websites and databases with restic
Wants=network-online.target
After=network-online.target mariadb.service
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/local/sbin/site-backup
Nice=10
IOSchedulingClass=idle
Then /etc/systemd/system/site-backup.timer:
[Unit]
Description=Run the website backup every night
[Timer]
OnCalendar=*-*-* 02:30
RandomizedDelaySec=30min
Persistent=true
[Install]
WantedBy=timers.target
This runs between 02:30 and 03:00 server time. Nice and IOSchedulingClass make the backup give way to your websites. Switch it on and run the first backup now:
systemctl daemon-reload
systemctl enable --now site-backup.timer
systemctl start site-backup.service
The first backup takes longest, because it sends everything. See what happened, and when the next run is:
journalctl -u site-backup.service -n 30
systemctl list-timers site-backup.timer
A good run ends with no errors were found and Finished site-backup.service. If a run fails, systemctl status site-backup.service says failed, and the log shows why. To get a message when a backup fails or doesn't run at all, add a heartbeat ping.
Prefer cron? Skip the two files and add this one line with crontab -e (as root):
30 2 * * * set -a; . /etc/restic/env; /usr/local/sbin/site-backup >> /var/log/site-backup.log 2>&1
8. How many copies to keep
The restic forget line in the script keeps the last 7 days, one backup per week for 4 weeks, and one per month for 6 months. --prune then really deletes the data that no backup needs any more, so the storage doesn't keep growing. Change the numbers if you like. More copies cost a bit more storage, but let you go further back, for example if you find out late that a site was hacked.
See the backups you have at any time:
restic snapshots
9. Test the restore
This is the step most people skip, and the one that matters most. Do it now, then once a month. Put a reminder in your calendar.
Restore the newest backup into a spare folder. This doesn't touch your live site:
restic restore latest --target /root/restore-test
Compare the files with the live site. No output means they are the same (files that changed since the backup will show up):
diff -r /var/www/shop /root/restore-test/var/www/shop
Load the database into a spare database and count the rows of one table:
mariadb -e "CREATE DATABASE shop_restore_test;"
mariadb shop_restore_test < /root/restore-test/var/backups/db/shop.sql
mariadb -e "SELECT COUNT(*) FROM shop_restore_test.orders;"
mariadb -e "SELECT COUNT(*) FROM shop.orders;"
(On MySQL, use mysql instead of mariadb.) The numbers should match, or be close if orders came in since last night. Because the dump was made of one database, it loads into whatever database you name and can't overwrite the real one by mistake.
Clean up:
mariadb -e "DROP DATABASE shop_restore_test;"
rm -rf /root/restore-test
Read back part of the stored data. The nightly restic check checks that the backup store is in order. This also downloads 5% of the data and checks it, a different part each time:
restic check --read-data-subset=5%
Need just one file back? Restore only that file into a spare folder, then copy it where it belongs:
restic restore latest --target /root/one-file --include /var/www/shop/index.php
To go back further than last night, use an ID from restic snapshots instead of latest.
Good to know
- Keep what you need for a restore off the server: the backup password, the contents of
/etc/restic/env, and how to log in to the backup server or bucket. If the server dies, you install restic on a new one and need all three. - An attacker with root on your server can reach your backups too, because the server has the keys. Some storage providers can keep deleted files for a number of days (look for "object lock" or "versioning"). Turn that on if you can. If you think someone got in, see first steps for a hacked site before you restore.
- Check the backups once a week with
systemctl list-timers site-backup.timer(theLASTcolumn shows the last run) andrestic snapshots. A backup that quietly stopped two months ago is a common surprise. - Leave room on the disk for a test restore. It needs about as much free space as the backup is big.
- If you change sites, update the
for db in ...line and the folders in the script.
Official sources
- Preparing a new repository: SFTP and S3 (restic docs)
- Backing up (restic docs)
- Restoring from backup (restic docs)
- Removing backup snapshots (restic docs)
- Checking integrity and consistency (restic docs)
- restic package for Ubuntu 24.04 and for Debian 13
- mariadb-dump (MariaDB)
- systemd.timer (systemd docs)
- sshd and the authorized_keys options (OpenSSH)
Next steps: keep MySQL and MariaDB safe, then work through the rest of the security checklist for a new PHP server.
ServerNest is being built to do this for you: daily backups of each app's files and databases with restic, encrypted on your server before they go to your own S3-compatible bucket, daily, weekly and monthly copies kept, and a monthly restore test that checks the stored data and restores the newest backup into a temporary folder. It is also being built to put one app's files or database back from a backup. Join early access to try it first.