├── .gitignore ├── license.md ├── readme.md └── restic-setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Working Concept Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Forge Backup 2 | 3 | This is a shell script for establishing encrypted, efficient backups of your [Laravel Forge](https://forge.laravel.com) server on cheap [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html) storage. For a more thorough overview, see my [Easy Forge Backups with Restic and Backblaze B2](https://workingconcept.com/blog/forge-backups-restic-backblaze-b2) blog post. 4 | 5 | **This is a free script with no support or warranty. You're responsible for whatever joy or sadness it brings you!** 6 | 7 | ## Requirements 8 | 9 | - Server provisioned with Laravel Forge running Ubuntu 18+. 10 | - Backblaze account. 11 | 12 | ## What It Does 13 | 14 | - Installs [restic](https://restic.net/), a free, secure, and efficient backup program. 15 | - Creates a read-only `backup` MySQL user for generating dumps. 16 | - Adds shell scripts: 17 | - `mysql-backup.sh` for generating and pruning dumps in `/home/forge/backup/mysql` 18 | - `restic-backup.sh` for running database dumps and backing up `/home/forge` to B2 19 | - `restic-mount.sh` for locally mounting and browsing+verifying your backups 20 | - Stores configuration and shell scripts in `/root/restic`. 21 | 22 | ## Setup 23 | 24 | Create a Backblaze B2 bucket for your server, and an application key limited to that new bucket. 25 | 26 | Run the installer **as root (not sudo!)** On its initial run, it will ask for B2 credentials and your `forge` database password to create the `backup` database user. 27 | 28 | ``` 29 | sudo bash 30 | curl -O https://raw.githubusercontent.com/workingconcept/forge-backup/master/restic-setup.sh && chmod +x restic-setup.sh && ./restic-setup.sh 31 | ``` 32 | 33 | ## Scheduling 34 | 35 | Once the restic repository is established, try running a few backups and mounting them with `restic-mount.sh` to be sure everything looks good. If it does, automate backups by adding a scheduled job that runs as `root`: 36 | 37 | ``` 38 | /root/restic/restic-backup.sh >/dev/null 2>&1 39 | ``` 40 | 41 | A custom value of `0 3 * * *` will run every day at 3am. 42 | 43 | Optionally run the MySQL backup on its own interval: 44 | 45 | ``` 46 | /root/restic/mysql-backup.sh >/dev/null 2>&1 47 | ``` 48 | -------------------------------------------------------------------------------- /restic-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################### 4 | # 5 | # Forge → B2 Backup 6 | # Backs up all databases and /home/forge to 7 | # Backblaze B2 using restic. 8 | # 9 | # by Matt Stein 10 | # No warranty provided, use at your own risk! 11 | # 12 | ############################################### 13 | # Instructions 14 | ############################################### 15 | # 16 | # 1. Create a B2 bucket and application key. 17 | # 2. Run this script as root. (Not sudo.) 18 | # 3. Run a mysql backup with mysql-backup.sh. 19 | # 4. Run a backup with restic-backup.sh. 20 | # 5. Use restic-mount.sh to mount and verify. 21 | # 6. Add scheduler job: 22 | # `/root/restic/restic-backup.sh >/dev/null 2>&1` 23 | # 24 | ############################################### 25 | 26 | echo "" 27 | echo "----------------------------------------" 28 | echo "Forge → B2 Backup" 29 | echo "----------------------------------------" 30 | echo "" 31 | 32 | if [ ! -f /root/restic/conf/b2.conf ]; then 33 | read -s -p "Enter B2 application key ID: " B2_ACCOUNT_ID 34 | echo "" 35 | read -s -p "Enter B2 application key: " B2_ACCOUNT_KEY 36 | echo "" 37 | read -s -p "Enter B2 bucket: " B2_BUCKET 38 | echo "" 39 | fi 40 | 41 | # location for local backup data 42 | BACKUP_DIR="/home/forge/backup" 43 | 44 | # location of files to back up 45 | BACKUP_TARGET="/home/forge" 46 | 47 | # MySQL backup user (password will be generated) 48 | MYSQL_USER="backup" 49 | TIMESTAMP=$(date +"%F") 50 | MYSQL=/usr/bin/mysql 51 | MYSQLDUMP=/usr/bin/mysqldump 52 | 53 | echo "" 54 | echo "----------------------------------------" 55 | echo "Installing restic..." 56 | echo "----------------------------------------" 57 | echo "" 58 | 59 | apt-get install restic 60 | 61 | if [ ! -d /root/restic ]; then 62 | echo "----------------------------------------" 63 | echo "Creating /root/restic..." 64 | echo "----------------------------------------" 65 | 66 | # create directories we'll need 67 | mkdir -p /root/restic 68 | mkdir -p /root/restic/conf 69 | mkdir -p $BACKUP_DIR 70 | chown -R forge:forge $BACKUP_DIR 71 | fi 72 | 73 | echo "" 74 | echo "----------------------------------------" 75 | echo "Confirming MySQL setup..." 76 | echo "----------------------------------------" 77 | echo "" 78 | 79 | # create or source backup user MySQL password 80 | if [ ! -f /root/restic/conf/mysql.conf ]; then 81 | read -s -p "Enter MySQL password for 'forge': " ROOT_MYSQL_PASSWORD 82 | 83 | # generate random 32 character alphanumeric string (upper and lowercase) 84 | MYSQL_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 85 | touch /root/restic/conf/mysql.conf 86 | echo "export MYSQL_PASSWORD=\"$MYSQL_PASSWORD\"" >> /root/restic/conf/mysql.conf 87 | echo "export MYSQL_USER=\"$MYSQL_USER\"" >> /root/restic/conf/mysql.conf 88 | echo "export MYSQL=\"$MYSQL\"" >> /root/restic/conf/mysql.conf 89 | echo "export MYSQLDUMP=\"$MYSQLDUMP\"" >> /root/restic/conf/mysql.conf 90 | echo "export BACKUP_DIR=\"$BACKUP_DIR\"" >> /root/restic/conf/mysql.conf 91 | 92 | # create read-only mysql backup user and store credentials 93 | $MYSQL --user="forge" --password="$ROOT_MYSQL_PASSWORD" --execute="CREATE USER '${MYSQL_USER}'@'localhost' IDENTIFIED BY '${MYSQL_PASSWORD}';" 94 | $MYSQL --user="forge" --password="$ROOT_MYSQL_PASSWORD" --execute="GRANT SELECT, LOCK TABLES ON *.* TO '${MYSQL_USER}'@'localhost';" 95 | $MYSQL --user="forge" --password="$ROOT_MYSQL_PASSWORD" --execute="FLUSH PRIVILEGES;" 96 | 97 | echo "!! created backup user with password: $MYSQL_PASSWORD" 98 | else 99 | echo "✓ found MySQL backup password" 100 | source /root/restic/conf/mysql.conf 101 | fi 102 | 103 | echo "" 104 | echo "----------------------------------------" 105 | echo "Confirming restic repository..." 106 | echo "----------------------------------------" 107 | echo "" 108 | 109 | # write restic backup exclude file if one doesn't exist 110 | if [ ! -f /root/restic/conf/excludes.conf ]; then 111 | touch /root/restic/conf/excludes.conf 112 | echo ".git/*" > /root/restic/conf/excludes.conf 113 | echo "created /root/restic/conf/excludes.conf" 114 | else 115 | echo "✓ found backup exclude file" 116 | fi 117 | 118 | if [ ! -f /root/restic/conf/b2.conf ]; then 119 | # create B2 settings file 120 | touch /root/restic/conf/b2.conf 121 | 122 | echo "export B2_ACCOUNT_ID=\"$B2_ACCOUNT_ID\"" >> /root/restic/conf/b2.conf 123 | echo "export B2_ACCOUNT_KEY=\"$B2_ACCOUNT_KEY\"" >> /root/restic/conf/b2.conf 124 | echo "export B2_BUCKET=\"$B2_BUCKET\"" >> /root/restic/conf/b2.conf 125 | echo "wrote B2 settings" 126 | else 127 | echo "✓ found B2 settings" 128 | fi 129 | 130 | # create or source restic password 131 | if [ ! -f /root/restic/conf/password.conf ]; then 132 | # generate random 32 character alphanumeric string (upper and lowercase) 133 | RESTIC_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 134 | 135 | touch /root/restic/conf/password.conf 136 | echo $RESTIC_PASSWORD > /root/restic/conf/password.conf 137 | echo "!! generated restic repository password: $RESTIC_PASSWORD" 138 | else 139 | echo "✓ found restic password" 140 | fi 141 | 142 | # save MySQL backup script 143 | cat >/root/restic/mysql-backup.sh <<'EOL' 144 | #! /bin/bash 145 | 146 | echo "" 147 | echo "----------------------------------------" 148 | echo "Running MySQL backup routine..." 149 | echo "----------------------------------------" 150 | echo "" 151 | 152 | source /root/restic/conf/mysql.conf 153 | 154 | DATESTAMP=$(date +"%F") 155 | TIMESTAMP=$(date +"%H%M%S") 156 | 157 | mkdir -p $BACKUP_DIR/mysql/$DATESTAMP 158 | 159 | databases=`$MYSQL --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --execute="SHOW DATABASES;" | grep -Ev "(Database|information_schema)"` 160 | 161 | for db in $databases; do 162 | if [ $db != "performance_schema" ]&&[ $db != "mysql" ];then 163 | FILENAME=$BACKUP_DIR/mysql/$DATESTAMP/$db-$TIMESTAMP.gz 164 | 165 | echo -e "backing up '$db' → $FILENAME" 166 | 167 | # with GZIP 168 | $MYSQLDUMP --force --opt --user=$MYSQL_USER -p$MYSQL_PASSWORD --databases $db | gzip > "$FILENAME" 169 | 170 | # let the forge user inspect backups 171 | chown forge:forge $FILENAME 172 | fi 173 | done 174 | 175 | echo "" 176 | echo "----------------------------------------" 177 | echo "Pruning old backups..." 178 | echo "----------------------------------------" 179 | echo "" 180 | 181 | # prune files more than 7 days old 182 | find $BACKUP_DIR/mysql/ -mtime +7 -name '*.gz' -execdir rm -- '{}' \; 183 | 184 | echo "Done." 185 | EOL 186 | 187 | # save restic backup script 188 | cat >/root/restic/restic-backup.sh <<'EOL' 189 | #/bin/bash 190 | 191 | # load b2.conf environment variables into our session 192 | . /root/restic/conf/b2.conf 193 | . /root/restic/conf/mysql.conf 194 | 195 | echo "" 196 | echo "----------------------------------------" 197 | echo "Running restic backup → $B2_BUCKET..." 198 | echo "----------------------------------------" 199 | echo "" 200 | 201 | # create a mysql backup 202 | /root/restic/mysql-backup.sh 203 | 204 | /usr/bin/restic -r b2:$B2_BUCKET:/ backup /home/forge --exclude-file=/root/restic/conf/excludes.conf --password-file=/root/restic/conf/password.conf 205 | /usr/bin/restic -r b2:$B2_BUCKET:/ --password-file=/root/restic/conf/password.conf forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 206 | /usr/bin/restic -r b2:$B2_BUCKET:/ --password-file=/root/restic/conf/password.conf prune 207 | /usr/bin/restic -r b2:$B2_BUCKET:/ --password-file=/root/restic/conf/password.conf check 208 | EOL 209 | 210 | # save restic mount script 211 | cat >/root/restic/restic-mount.sh <<'EOL' 212 | #/bin/bash 213 | 214 | echo "----------------------------------------" 215 | echo "Mounting backup at /mnt/restic..." 216 | echo "----------------------------------------" 217 | 218 | mkdir /mnt/restic 219 | . /root/restic/conf/b2.conf 220 | restic -r b2:$B2_BUCKET mount /mnt/restic --password-file=/root/restic/conf/password.conf 221 | echo "Unmount with 'umount /mnt/restic' when you're done!" 222 | EOL 223 | 224 | # make scripts executable 225 | chmod +x /root/restic/mysql-backup.sh 226 | chmod +x /root/restic/restic-backup.sh 227 | chmod +x /root/restic/restic-mount.sh 228 | 229 | # init backup 230 | # ``` 231 | # source /root/restic/conf/b2.conf 232 | # restic -r b2:$B2_BUCKET:/ init 233 | # ``` 234 | while true; do 235 | echo "" 236 | read -p "Do you want to initialize the restic repo now? [y/n] " yn 237 | echo "" 238 | case $yn in 239 | [Yy]* ) source /root/restic/conf/b2.conf && restic -r b2:$B2_BUCKET:/ init; break;; 240 | [Nn]* ) exit;; 241 | * ) echo "Please answer yes or no.";; 242 | esac 243 | done 244 | 245 | echo "" 246 | echo "----------------------------------------" 247 | echo "Looking good!" 248 | echo "Don't forget to finish setting up restic!" 249 | echo "" 250 | echo "- [ ] run a MySQL backup with mysql-backup.sh" 251 | echo "- [ ] run a filesystem backup with restic-backup.sh" 252 | echo "- [ ] mount and verify backups with restic-mount.sh" 253 | echo "- [ ] add scheduler job for restic-backup.sh" 254 | echo "----------------------------------------" 255 | 256 | --------------------------------------------------------------------------------