├── README.md ├── backup.sh └── restore.sh /README.md: -------------------------------------------------------------------------------- 1 | # mailcow backup with borg 🐮🐋 + 🤖 = 💕 2 | 3 | Thanks to these awesome projects! 4 | - [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized) 5 | - [borgbackup](https://github.com/borgbackup/borg) 6 | 7 | Unlike the provided `helper-scripts/backup_and_restore.sh`, this one uses borgbackup which works incrementally and deduplicating and does not need to tar everything at every single backup. 8 | Means it is a lot faster and saves a huge amount of storage space! 9 | 10 | Example: for about 1.5 GB of mail storage the whole script needs only about 25 seconds to back up the data to a remote server. 11 | 12 | 13 | ## Backup 14 | This script disables dovecot and postfix at first and lets nginx just return a 503-Error, to ensure no files will be changed during backup process. 15 | Next it prepares the backup-data (database dump, etc.), when done triggers borgbackup itself and saves all the data. 16 | Afterwards the stopped services are started again and temporary files are cleaned up. 17 | 18 | Its pretty easy to use: adjust (system and borg variables) to your needs and run manually or as a cronjob (example in script) - while mailcow is running. 19 | If you have added or customized some mailcow files or configurations just add them to the borg create command. 20 | 21 | Dont forget to prune your borg repository from time to time. For example an extra cron on the borg-host-system - if external. 22 | 23 | 24 | ## Restore 25 | **_! not finally tested for production yet_** 26 | 27 | Steps to restore: 28 | - adjust the script-settings-variables 29 | - extract your borg repository snapshot completely into `restoreVolumesFrom="/path/to/extracted/borgbackup"` 30 | - install mailcow as [shown](https://mailcow.github.io/mailcow-dockerized-docs/i_u_m_install/) 31 | - adjust paths in new mailcow.conf as in old installation 32 | - start mailcow once, to create all required files and directories and stop it again\ 33 | `docker-compose pull`, `docker-compose up`, wait a few minutes until setup is done, `control+c` 34 | - run restore.sh when containers are completely downed 35 | - start mailcow again and check if everything worked 36 | 37 | 38 | 39 | --- 40 | 41 | No liability for any damage to the host/system or data loss! 42 | The use of the software is at your own risk. 43 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################### 4 | # # 5 | # Mailcow Borg Backup # 6 | # # 7 | ############################### 8 | # Author: Matthis B. # 9 | # Created: 20201105 # 10 | # Lastchange: 20201119 # 11 | ############################### 12 | # Changelog: # 13 | # - 20201105: init # 14 | # - 20201107: small changes # 15 | # - 20201119: bug fixes # 16 | ############################### 17 | 18 | ## 19 | ## Cronjob: 20 | ## 7 4 * * * /home/path/to/backup.sh > /home/path/to/backup/log/mailcow_sys_$(date +\%Y-\%m-\%d-\%H-\%M-\%S).log 2>&1 21 | ## 22 | 23 | # 24 | # Functions 25 | # 26 | getDate() { echo $(date +'%d.%m.%Y %H:%M:%S'); } 27 | info() { printf "\n- %s %s\n\n" "$( getDate )" "$*" >&2; } 28 | trap "echo $( getDate ) Backup interrupted >&2; exit 2" INT TERM 29 | 30 | 31 | # 32 | # Settings 33 | # 34 | 35 | # System vars 36 | workDirectory='/opt/mailcow-dockerized' # path to docker-compose.yml 37 | 38 | logDirectory='/home/path/to/backup/log' # path top log archive (keep in mind to change path in cron also) 39 | logKeepAmount='10' # how many logfiles should be kept 40 | 41 | 42 | # Borg env vars 43 | BORG_PREFIX='mx10' 44 | 45 | export BORG_REPO='ssh://borguser@123.123.123.123:22/repo/path' 46 | export BORG_PASSPHRASE='passw0rd' 47 | 48 | export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes 49 | export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes 50 | 51 | 52 | # autocatch vars 53 | source "${workDirectory}/mailcow.conf" 54 | CMPS_PRJ=$(echo $COMPOSE_PROJECT_NAME | tr -cd "[A-Za-z-_]") 55 | 56 | volumeVMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_vmail-vol-1) 57 | volumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_crypt-vol-1) 58 | volumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_redis-vol-1) 59 | volumeRSpamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_rspamd-vol-1) 60 | volumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_postfix-vol-1) 61 | volumeMySQL=$(docker volume inspect -f '{{ .Mountpoint }}' ${CMPS_PRJ}_mysql-vol-1) 62 | 63 | 64 | 65 | # 66 | # Prerequisites 67 | # 68 | 69 | # check for root 70 | if [[ "$(id -u)" != "0" ]] ; then 71 | info "ERROR: no root!" 72 | exit 1 73 | fi 74 | 75 | # check project name 76 | if [[ -z "${CMPS_PRJ}" ]] ; then 77 | info "ERROR: empty docker-project-name" 78 | exit 1 79 | fi 80 | 81 | # create directories if not already done 82 | if [[ ! -d "${logDirectory}" ]] ; then 83 | if ! mkdir -p "${logDirectory}" ; then 84 | info "ERROR: could not create logDirectory ($logDirectory)" 85 | exit 1 86 | fi 87 | fi 88 | 89 | 90 | 91 | # 92 | # Pre-BackupProcess 93 | # 94 | startTime=$(date +%s) 95 | 96 | echo 97 | echo "==================================================" 98 | info "start backup" 99 | 100 | 101 | # clean old stuff 102 | echo "-- clean up temp/log" 103 | countLogs=$(ls -l ${logDirectory}/*.log 2>/dev/null | wc -l) 104 | echo "--- found $countLogs old log files" 105 | if (( "$countLogs" > "$logKeepAmount" )) ; then 106 | echo "--- delete old logfiles up to the last $logKeepAmount .." 107 | ls -dt ${logDirectory}/*.log | tail -n "+$((logKeepAmount+1))" | xargs rm -v 108 | fi 109 | 110 | echo 111 | echo "-- pre borg stuff" 112 | 113 | # ensure no more changes are made (e.g. send/receive mails, WebUI changes) 114 | echo "--- mailcow: stop services" 115 | echo "---- stop dovecot" 116 | dovecot_id=$(docker stop "$(docker ps -qf name=dovecot-mailcow)") 117 | if [[ "$(docker inspect -f '{{ .State.ExitCode }}' "$dovecot_id")" == "0" && "$(docker inspect -f '{{ .State.Running }}' "$dovecot_id")" == "false" ]] ; then 118 | echo "----- success" 119 | else 120 | echo "----- FAILED" 121 | fi 122 | echo "---- stop postfix" 123 | postfix_id=$(docker stop "$(docker ps -qf name=postfix-mailcow)") 124 | if [[ "$(docker inspect -f '{{ .State.ExitCode }}' "$postfix_id")" == "0" && "$(docker inspect -f '{{ .State.Running }}' "$postfix_id")" == "false" ]] ; then 125 | echo "----- success" 126 | else 127 | echo "----- FAILED" 128 | fi 129 | 130 | # show error on webstuff 131 | echo "--- nginx return service unavailable" 132 | echo "return 503;" > "${workDirectory}/data/conf/nginx/site.backup.custom" 133 | nginx_id=$(docker restart "$(docker ps -qf name=nginx-mailcow)") 134 | if [[ "$(docker inspect -f '{{ .State.Running }}' "$nginx_id")" == "true" ]] ; then 135 | echo "---- success" 136 | else 137 | echo "---- FAILED" 138 | fi 139 | 140 | # backup database: mysql 141 | echo "--- mysql-dump" 142 | if [[ -d "${volumeMySQL}/tmp_backup" ]] ; then 143 | echo "---- WARN: tmp backup dir already exists" 144 | rm -r "${volumeMySQL}/tmp_backup" 145 | if [[ -d "${volumeMySQL}/tmp_backup" ]] ; then 146 | echo "---- FAILED: could not delete" 147 | fi 148 | fi 149 | mysql_id=$(docker ps -qf 'name=mysql-mailcow') 150 | docker exec ${mysql_id} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} \ 151 | --backup --rsync --target-dir=/var/lib/mysql/tmp_backup ; \ 152 | mariabackup --prepare --target-dir=/var/lib/mysql/tmp_backup ; \ 153 | chown -R 999:999 /var/lib/mysql/tmp_backup ;" > /dev/null 2>&1 154 | if [[ -d "${volumeMySQL}/tmp_backup" ]] ; then 155 | echo "---- success" 156 | else 157 | echo "---- FAILED: could not create backup" 158 | fi 159 | 160 | # backup database: redis 161 | echo "--- redis-dump" 162 | redis_id=$(docker ps -qf name=redis-mailcow) 163 | redis_dump=$(docker exec $redis_id redis-cli save) 164 | if [[ "$redis_dump" == "OK" ]]; then 165 | echo "---- success" 166 | else 167 | echo "---- FAILED: $redisdump" 168 | fi 169 | 170 | 171 | 172 | # 173 | # BackupProcess 174 | # 175 | 176 | # starting borg 177 | echo 178 | echo "-- start syncing files" 179 | echo 180 | 181 | thisDir="$( cd $( dirname ${BASH_SOURCE[0]} ) >/dev/null 2>&1 && pwd )" 182 | thisFile="$(basename ${0})" 183 | 184 | borg create \ 185 | --show-rc \ 186 | --verbose \ 187 | --stats \ 188 | --compression lz4 \ 189 | --exclude-caches \ 190 | ::"${BORG_PREFIX}-{now:%Y-%m-%d_%H:%M:%S}" \ 191 | "${workDirectory}/.env" \ 192 | "${workDirectory}/docker-compose.yml" \ 193 | "${workDirectory}/mailcow.conf" \ 194 | "${volumeVMail}" \ 195 | "${volumeCrypt}" \ 196 | "${volumeRedis}" \ 197 | "${volumeRSpamd}" \ 198 | "${volumePostfix}" \ 199 | "${volumeMySQL}/tmp_backup" \ 200 | "${thisDir}/${thisFile}" 201 | 202 | borg_create_exit=$? 203 | 204 | # check state 205 | echo 206 | echo "-- borg finished" 207 | if [[ "${borg_create_exit}" == "0" ]] ; then 208 | echo "--- success" 209 | elif [[ "${borg_create_exit}" == "1" ]] ; then 210 | echo "--- WARN: 1" 211 | else 212 | echo "--- FAILED: ${borg_create_exit}" 213 | fi 214 | 215 | 216 | 217 | # 218 | # Post-BackupProcess 219 | # 220 | 221 | echo 222 | echo "-- post borg stuff" 223 | 224 | echo "--- mailcow: re/start services" 225 | echo "---- start dovecot" 226 | dovecot_id=$(docker start "$(docker ps -aqf name=dovecot-mailcow)") 227 | if [[ "$(docker inspect -f '{{ .State.Running }}' "$dovecot_id")" == "true" ]] ; then 228 | echo "----- success" 229 | else 230 | echo "----- FAILED" 231 | fi 232 | echo "---- start postfix" 233 | postfix_id=$(docker start "$(docker ps -aqf name=postfix-mailcow)") 234 | if [[ "$(docker inspect -f '{{ .State.Running }}' "$postfix_id")" == "true" ]] ; then 235 | echo "----- success" 236 | else 237 | echo "----- FAILED" 238 | fi 239 | 240 | # show error on webstuff 241 | echo "--- nginx normal mode" 242 | if [[ -f "${workDirectory}/data/conf/nginx/site.backup.custom" ]] ; then 243 | rm "${workDirectory}/data/conf/nginx/site.backup.custom" 244 | fi 245 | nginx_id=$(docker restart "$(docker ps -qf name=nginx-mailcow)") 246 | if [[ "$(docker inspect -f '{{ .State.Running }}' "$nginx_id")" == "true" ]] ; then 247 | echo "---- success" 248 | else 249 | echo "---- FAILED" 250 | fi 251 | 252 | # clean tmp stuff 253 | echo "--- clean up" 254 | 255 | if [[ -d "${volumeMySQL}/tmp_backup" ]] ; then 256 | rm -r "${volumeMySQL}/tmp_backup" 257 | echo "---- removed MySQL tmp_backup dir" 258 | fi 259 | 260 | 261 | # calc backup duration 262 | endTime=$(date +%s) 263 | duration=$((endTime-startTime)) 264 | echo 265 | echo "-- backup duration: $(printf '%02d hours %02d minutes %02d seconds' $((duration / 3600)) $(((duration / 60) % 60)) $((duration % 60)))" 266 | info "-> everything done: exit" 267 | 268 | exit ${borg_create_exit} 269 | -------------------------------------------------------------------------------- /restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################### 4 | # # 5 | # Mailcow Borg Restore # 6 | # # 7 | ############################### 8 | # Author: Matthis B. # 9 | # Created: 20201106 # 10 | # Lastchange: 20201107 # 11 | ############################### 12 | # Changelog: # 13 | # - 20201106: init # 14 | # - 20201107: small changes # 15 | ############################### 16 | 17 | # 18 | # Settings 19 | # 20 | 21 | dockerDir="/opt/mailcow-dockerized" # path to docker-compose.yml 22 | 23 | restoreVolumesFrom="/path/to/extracted/borgbackup" # path to EXTRACTED backup directory 24 | restoreVolumesTo="/var/lib/docker/volumes" # path to volumes directory 25 | 26 | 27 | dirnameVMail="mailcowdockerized_vmail-vol-1" 28 | dirnameCrypt="mailcowdockerized_crypt-vol-1" 29 | dirnameRedis="mailcowdockerized_redis-vol-1" 30 | dirnameRSpamd="mailcowdockerized_rspamd-vol-1" 31 | dirnamePostfix="mailcowdockerized_postfix-vol-1" 32 | dirnameMySQL="mailcowdockerized_mysql-vol-1" 33 | 34 | 35 | 36 | # 37 | # Pre-Restore 38 | # 39 | if [[ "$(id -u)" != "0" ]] ; then 40 | echo "- ERROR: no root!" 41 | exit 1 42 | fi 43 | if [[ ! -d "${dockerDir}" ]] ; then 44 | echo "- ERROR: docker directory does not exist (${dockerDir})!" 45 | exit 1 46 | fi 47 | if [[ ! -d "${restoreVolumesFrom}" ]] ; then 48 | echo "- ERROR: directory to restore data from does not exist (${restoreVolumesFrom})!" 49 | exit 1 50 | fi 51 | if [[ ! -d "${restoreVolumesTo}" ]] ; then 52 | echo "- ERROR: volumes directory does not exist (${restoreVolumesTo})!" 53 | echo "- HINT: before running this script you have to install and start+stop mailcow once!" 54 | exit 1 55 | fi 56 | volumeDirs=("$dirnameVMail" "$dirnameCrypt" "$dirnameRedis" "$dirnameRSpamd" "$dirnamePostfix" "$dirnameMySQL") 57 | for dir in "${volumeDirs[@]}" ; do 58 | dir="${restoreVolumesTo}/${dir}" 59 | if [[ ! -d "$dir" ]] ; then 60 | echo "- ERROR: volume directory does not exist (${dir})!" 61 | echo "- HINT: before running this script you have to install and start+stop mailcow once!" 62 | exit 1 63 | fi 64 | done 65 | 66 | 67 | # 68 | # Restore 69 | # 70 | echo "- start restoring data" 71 | 72 | echo 73 | echo "-- vmail" 74 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnameVMail}/ -> ${restoreVolumesTo}/${dirnameVMail}/" 75 | rm -rf "${restoreVolumesTo}/${dirnameVMail}/"* 76 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnameVMail}/" "${restoreVolumesTo}/${dirnameVMail}/" 77 | 78 | echo 79 | echo "-- crypt" 80 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnameCrypt}/ -> ${restoreVolumesTo}/${dirnameCrypt}/" 81 | rm -rf "${restoreVolumesTo}/${dirnameCrypt}/"* 82 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnameCrypt}/" "${restoreVolumesTo}/${dirnameCrypt}/" 83 | 84 | echo 85 | echo "-- redis" 86 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnameRedis}/ -> ${restoreVolumesTo}/${dirnameRedis}/" 87 | rm -rf "${restoreVolumesTo}/${dirnameRedis}/"* 88 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnameRedis}/" "${restoreVolumesTo}/${dirnameRedis}/" 89 | 90 | echo 91 | echo "-- rspamd" 92 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnameRSpamd}/ -> ${restoreVolumesTo}/${dirnameRSpamd}/" 93 | rm -rf "${restoreVolumesTo}/${dirnameRSpamd}/"* 94 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnameRSpamd}/" "${restoreVolumesTo}/${dirnameRSpamd}/" 95 | 96 | echo 97 | echo "-- postfix" 98 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnamePostfix}/ -> ${restoreVolumesTo}/${dirnamePostfix}/" 99 | rm -rf "${restoreVolumesTo}/${dirnamePostfix}/"* 100 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnamePostfix}/" "${restoreVolumesTo}/${dirnamePostfix}/" 101 | 102 | echo 103 | echo "-- mysql" 104 | echo "--- ${restoreVolumesFrom}${restoreVolumesTo}/${dirnameMySQL}/_data/tmp_backup/ -> ${restoreVolumesTo}/${dirnameMySQL}/_data/" 105 | rm -rf "${restoreVolumesTo}/${dirnameMySQL}/"* 106 | rsync -axh --stats "${restoreVolumesFrom}${restoreVolumesTo}/${dirnameMySQL}/_data/tmp_backup/" "${restoreVolumesTo}/${dirnameMySQL}/_data/" 107 | 108 | echo 109 | echo "-- mailcow" 110 | echo "--- ${restoreVolumesFrom}/${dockerDir}/ -> ${dockerDir}/" 111 | rsync -axh --stats "${restoreVolumesFrom}${dockerDir}/" "${dockerDir}/" 112 | 113 | 114 | 115 | # 116 | # Pre-Restore 117 | # 118 | 119 | echo 120 | echo "- Done!" 121 | echo "- You should be able to start your mailcow as usual (docker-compose up -d)." 122 | echo "- Have fun :)" 123 | --------------------------------------------------------------------------------