├── docker-compose.test.broken.yml ├── .github ├── dependabot.yml └── workflows │ └── docker-publish.yml ├── run.sh ├── config-broken.yml ├── entry.sh ├── CHANGELOG.md ├── Dockerfile ├── check.sh ├── backup.sh ├── LICENSE └── README.md /docker-compose.test.broken.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | sut: 5 | image: gcr.io/gcp-runtimes/container-structure-test 6 | command: ["test", "--image", "restic-backup", "--config", "config.yml"] 7 | volumes: 8 | - ./config.yml:/config.yml 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Docs: 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: {interval: monthly} 8 | 9 | - package-ecosystem: docker 10 | directory: / 11 | schedule: {interval: monthly} 12 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Removing old container names 'backup-test' if exists" 4 | docker rm -f -v backup-test || true 5 | 6 | echo "Start backup-test container. Backup of ~/test-data/ to repository ~/test-repo/ every minute" 7 | docker run --privileged --name backup-test \ 8 | -e "RESTIC_PASSWORD=test" \ 9 | -e "RESTIC_TAG=test" \ 10 | -e "BACKUP_CRON=* * * * *" \ 11 | -e "RESTIC_FORGET_ARGS=--keep-last 10" \ 12 | -v ~/test-data:/data \ 13 | -v ~/test-repo/:/mnt/restic \ 14 | -t restic-backup 15 | 16 | 17 | -------------------------------------------------------------------------------- /config-broken.yml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.0.0' 2 | commandTests: 3 | - name: "restic package installation" 4 | setup: [["/entry.sh"]] 5 | command: "which" 6 | args: ["restic"] 7 | expectedOutput: ["/bin/restic"] 8 | fileExistenceTests: 9 | - name: 'log directory exists' 10 | path: '/var/log' 11 | shouldExist: true 12 | - name: 'cron log file exists' 13 | path: '/var/log/cron.log' 14 | shouldExist: true 15 | - name: 'backup script exists' 16 | path: '/bin/backup' 17 | shouldExist: true 18 | metadataTest: 19 | volumes: ["/data"] 20 | entrypoint: ["/entry.sh"] 21 | cmd: ["tail","-fn0", "/var/log/cron.log"] 22 | -------------------------------------------------------------------------------- /entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Starting container ..." 4 | 5 | if [ -n "${NFS_TARGET}" ]; then 6 | echo "Mounting NFS based on NFS_TARGET: ${NFS_TARGET}" 7 | mount -o nolock -v ${NFS_TARGET} /mnt/restic 8 | fi 9 | 10 | restic snapshots ${RESTIC_INIT_ARGS} &>/dev/null 11 | status=$? 12 | echo "Check Repo status $status" 13 | 14 | if [ $status != 0 ]; then 15 | echo "Restic repository '${RESTIC_REPOSITORY}' does not exists. Running restic init." 16 | restic init ${RESTIC_INIT_ARGS} 17 | 18 | init_status=$? 19 | echo "Repo init status $init_status" 20 | 21 | if [ $init_status != 0 ]; then 22 | echo "Failed to init the repository: '${RESTIC_REPOSITORY}'" 23 | exit 1 24 | fi 25 | fi 26 | 27 | 28 | 29 | echo "Setup backup cron job with cron expression BACKUP_CRON: ${BACKUP_CRON}" 30 | echo "${BACKUP_CRON} /usr/bin/flock -n /var/run/backup.lock /bin/backup >> /var/log/cron.log 2>&1" > /var/spool/cron/crontabs/root 31 | 32 | # If CHECK_CRON is set we will enable automatic backup checking 33 | if [ -n "${CHECK_CRON}" ]; then 34 | echo "Setup check cron job with cron expression CHECK_CRON: ${CHECK_CRON}" 35 | echo "${CHECK_CRON} /usr/bin/flock -n /var/run/backup.lock /bin/check >> /var/log/cron.log 2>&1" >> /var/spool/cron/crontabs/root 36 | fi 37 | 38 | # Make sure the file exists before we start tail 39 | touch /var/log/cron.log 40 | 41 | # start the cron deamon 42 | crond 43 | 44 | echo "Container started." 45 | 46 | exec "$@" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.4.2 4 | ### Removed 5 | * Execute a repository check after prune was passed as a parameter in the forget call - use RESTIC_CHECK_CRON Var instead 6 | 7 | ## v1.4.1 8 | ### Changed 9 | * Execute a repository check after prune was passed as a parameter in the forget call 10 | 11 | ## v1.4.0 12 | 13 | ### Added 14 | * Option to set the target folder backup 15 | * Multi-platform image build 16 | 17 | ## v1.3.2 (restic 0.16.0) 18 | 19 | ### Changed 20 | * Base image directly on official restic image 21 | * [Semver](https://semver.org/) aligned version naming including restic version 22 | * Updated to restic 0.16.0 23 | 24 | ### Added 25 | * rclone to docker image 26 | * Implemented a simple mail notification after backups using mailx 27 | * MAILX_ARGS environment variable 28 | 29 | ## v1.3.1-0.9.6 30 | 31 | ### Changed 32 | * Update to Restic v0.9.5 33 | * Reduced the number of layers in the Docker image 34 | 35 | ### Fixed 36 | * Check if a repo already exists works now for all repository types 37 | 38 | ### Added 39 | * shh added to container 40 | * fuse added to container 41 | * support to send mails using external SMTP server after backups 42 | 43 | ## v1.2-0.9.4 44 | 45 | ### Added 46 | * AWS Support 47 | 48 | ## v1.1 49 | 50 | ### Fixed 51 | * `--prune` must be passed to `RESTIC_FORGET_ARGS` to execute prune after forget. 52 | 53 | ### Changed 54 | * Switch to base Docker container to `golang:1.7-alpine` to support latest restic build. 55 | 56 | ## v1.0 57 | 58 | Initial release. 59 | 60 | The container has proper logs now and was running for over a month in production. 61 | There are still some features missing. Sticking to semantic versioning we do not expect any breaking changes in the 1.x releases. 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM docker.io/alpine:latest as rclone 2 | ARG TARGETPLATFORM 3 | 4 | RUN apk add wget 5 | 6 | # Get rclone executable 7 | RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ 8 | wget https://downloads.rclone.org/rclone-current-linux-amd64.zip && unzip rclone-current-linux-amd64.zip && mv rclone-*-linux-amd64/rclone /bin/rclone && chmod +x /bin/rclone; \ 9 | elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ 10 | wget https://downloads.rclone.org/rclone-current-linux-arm64.zip && unzip rclone-current-linux-arm64.zip && mv rclone-*-linux-arm64/rclone /bin/rclone && chmod +x /bin/rclone; \ 11 | elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ 12 | wget https://downloads.rclone.org/rclone-current-linux-arm-v7.zip && unzip rclone-current-linux-arm-v7.zip && mv rclone-*-linux-arm-v7/rclone /bin/rclone && chmod +x /bin/rclone; \ 13 | fi 14 | 15 | 16 | FROM docker.io/restic/restic:0.18.0 17 | 18 | RUN apk add --update --no-cache curl mailx shadow 19 | 20 | COPY --from=rclone /bin/rclone /bin/rclone 21 | 22 | RUN \ 23 | mkdir -p /mnt/restic /var/spool/cron/crontabs /var/log; \ 24 | touch /var/log/cron.log; 25 | 26 | ENV RESTIC_REPOSITORY=/mnt/restic 27 | ENV RESTIC_PASSWORD="" 28 | ENV RESTIC_TAG="" 29 | ENV NFS_TARGET="" 30 | ENV BACKUP_CRON="0 */6 * * *" 31 | ENV CHECK_CRON="" 32 | ENV RESTIC_INIT_ARGS="" 33 | ENV RESTIC_FORGET_ARGS="" 34 | ENV RESTIC_JOB_ARGS="" 35 | ENV RESTIC_DATA_SUBSET="" 36 | ENV MAILX_ARGS="" 37 | ENV OS_AUTH_URL="" 38 | ENV OS_PROJECT_ID="" 39 | ENV OS_PROJECT_NAME="" 40 | ENV OS_USER_DOMAIN_NAME="Default" 41 | ENV OS_PROJECT_DOMAIN_ID="default" 42 | ENV OS_USERNAME="" 43 | ENV OS_PASSWORD="" 44 | ENV OS_REGION_NAME="" 45 | ENV OS_INTERFACE="" 46 | ENV OS_IDENTITY_API_VERSION=3 47 | ENV BACKUP_SOURCES="" 48 | 49 | # openshift fix 50 | RUN mkdir /.cache && \ 51 | chgrp -R 0 /.cache && \ 52 | chmod -R g=u /.cache && \ 53 | chgrp -R 0 /mnt && \ 54 | chmod -R g=u /mnt && \ 55 | chgrp -R 0 /var/spool/cron/crontabs/root && \ 56 | chmod -R g=u /var/spool/cron/crontabs/root && \ 57 | chgrp -R 0 /var/log/cron.log && \ 58 | chmod -R g=u /var/log/cron.log 59 | 60 | # /data is the dir where you have to put the data to be backed up 61 | VOLUME /data 62 | 63 | COPY backup.sh /bin/backup 64 | COPY check.sh /bin/check 65 | COPY entry.sh /entry.sh 66 | 67 | ENTRYPOINT ["/entry.sh"] 68 | CMD ["tail","-fn0","/var/log/cron.log"] 69 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | lastLogfile="/var/log/check-last.log" 4 | lastMailLogfile="/var/log/check-mail-last.log" 5 | lastMicrosoftTeamsLogfile="/var/log/check-microsoft-teams-last.log" 6 | 7 | copyErrorLog() { 8 | cp ${lastLogfile} /var/log/check-error-last.log 9 | } 10 | 11 | logLast() { 12 | echo "$1" >> ${lastLogfile} 13 | } 14 | 15 | if [ -f "/hooks/pre-check.sh" ]; then 16 | echo "Starting pre-check script ..." 17 | /hooks/pre-check.sh 18 | else 19 | echo "Pre-check script not found ..." 20 | fi 21 | 22 | start=`date +%s` 23 | rm -f ${lastLogfile} ${lastMailLogfile} 24 | echo "Starting Check at $(date +"%Y-%m-%d %H:%M:%S")" 25 | echo "Starting Check at $(date)" >> ${lastLogfile} 26 | logLast "CHECK_CRON: ${CHECK_CRON}" 27 | logLast "RESTIC_DATA_SUBSET: ${RESTIC_DATA_SUBSET}" 28 | logLast "RESTIC_REPOSITORY: ${RESTIC_REPOSITORY}" 29 | logLast "AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}" 30 | 31 | # Do not save full check log to logfile but to check-last.log 32 | if [ -n "${RESTIC_DATA_SUBSET}" ]; then 33 | restic check --read-data-subset=${RESTIC_DATA_SUBSET} >> ${lastLogfile} 2>&1 34 | else 35 | restic check >> ${lastLogfile} 2>&1 36 | fi 37 | checkRC=$? 38 | logLast "Finished check at $(date)" 39 | if [[ $checkRC == 0 ]]; then 40 | echo "Check Successful" 41 | else 42 | echo "Check Failed with Status ${checkRC}" 43 | restic unlock 44 | copyErrorLog 45 | fi 46 | 47 | end=`date +%s` 48 | echo "Finished Check at $(date +"%Y-%m-%d %H:%M:%S") after $((end-start)) seconds" 49 | 50 | if [ -n "${TEAMS_WEBHOOK_URL}" ]; then 51 | teamsTitle="Restic Last Check Log" 52 | teamsMessage=$( cat ${lastLogfile} | sed 's/"/\"/g' | sed "s/'/\'/g" | sed ':a;N;$!ba;s/\n/\n\n/g' ) 53 | teamsReqBody="{\"title\": \"${teamsTitle}\", \"text\": \"${teamsMessage}\" }" 54 | sh -c "curl -H 'Content-Type: application/json' -d '${teamsReqBody}' '${TEAMS_WEBHOOK_URL}' > ${lastMicrosoftTeamsLogfile} 2>&1" 55 | if [ $? == 0 ]; then 56 | echo "Microsoft Teams notification successfully sent." 57 | else 58 | echo "Sending Microsoft Teams notification FAILED. Check ${lastMicrosoftTeamsLogfile} for further information." 59 | fi 60 | fi 61 | 62 | if [ -n "${MAILX_ARGS}" ]; then 63 | sh -c "mail -v -S sendwait ${MAILX_ARGS} < ${lastLogfile} > ${lastMailLogfile} 2>&1" 64 | if [ $? == 0 ]; then 65 | echo "Mail notification successfully sent." 66 | else 67 | echo "Sending mail notification FAILED. Check ${lastMailLogfile} for further information." 68 | fi 69 | fi 70 | 71 | if [ -f "/hooks/post-check.sh" ]; then 72 | echo "Starting post-check script ..." 73 | /hooks/post-check.sh $checkRC 74 | else 75 | echo "Post-check script not found ..." 76 | fi 77 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | lastLogfile="/var/log/backup-last.log" 4 | lastMailLogfile="/var/log/mail-last.log" 5 | lastMicrosoftTeamsLogfile="/var/log/microsoft-teams-last.log" 6 | 7 | if [ -n "$BACKUP_SOURCES" ]; then 8 | backupSources="$BACKUP_SOURCES" 9 | else 10 | backupSources="/data" 11 | fi 12 | 13 | copyErrorLog() { 14 | cp ${lastLogfile} /var/log/backup-error-last.log 15 | } 16 | 17 | logLast() { 18 | echo "$1" >> ${lastLogfile} 19 | } 20 | 21 | if [ -f "/hooks/pre-backup.sh" ]; then 22 | echo "Starting pre-backup script ..." 23 | /hooks/pre-backup.sh 24 | else 25 | echo "Pre-backup script not found ..." 26 | fi 27 | 28 | start=`date +%s` 29 | rm -f ${lastLogfile} ${lastMailLogfile} 30 | echo "Starting Backup at $(date +"%Y-%m-%d %H:%M:%S")" 31 | echo "Starting Backup at $(date)" >> ${lastLogfile} 32 | logLast "BACKUP_CRON: ${BACKUP_CRON}" 33 | logLast "RESTIC_TAG: ${RESTIC_TAG}" 34 | logLast "RESTIC_FORGET_ARGS: ${RESTIC_FORGET_ARGS}" 35 | logLast "RESTIC_JOB_ARGS: ${RESTIC_JOB_ARGS}" 36 | logLast "RESTIC_REPOSITORY: ${RESTIC_REPOSITORY}" 37 | logLast "AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}" 38 | 39 | # Do not save full backup log to logfile but to backup-last.log 40 | restic backup ${backupSources} ${RESTIC_JOB_ARGS} --tag=${RESTIC_TAG?"Missing environment variable RESTIC_TAG"} >> ${lastLogfile} 2>&1 41 | backupRC=$? 42 | logLast "Finished backup at $(date)" 43 | if [[ $backupRC == 0 ]]; then 44 | echo "Backup Successful" 45 | else 46 | echo "Backup Failed with Status ${backupRC}" 47 | restic unlock 48 | copyErrorLog 49 | fi 50 | 51 | if [[ $backupRC == 0 ]] && [ -n "${RESTIC_FORGET_ARGS}" ]; then 52 | echo "Forget about old snapshots based on RESTIC_FORGET_ARGS = ${RESTIC_FORGET_ARGS}" 53 | restic forget ${RESTIC_FORGET_ARGS} >> ${lastLogfile} 2>&1 54 | rc=$? 55 | logLast "Finished forget at $(date)" 56 | if [[ $rc == 0 ]]; then 57 | echo "Forget Successful" 58 | else 59 | echo "Forget Failed with Status ${rc}" 60 | restic unlock 61 | copyErrorLog 62 | fi 63 | fi 64 | 65 | end=`date +%s` 66 | echo "Finished Backup at $(date +"%Y-%m-%d %H:%M:%S") after $((end-start)) seconds" 67 | 68 | if [ -n "${TEAMS_WEBHOOK_URL}" ]; then 69 | teamsTitle="Restic Last Backup Log" 70 | teamsMessage=$( cat ${lastLogfile} | sed 's/"/\"/g' | sed "s/'/\'/g" | sed ':a;N;$!ba;s/\n/\n\n/g' ) 71 | teamsReqBody="{\"title\": \"${teamsTitle}\", \"text\": \"${teamsMessage}\" }" 72 | sh -c "curl -H 'Content-Type: application/json' -d '${teamsReqBody}' '${TEAMS_WEBHOOK_URL}' > ${lastMicrosoftTeamsLogfile} 2>&1" 73 | if [ $? == 0 ]; then 74 | echo "Microsoft Teams notification successfully sent." 75 | else 76 | echo "Sending Microsoft Teams notification FAILED. Check ${lastMicrosoftTeamsLogfile} for further information." 77 | fi 78 | fi 79 | 80 | if [ -n "${MAILX_ARGS}" ]; then 81 | sh -c "mail -v -S sendwait ${MAILX_ARGS} < ${lastLogfile} > ${lastMailLogfile} 2>&1" 82 | if [ $? == 0 ]; then 83 | echo "Mail notification successfully sent." 84 | else 85 | echo "Sending mail notification FAILED. Check ${lastMailLogfile} for further information." 86 | fi 87 | fi 88 | 89 | if [ -f "/hooks/post-backup.sh" ]; then 90 | echo "Starting post-backup script ..." 91 | /hooks/post-backup.sh $backupRC 92 | else 93 | echo "Post-backup script not found ..." 94 | fi 95 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | branches: [ "master" ] 11 | # Publish semver tags as releases. 12 | tags: [ 'v*.*.*' ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | env: 17 | # Use docker.io for Docker Hub if empty 18 | REGISTRY: ghcr.io 19 | # github.repository as / 20 | IMAGE_NAME: ${{ github.repository }} 21 | 22 | 23 | jobs: 24 | build: 25 | 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | packages: write 30 | # This is used to complete the identity challenge 31 | # with sigstore/fulcio when running outside of PRs. 32 | id-token: write 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | # Install the cosign tool except on PR 39 | # https://github.com/sigstore/cosign-installer 40 | - name: Install cosign 41 | if: github.event_name != 'pull_request' 42 | uses: sigstore/cosign-installer@v3.8.2 43 | with: 44 | cosign-release: 'v2.4.3' 45 | 46 | # Workaround: https://github.com/docker/build-push-action/issues/461 47 | - name: Setup Docker buildx 48 | uses: docker/setup-buildx-action@v3 49 | 50 | # Login against a Docker registry except on PR 51 | # https://github.com/docker/login-action 52 | - name: Log into registry ${{ env.REGISTRY }} 53 | if: github.event_name != 'pull_request' 54 | uses: docker/login-action@v3 55 | with: 56 | registry: ${{ env.REGISTRY }} 57 | username: ${{ github.actor }} 58 | password: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | # Extract metadata (tags, labels) for Docker 61 | # https://github.com/docker/metadata-action 62 | - name: Extract Docker metadata 63 | id: meta 64 | uses: docker/metadata-action@v5 65 | with: 66 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 67 | 68 | # Build and push Docker image with Buildx (don't push on PR) 69 | # https://github.com/docker/build-push-action 70 | - name: Build and push Docker image 71 | id: build-and-push 72 | uses: docker/build-push-action@v6 73 | with: 74 | context: . 75 | platforms: linux/amd64,linux/arm64,linux/arm/v7 76 | push: ${{ github.event_name != 'pull_request' }} 77 | tags: ${{ steps.meta.outputs.tags }} 78 | labels: ${{ steps.meta.outputs.labels }} 79 | cache-from: type=gha 80 | cache-to: type=gha,mode=max 81 | 82 | 83 | # Sign the resulting Docker image digest except on PRs. 84 | # This will only write to the public Rekor transparency log when the Docker 85 | # repository is public to avoid leaking data. If you would like to publish 86 | # transparency data even for private images, pass --force to cosign below. 87 | # https://github.com/sigstore/cosign 88 | - name: Sign the published Docker image 89 | if: ${{ github.event_name != 'pull_request' }} 90 | env: 91 | # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable 92 | TAGS: ${{ steps.meta.outputs.tags }} 93 | DIGEST: ${{ steps.build-and-push.outputs.digest }} 94 | # This step uses the identity token to provision an ephemeral certificate 95 | # against the sigstore community Fulcio instance. 96 | run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restic Backup Docker Container 2 | A docker container to automate [restic backups](https://restic.github.io/) 3 | 4 | This container runs restic backups in regular intervals. 5 | 6 | * Easy setup and maintanance 7 | * Support for different targets (tested with: Local, NFS, SFTP, AWS) 8 | * Support `restic mount` inside the container to browse the backup files 9 | 10 | **Container**: 11 | * [ghcr.io/lobaro/restic-backup-docker](https://github.com/lobaro/restic-backup-docker/pkgs/container/restic-backup-docker) 12 | * Old: [lobaro/restic-backup-docker](https://hub.docker.com/r/lobaro/restic-backup-docker/) 13 | 14 | Latest master (experimental): 15 | ``` 16 | docker pull ghcr.io/lobaro/restic-backup-docker:master 17 | ``` 18 | 19 | Latest release: 20 | ``` 21 | docker pull ghcr.io/lobaro/restic-backup-docker:latest 22 | ``` 23 | 24 | # Contributing 25 | Pull Requests to improve the image are always wellcome. Please create an issue about the PR first. 26 | 27 | When behaviour of the image changes (Features, Bugfixes, Changes in the API) please update the "Unreleased" section of the [CHANGELOG.md](https://github.com/lobaro/restic-backup-docker/blob/master/CHANGELOG.md) 28 | 29 | 30 | ## Hooks 31 | 32 | If you need to execute a script before or after each backup or check, you need to add your hook scripts in the container folder `/hooks`: 33 | ``` 34 | -v ~/home/user/hooks:/hooks 35 | ``` 36 | 37 | Call your pre-backup script `pre-backup.sh` and post-backup script `post-backup.sh`. You can also have separate scripts when running data verification checks `pre-check.sh` and `post-check.sh`. 38 | 39 | Please don't hesitate to report any issues you find. **Thanks.** 40 | 41 | # Test the container 42 | 43 | Clone this repository: 44 | ``` 45 | git clone https://github.com/Lobaro/restic-backup-docker.git 46 | cd restic-backup-docker 47 | ``` 48 | 49 | Build the container (the container is named `backup-test`): 50 | ``` 51 | ./build.sh 52 | ``` 53 | 54 | Run the container: 55 | ``` 56 | ./run.sh 57 | ``` 58 | 59 | This will run the container `backup-test` with the name `backup-test`. Existing containers with that name are completely removed automatically. 60 | 61 | The container will back up `~/test-data` to a repository with password `test` at `~/test-repo` every minute. The repository is initialized automatically by the container. If you'd like to change the arguments passed to `restic init`, you can do so using the `RESTIC_INIT_ARGS` env variable. 62 | 63 | To enter your container execute: 64 | ``` 65 | docker exec -ti backup-test /bin/sh 66 | ``` 67 | 68 | Now you can use restic [as documented](https://restic.readthedocs.io/en/stable/), e.g. try to run `restic snapshots` to list all your snapshots. 69 | 70 | ## Logfiles 71 | Logfiles are inside the container. If needed, you can create volumes for them. 72 | ``` 73 | docker logs 74 | ``` 75 | Shows `/var/log/cron.log`. 76 | 77 | Additionally you can see the full log, including restic output, of the last execution in `/var/log/backup-last.log`. When the backup fails, the log is copied to `/var/log/restic-error-last.log`. If configured, you can find the full output of the mail notification in `/var/log/mail-last.log`. 78 | 79 | # Use the running container 80 | 81 | Assuming the container name is `restic-backup-var`, you can execute restic with: 82 | 83 | docker exec -ti restic-backup-var restic 84 | 85 | ## Backup 86 | 87 | To execute a backup manually, independent of the CRON, run: 88 | 89 | docker exec -ti restic-backup-var /bin/backup 90 | 91 | Back up a single file or directory: 92 | 93 | docker exec -ti restic-backup-var restic backup /data/path/to/dir --tag my-tag 94 | 95 | ## Data verification check 96 | 97 | To verify backup integrity and consistency manually, independent of the CRON, run: 98 | 99 | docker exec -ti restic-backup-var /bin/check 100 | 101 | ## Restore 102 | 103 | You might want to mount a separate host volume at e.g. `/restore` to not override existing data while restoring. 104 | 105 | Get your snapshot ID with: 106 | 107 | docker exec -ti restic-backup-var restic snapshots 108 | 109 | e.g. `abcdef12` 110 | 111 | docker exec -ti restic-backup-var restic restore --include /data/path/to/files --target / abcdef12 112 | 113 | The target is `/` since all data backed up should be inside the host mounted `/data` dir. If you mount `/restore` you should set `--target /restore` and the data will end up in `/restore/data/path/to/files`. 114 | 115 | # Customize the Container 116 | 117 | The container is set up by setting [environment variables](https://docs.docker.com/engine/reference/run/#/env-environment-variables) and [volumes](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems). 118 | 119 | ## Environment variables 120 | 121 | * `RESTIC_REPOSITORY` - the location of the restic repository. Default `/mnt/restic`. For S3: `s3:https://s3.amazonaws.com/BUCKET_NAME` 122 | * `RESTIC_PASSWORD` - the password for the restic repository. Will also be used for restic init during first start when the repository is not initialized. 123 | * `RESTIC_TAG` - Optional. To tag the images created by the container. 124 | * `NFS_TARGET` - Optional. If set, the given NFS is mounted, i.e. `mount -o nolock -v ${NFS_TARGET} /mnt/restic`. `RESTIC_REPOSITORY` must remain its default value! 125 | * `BACKUP_CRON` - A cron expression to run the backup. Note: The cron daemon uses UTC time zone. Default: `0 */6 * * *` aka every 6 hours. 126 | * `CHECK_CRON` - Optional. A cron expression to run data integrity check (`restic check`). If left unset, data will not be checked. Note: The cron daemon uses UTC time zone. Example: `0 23 * * 3` to run 11PM every Tuesday. 127 | * `RESTIC_FORGET_ARGS` - Optional. Only if specified, `restic forget` is run with the given arguments after each backup. Example value: `-e "RESTIC_FORGET_ARGS=--prune --keep-last 10 --keep-hourly 24 --keep-daily 7 --keep-weekly 52 --keep-monthly 120 --keep-yearly 100"` 128 | * `RESTIC_INIT_ARGS` - Optional. Allows specifying extra arguments to `restic init` such as a password file with `--password-file`. 129 | * `RESTIC_JOB_ARGS` - Optional. Allows specifying extra arguments to the backup job such as limiting bandwith with `--limit-upload` or excluding file masks with `--exclude`. 130 | * `RESTIC_DATA_SUBSET` - Optional. You can pass a value to `--read-data-subset` when a repository check is run. If left unset, only the structure of the repository is verified. Note: `CHECK_CRON` must be set for check to be run automatically. 131 | * `AWS_ACCESS_KEY_ID` - Optional. When using restic with AWS S3 storage. 132 | * `AWS_SECRET_ACCESS_KEY` - Optional. When using restic with AWS S3 storage. 133 | * `TEAMS_WEBHOOK_URL` - Optional. If specified, the content of `/var/log/backup-last.log` and `/var/log/check-last.log` is sent to your Microsoft Teams channel after each backup and data integrity check. 134 | * `MAILX_ARGS` - Optional. If specified, the content of `/var/log/backup-last.log` and `/var/log/check-last.log` is sent via mail after each backup and data integrity check using an *external SMTP*. To have maximum flexibility, you have to specify the mail/smtp parameters on your own. Have a look at the [mailx manpage](https://linux.die.net/man/1/mailx) for further information. Example value: `-e "MAILX_ARGS=-r 'from@example.de' -s 'Result of the last restic run' -S smtp='smtp.example.com:587' -S smtp-use-starttls -S smtp-auth=login -S smtp-auth-user='username' -S smtp-auth-password='password' 'to@example.com'"`. 135 | * `OS_AUTH_URL` - Optional. When using restic with OpenStack Swift container. 136 | * `OS_PROJECT_ID` - Optional. When using restic with OpenStack Swift container. 137 | * `OS_PROJECT_NAME` - Optional. When using restic with OpenStack Swift container. 138 | * `OS_USER_DOMAIN_NAME` - Optional. When using restic with OpenStack Swift container. 139 | * `OS_PROJECT_DOMAIN_ID` - Optional. When using restic with OpenStack Swift container. 140 | * `OS_USERNAME` - Optional. When using restic with OpenStack Swift container. 141 | * `OS_PASSWORD` - Optional. When using restic with OpenStack Swift container. 142 | * `OS_REGION_NAME` - Optional. When using restic with OpenStack Swift container. 143 | * `OS_INTERFACE` - Optional. When using restic with OpenStack Swift container. 144 | * `OS_IDENTITY_API_VERSION` - Optional. When using restic with OpenStack Swift container. 145 | * `BACKUP_SOURCES` - Optional. Set the folder that will be backed up. 146 | 147 | ## Volumes 148 | 149 | * `/data` - This is the data that gets backed up. Just [mount](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems) it to wherever you want. 150 | 151 | ## Set the hostname 152 | 153 | Since restic saves the hostname with each snapshot and the hostname of a docker container is derived from its id, you might want to customize this by setting the hostname of the container to another value. 154 | 155 | Set `--hostname` in the [network settings](https://docs.docker.com/engine/reference/run/#network-settings) 156 | 157 | ## Backup via SFTP 158 | 159 | Since restic needs a **passwordless login** to the SFTP server, make sure you can do `sftp user@host` from inside the container. If you can do so from your host system, the easiest way is to just mount your `.ssh` folder containing the authorized cert into the container by specifying `-v ~/.ssh:/root/.ssh` as an argument for `docker run`. 160 | 161 | Now you can simply specify the restic repository to be an [SFTP repository](https://restic.readthedocs.io/en/stable/Manual/#create-an-sftp-repository). 162 | 163 | ``` 164 | -e "RESTIC_REPOSITORY=sftp:user@host:/tmp/backup" 165 | ``` 166 | 167 | ## Backup via OpenStack Swift 168 | 169 | Restic can back up data to an OpenStack Swift container. Because Swift supports various authentication methods, credentials are passed through environment variables. In order to help integration with existing OpenStack installations, the naming convention of those variables follows the official Python Swift client. 170 | 171 | Now you can simply specify the restic repository to be a [Swift repository](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#openstack-swift). 172 | 173 | ``` 174 | -e "RESTIC_REPOSITORY=swift:backup:/" 175 | -e "RESTIC_PASSWORD=password" 176 | -e "OS_AUTH_URL=https://auth.cloud.ovh.net/v3" 177 | -e "OS_PROJECT_ID=xxxx" 178 | -e "OS_PROJECT_NAME=xxxx" 179 | -e "OS_USER_DOMAIN_NAME=Default" 180 | -e "OS_PROJECT_DOMAIN_ID=default" 181 | -e "OS_USERNAME=username" 182 | -e "OS_PASSWORD=password" 183 | -e "OS_REGION_NAME=SBG" 184 | -e "OS_INTERFACE=public" 185 | -e "OS_IDENTITY_API_VERSION=3" 186 | ``` 187 | 188 | ## Backup via rclone 189 | 190 | To use rclone as a backend for restic, simply add the rclone config file as a volume with `-v /absolute/path/to/rclone.conf:/root/.config/rclone/rclone.conf`. 191 | 192 | Note that for some backends (Among them Google Drive and Microsoft OneDrive), rclone writes data back to the `rclone.conf` file. In this case it needs to be writable by Docker. 193 | 194 | If the container fails to write the new `rclone.conf` file with the error message `Failed to save config after 10 tries: Failed to move previous config to backup location`, add the entire `rclone` directory as a volume: `-v /absolute/path/to/rclone-dir:/root/.config/rclone`. 195 | 196 | ## Example docker-compose 197 | 198 | This is an example `docker-compose.yml`. The container will back up two directories to an SFTP server and check data integrity once a week. 199 | 200 | ``` 201 | version: '3' 202 | 203 | services: 204 | restic: 205 | image: ghcr.io/lobaro/restic-backup-docker:latest 206 | hostname: nas # This will be visible in restic snapshot list 207 | restart: always 208 | privileged: true 209 | volumes: 210 | - /volume1/Backup:/data/Backup:ro # Backup /volume1/Backup from host 211 | - /home/user:/data/home:ro # Backup /home/user from host 212 | - ./post-backup.sh:/hooks/post-backup.sh:ro # Run script post-backup.sh after every backup 213 | - ./post-check.sh:/hooks/post-check.sh:ro # Run script post-check.sh after every check 214 | - ./ssh:/root/.ssh # SSH keys and config so we can login to "storageserver" without password 215 | environment: 216 | - RESTIC_REPOSITORY=sftp:storageserver:/storage/nas # Backup to server "storageserver" 217 | - RESTIC_PASSWORD=passwordForRestic # Password restic uses for encryption 218 | - BACKUP_CRON=0 22 * * 0 # Start backup every Sunday 22:00 UTC 219 | - CHECK_CRON=0 22 * * 3 # Start check every Wednesday 22:00 UTC 220 | - RESTIC_DATA_SUBSET=50G # Download 50G of data from "storageserver" every Wednesday 22:00 UTC and check the data integrity 221 | - RESTIC_FORGET_ARGS=--prune --keep-last 12 # Only keep the last 12 snapshots 222 | ``` 223 | 224 | # Versioning 225 | Starting from v1.3.0 versioning follows [Semantic versioning](http://semver.org/) 226 | 227 | --------------------------------------------------------------------------------