├── .gitignore ├── Makefile ├── README.md ├── all-backups.sh ├── common.sh ├── mysql-backup.sh ├── postgresql-backup.sh ├── purge.sh ├── push.sh ├── sample.env.restic └── sample.files-backup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | files-backup.sh 2 | log 3 | runitor 4 | rclone 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tools: runitor rclone 2 | 3 | P=linux-amd64 4 | V=v0.6.0 5 | 6 | runitor: 7 | curl -sLo runitor https://github.com/bdd/runitor/releases/download/$V/runitor-$V-$P 8 | chmod +x runitor 9 | 10 | rclone: 11 | curl -sLo rclone.zip https://downloads.rclone.org/rclone-current-$P.zip 12 | unzip -j rclone.zip */rclone && rm rclone.zip 13 | chmod +x rclone 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restic Backup Scripts 2 | 3 | This repository contains a set of shell scripts to maintain backups of a server 4 | using [restic](https://restic.net/). 5 | Main features: 6 | 7 | * Filesystem backups with restic 8 | * SQL backups of MySQL databases using `mysqldump` 9 | * SQL backups of PostgreSQL databases using `pgdump` 10 | * Intended to run daily from `cron` 11 | * Will purge old backups to a retention policy 12 | * Optional integration with [healthchecks.io](https://healthchecks.io/) 13 | * Handles transient files with a separate retention policy 14 | 15 | Currently in use in production backing up an Ubuntu 18.04 server to 16 | [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html). 17 | 18 | ## Getting Started 19 | 20 | Create a Unix user who will execute the backup jobs: 21 | 22 | ``` 23 | adduser --disabled-password restic 24 | ``` 25 | 26 | Follow the instructions 27 | [in the restic documentation](https://restic.readthedocs.io/en/stable/080_examples.html#backing-up-your-system-without-running-restic-as-root) 28 | to download the latest restic binary from the project's 29 | [releases page](https://github.com/restic/restic/releases/latest), 30 | install it in the `bin` directory of the user you just created, 31 | and give the `restic` binary permission to access the filesystem as root. 32 | 33 | Now switch user to the `restic` user and clone this repository: 34 | 35 | ``` 36 | su - restic 37 | git clone https://github.com/mhw/restic-backup-scripts 38 | ``` 39 | 40 | Create a `~/.env.restic` file and fill it in with the key needed to 41 | access your storage, and the restic repository in it: 42 | 43 | ``` 44 | cd restic-backup-scripts 45 | cp sample.env.restic ~/.env.restic 46 | dd if=/dev/urandom bs=15 count=1 2>/dev/null | openssl enc -a >~/.restic.pwd 47 | chmod o-r ~/.restic.pwd 48 | vi ~/.env.restic 49 | ``` 50 | 51 | **Note**: the contents of the `~/.restic.pwd` file is required to access 52 | the whole restic repository. 53 | Take appropriate precautions to protect it. 54 | 55 | Once you've got the environment set up correctly you'll need to initialise 56 | the restic repository: 57 | 58 | ``` 59 | . ~/.env.restic 60 | restic init 61 | # if using a separate repository for transient files 62 | restic -r $RESTIC_TRANSIENT_REPOSITORY init 63 | ``` 64 | 65 | The sample assumes Backblaze B2 is being used as restic storage provider; 66 | replace setting as appropriate for your chosen storage provider. 67 | 68 | Source `.env.restic` from `.bashrc` if you want to be able to run restic 69 | easily from the command line. 70 | 71 | Comment out or remove lines in `all-backups.sh` that you do not need. 72 | For example, if you do not have a MySQL database, comment out the 73 | `./mysql-backup.sh` line. 74 | 75 | ## Files Set Up 76 | 77 | Copy the `sample.files-backup.sh` file to `files-backup.sh`: 78 | 79 | ``` 80 | cp sample.files-backup.sh files-backup.sh 81 | ``` 82 | 83 | Customise the `restic` command lines as necessary: 84 | replace `/where/the/important/files/are` with the path to the 85 | important files you need to backup. 86 | Update or remove the second `restic` command and the lines 87 | mentioning `transient-log-files` if you do not need an alternative 88 | retention policy for transient files. 89 | 90 | ## MySQL Set Up 91 | 92 | Create a MySQL user for the Unix user, and grant the necessary 93 | privileges: 94 | 95 | ``` 96 | create user 'restic'@'localhost'; 97 | grant process on *.* to 'restic'@'localhost'; 98 | grant lock tables, select, show view, event, trigger on app_production.* to 'restic'@'localhost'; 99 | ``` 100 | 101 | The global `PROCESS` privilege is required to use `mysqldump` without the 102 | `--no-tablespaces` option. 103 | 104 | ## PostgreSQL Set Up 105 | 106 | Create a PostgreSQL role for the Unix user, and grant the necessary 107 | privileges. Connecting as the `postgres` user: 108 | 109 | ``` 110 | create role restic with login; 111 | ``` 112 | 113 | For each database to be dumped (`app_production` below): 114 | 115 | ``` 116 | grant connect on database app_production to restic; 117 | \c app_production 118 | set role app_production; 119 | ``` 120 | 121 | (This assumes your data is stored in a database named `app_production`, 122 | and that the role `app_production` owns the schema objects within the 123 | database.) 124 | 125 | Typically all an application's schema objects will be in the `public` schema. 126 | To give `restic` access to these objects run the following commands for the 127 | `public` schema and any additional schemas used in your database. 128 | 129 | ``` 130 | grant usage on schema public to restic; 131 | grant select on all tables in schema public to restic; 132 | alter default privileges in schema public grant select on tables to restic; 133 | grant select on all sequences in schema public to restic; 134 | alter default privileges in schema public grant select on sequences to restic; 135 | ``` 136 | 137 | The `alter default privileges` commands included above will grant the 138 | necessary privileges on schema objects created in the future, 139 | but **only** when those schema objects are created by the `app_production` 140 | role. 141 | 142 | ## Scheduling 143 | 144 | Edit the user's crontab: `crontab -e`. Use a line like this: 145 | 146 | ``` 147 | 30 2 * * * /home/restic/restic-backup-scripts/all-backups.sh 148 | ``` 149 | 150 | ## Healthchecks.io (Optional) 151 | 152 | To use [healthchecks.io](https://healthchecks.io/) to monitor your backups 153 | use the `Makefile` to download a copy of 154 | [runitor](https://github.com/bdd/runitor). 155 | Just run `make` and it should pull a release down. 156 | Update the variables in the Makefile to choose a different platform or version. 157 | 158 | Then use a crontab line like this: 159 | 160 | ``` 161 | 30 2 * * * cd /home/restic/restic-backup-scripts; ./runitor -uuid 2f9-a5c-0123 -silent -- ./all-backups.sh 162 | ``` 163 | 164 | Substitute a valid check UUID from healthchecks.io in the command above. 165 | 166 | ## Dealing With Transient Files 167 | 168 | You might have files that change entirely between backups, such as a log 169 | file that is rotated nightly and compressed a day or so later. 170 | Backing this file up every day will make your restic repository grow 171 | rapidly. 172 | One strategy is to list these transient files in a file that is passed 173 | to restic's `--exclude-file` option, 174 | then run a second backup with an additional `transient` tag passing the same 175 | file to the `--files-from` option. 176 | This is illustrated in the `sample.files-backup.sh` script. 177 | -------------------------------------------------------------------------------- /all-backups.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | cd `dirname $0` 6 | 7 | ./mysql-backup.sh 8 | ./postgresql-backup.sh 9 | ./files-backup.sh 10 | 11 | ./purge.sh --really 12 | # ./push.sh 13 | -------------------------------------------------------------------------------- /common.sh: -------------------------------------------------------------------------------- 1 | set -e -E -o pipefail 2 | 3 | err_exit() { 4 | echo "$0: exit with error on line $1" >&2 5 | } 6 | 7 | trap 'err_exit $LINENO' ERR 8 | 9 | [ -f "$HOME/.env.restic" ] && . $HOME/.env.restic 10 | 11 | if [ -z "$LOGNAME" ] 12 | then 13 | echo LOGNAME must contain the username 14 | exit 2 15 | fi 16 | 17 | if [ -z "$RESTIC_REPOSITORY" -a -z "$RESTIC_ARCHIVE_REPOSITORY" ] 18 | then 19 | echo one of RESTIC_REPOSITORY and RESTIC_ARCHIVE_REPOSITORY 20 | echo must specify the restic repository. 21 | exit 2 22 | fi 23 | 24 | if [ -z "$RESTIC_PASSWORD_FILE" ] 25 | then 26 | echo RESTIC_PASSWORD_FILE must specify the restic repository password. 27 | exit 2 28 | fi 29 | 30 | use_archive_repository() { 31 | if [ -n "$RESTIC_ARCHIVE_REPOSITORY" ] 32 | then 33 | export RESTIC_REPOSITORY=$RESTIC_ARCHIVE_REPOSITORY 34 | fi 35 | } 36 | 37 | use_transient_repository() { 38 | if [ -n "$RESTIC_TRANSIENT_REPOSITORY" ] 39 | then 40 | export RESTIC_REPOSITORY=$RESTIC_TRANSIENT_REPOSITORY 41 | fi 42 | } 43 | 44 | [ -d "log" ] || mkdir log 45 | 46 | LOG="log/$(basename $0)-$(date +%Y-%m-%d).log" 47 | -------------------------------------------------------------------------------- /mysql-backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | . ./common.sh 6 | 7 | TAG=mysql 8 | 9 | use_archive_repository 10 | 11 | for DB in $(mysql -u$LOGNAME -BNe 'show databases' | grep -Ev --line-regexp 'mysql|information_schema|performance_schema|sys') 12 | do 13 | mysqldump -u$LOGNAME --skip-dump-date --force $DB | \ 14 | gzip --rsyncable | \ 15 | restic backup \ 16 | --stdin --stdin-filename mysql/$DB.sql.gz \ 17 | --tag "$TAG" \ 18 | --tag "$DB" 19 | done 20 | -------------------------------------------------------------------------------- /postgresql-backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | . ./common.sh 6 | 7 | TAG=postgresql 8 | 9 | use_archive_repository 10 | 11 | for DB in $(psql -l | \ 12 | awk '{print $1}' | \ 13 | grep -Ev "^(List|Name|-*\+|postgres|template|\||\(|$)") 14 | do 15 | pg_dump --create --clean --if-exists --no-owner --no-privileges $DB | \ 16 | gzip --rsyncable | \ 17 | restic backup \ 18 | --stdin --stdin-filename postgresql/$DB.sql.gz \ 19 | --tag "$TAG" \ 20 | --tag "$DB" 21 | done 22 | -------------------------------------------------------------------------------- /purge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | . ./common.sh 6 | 7 | DRY_RUN=--dry-run 8 | COMPACT='' 9 | 10 | if [ "$1" = '--really' ] 11 | then 12 | DRY_RUN='' 13 | COMPACT=--compact 14 | fi 15 | 16 | forget() { 17 | use_archive_repository 18 | echo forget non-transient files 19 | restic forget $DRY_RUN $COMPACT \ 20 | --tag mysql \ 21 | --tag postgresql \ 22 | --tag files \ 23 | --keep-tag transient \ 24 | --keep-daily 14 \ 25 | --keep-weekly 5 \ 26 | --keep-monthly 4 27 | 28 | use_transient_repository 29 | if [ -n "$RESTIC_REPOSITORY" ] 30 | then 31 | echo forget transient files 32 | # Don't group transient files by path as the paths may change from one 33 | # snapshot to the next (e.g. paths with dates in them). 34 | restic forget $DRY_RUN $COMPACT \ 35 | --group-by host \ 36 | --tag transient \ 37 | --keep-daily 2 38 | fi 39 | } 40 | 41 | prune() { 42 | use_archive_repository 43 | restic prune 44 | use_transient_repository 45 | test -n "$RESTIC_REPOSITORY" && restic prune 46 | } 47 | 48 | if [ "$1" = '--really' ] 49 | then 50 | forget >$LOG 51 | egrep '^(forget|Applying|(keep|remove) [[:digit:]]+ snapshot)' $LOG 52 | prune 53 | else 54 | forget 55 | fi 56 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | . ./common.sh 6 | 7 | RCLONE=./rclone 8 | OPTS='--transfers 32' 9 | 10 | if [[ "$RESTIC_ARCHIVE_REPOSITORY" =~ ^/ ]] && \ 11 | [[ -n "$RCLONE_ARCHIVE_REPOSITORY" ]] 12 | then 13 | $RCLONE $OPTS sync $RESTIC_ARCHIVE_REPOSITORY $RCLONE_ARCHIVE_REPOSITORY \ 14 | --create-empty-src-dirs 15 | fi 16 | 17 | if [[ "$RESTIC_TRANSIENT_REPOSITORY" =~ ^/ ]] && \ 18 | [[ "$RESTIC_TRANSIENT_REPOSITORY" != "$RESTIC_ARCHIVE_REPOSITORY" ]] && \ 19 | [[ -n "$RCLONE_TRANSIENT_REPOSITORY" ]] 20 | then 21 | $RCLONE $OPTS sync $RESTIC_TRANSIENT_REPOSITORY $RCLONE_TRANSIENT_REPOSITORY \ 22 | --create-empty-src-dirs 23 | fi 24 | -------------------------------------------------------------------------------- /sample.env.restic: -------------------------------------------------------------------------------- 1 | PATH=:$PATH 2 | PATH=$HOME/bin${PATH//:$HOME\/bin/} 3 | 4 | ### repository on local filesystem 5 | # export RESTIC_ARCHIVE_REPOSITORY='/raid/restic/archive' 6 | # export RESTIC_TRANSIENT_REPOSITORY='/raid/restic/transient' 7 | 8 | ### repository on B2 using key name: xxx 9 | # export B2_ACCOUNT_ID='xxx' 10 | # export B2_ACCOUNT_KEY='xxx' 11 | # export RESTIC_ARCHIVE_REPOSITORY='b2:bucket:path' 12 | 13 | export RESTIC_REPOSITORY=$RESTIC_ARCHIVE_REPOSITORY 14 | export RESTIC_PASSWORD_FILE='/home/restic/.restic.pwd' 15 | 16 | ### if using rclone through the push.sh script to replicate local filesystem 17 | ### repositories to a remote storage service like B2, set the following to 18 | ### the rclone remote, bucket and path to sync to. 19 | # export RCLONE_ARCHIVE_REPOSITORY='b2:bucket/path' 20 | # export RCLONE_TRANSIENT_REPOSITORY='b2:bucket/path' 21 | -------------------------------------------------------------------------------- /sample.files-backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd `dirname $0` 4 | 5 | . ./common.sh 6 | 7 | TAG=files 8 | 9 | use_archive_repository 10 | 11 | restic backup \ 12 | --tag "$TAG" \ 13 | /where/the/important/files/are \ 14 | --exclude .../shared/log/transient-log-files \ 15 | --exclude-file .../shared/log/transient-log-files 16 | 17 | use_transient_repository 18 | 19 | test -n "$RESTIC_REPOSITORY" && \ 20 | restic backup \ 21 | --tag "$TAG" \ 22 | --tag "transient" \ 23 | --files-from .../shared/log/transient-log-files 24 | --------------------------------------------------------------------------------