diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..be6afbc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: loomchild diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..343df9b --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,43 @@ +name: publish-image + +on: + push: + branches: + - 'master' + schedule: + - cron: '0 4 1 * *' +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: loomchild + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: loomchild + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: | + loomchild/volume-backup:latest + ghcr.io/loomchild/volume-backup:latest diff --git a/Dockerfile b/Dockerfile index 3075837..4a6bb5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM alpine +RUN apk update && apk add --no-cache dumb-init bash xz tar pigz zstd + COPY volume-backup.sh / -ENTRYPOINT [ "/bin/sh", "/volume-backup.sh" ] +ENTRYPOINT [ "/usr/bin/dumb-init", "--", "/volume-backup.sh" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d19387 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - 2019 Jarek Lipski + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md index 7da327a..97d6a49 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,57 @@ # volume-backup -An utility to backup and restore [docker volumes](https://docs.docker.com/engine/reference/commandline/volume/). +An utility to backup and restore [Docker volumes](https://docs.docker.com/storage/volumes/). For more info, read my article on [Medium](https://medium.com/@jareklipski/backup-restore-docker-named-volumes-350397b8e362). -**Note**: Make sure no container is using the volume before backup or restore, otherwise your data might be damaged. +It works just as well with [Podman](https://podman.io/) volumes, just prefix each command with `podman` instead of `docker`. + +**Note**: Make sure no container is using the volume before backup or restore, otherwise your data might be damaged. See [Miscellaneous](#miscellaneous) for instructions. + +**Note**: When using docker-compose, make sure to backup and restore volume labels. See [Miscellaneous](#miscellaneous) for more information. ## Backup +Syntax: + + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup > [archive-path] + +For example: + + docker run -v some_volume:/volume --rm --log-driver none loomchild/volume-backup backup > some_archive.tar.bz2 + +will archive volume named `some_volume` to `some_archive.tar.bz2` archive file. + +**Note**: `--log-driver none` option is necessary to avoid storing an entire backup in a temporary stdout JSON file. More info in [Docker logging documentation](https://docs.docker.com/config/containers/logging/configure/) and in [this issue](https://github.com/loomchild/volume-backup/issues/39). + +**WARNING**: This method should not be used under PowerShell on Windows as no usable backup will be generated. + +### Backup to a file (deprecated) + Syntax: docker run -v [volume-name]:/volume -v [output-dir]:/backup --rm loomchild/volume-backup backup [archive-name] For example: - docker run -v some_volume:/volume -v /tmp:/backup --rm loomchild/volume-backup backup archive1 + docker run -v some_volume:/volume -v /tmp:/backup --rm loomchild/volume-backup backup some_archive -will archive volume named `some_volume` to `/tmp/archive.tar.bz2` archive file. +will archive volume named `some_volume` to `/tmp/some_archive.tar.bz2` archive file. ## Restore -**Note**: This operation will delete all contents of the volume +Syntax: + + docker run -i -v [volume-name]:/volume --rm loomchild/volume-backup restore < [archive-path] + +For example: + + docker run -i -v some_volume:/volume --rm loomchild/volume-backup restore < some_archive.tar.bz2 + +will clean and restore volume named `some_volume` from `some_archive.tar.bz2` archive file. + +**Note**: Don't forget the `-i` switch for interactive operation. +**Note** Restore will fail if the target volume is not empty (use `-f` flag to override). + +### Restore from a file (deprecated) Syntax: @@ -26,6 +59,52 @@ Syntax: For example: - docker run -v some_volume:/volume -v /tmp:/backup --rm loomchild/volume-backup restore archive1 + docker run -v some_volume:/volume -v /tmp:/backup --rm loomchild/volume-backup restore some_archive + +will clean and restore volume named `some_volume` from `/tmp/some_archive.tar.bz2` archive file. + +## Miscellaneous + +1. Upgrade / update volume-backup + ``` + docker pull loomchild/volume-backup + ``` + +1. volume-backup is also available from GitHub Container Registry (ghcr.io), to avoid DockerHub usage limits: + ``` + docker pull ghcr.io/loomchild/volume-backup + ``` + **Note**: you'll need to write `ghcr.io/loomchild/volume-backup` instead of just `loomchild/volume-backup` when running the utility. + +1. Find all containers using a volume (to stop them before backing-up) + ``` + docker ps -a --filter volume=[volume-name] + ``` + +1. Exclude some files from the backup and send the archive to stdout + ``` + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup -e [excluded-glob] > [archive-path] + ``` + +1. Use different compression algorithm for better performance + ``` + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup -c pigz > [archive-path] + ``` + +1. Show simple progress indicator using verbose `-v` flag (works both for backup and restore) + ``` + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup -v > [archive-path] + ``` + +1. Pass additional arguments to the Tar utility using `-x` option + ``` + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup -x --verbose > [archive-path] + ``` + +1. Directly migrate the volume to a new host + ``` + docker run -v [volume-name]:/volume --rm --log-driver none loomchild/volume-backup backup | ssh [receiver] docker run -i -v [volume-name]:/volume --rm loomchild/volume-backup restore + ``` + **Note**: In case there are no traffic limitations between the hosts you can trade CPU time for bandwidth by turning off compression via `-c none` option. -will clean and restore volume named `some_volume` from `/tmp/archive.tar.bz2` archive file. +1. Volume labels are not backed-up or restored automatically, but they might be required for your application to work (e.g. when using `docker-compose`). If you need to preserve them, create a label backup file as follows: `docker inspect [volume-name] -f "{{json .Labels}}" > labels.json`. When restoring your data, target volume needs to be created manually with labels before launching the restore script: `docker volume create --label "label1" --label "label2" [volume-name]`. diff --git a/volume-backup.sh b/volume-backup.sh index b31f183..b4d481a 100755 --- a/volume-backup.sh +++ b/volume-backup.sh @@ -1,37 +1,137 @@ -#!/bin/sh +#!/bin/bash usage() { - echo "Usage: volume-backup " - exit + >&2 echo "Usage: volume-backup [options]. Reads from stdin and writes to stdout. Can also read/write files (deprecated)." + >&2 echo "" + >&2 echo "Options:" + >&2 echo " -c chooose compression algorithm: bz2 (default), gz, xz, pigz, zstd and 0 (none)" + >&2 echo " -e exclude files or directories (only for backup operation)" + >&2 echo " -f force overwrite even if target volume is not empty during restore" + >&2 echo " -x pass additional arguments to the Tar utility" + >&2 echo " -v verbose" } backup() { - mkdir -p `dirname /backup/$ARCHIVE` - tar -cjf /backup/$ARCHIVE -C /volume ./ + if [ -z "$(ls -A /volume)" ]; then + >&2 echo "Volume is empty or missing, check if you specified a correct name" + exit 1 + fi + + if ! [ "$ARCHIVE" == "-" ]; then + mkdir -p `dirname /backup/$ARCHIVE` + fi + + tar -C /volume "${TAROPTS[@]}" -cf $ARCHIVE_PATH ./ } restore() { - if ! [ -e /backup/$ARCHIVE ]; then - echo "Archive file $ARCHIVE does not exist" + if ! [ "$ARCHIVE" == "-" ]; then + if ! [ -e $ARCHIVE_PATH ]; then + >&2 echo "Archive file $ARCHIVE does not exist" + exit 1 + fi + fi + + + if ! [ -z "$(ls -A /volume)" -o -n "$FORCE" ]; then + >&2 echo "Target volume is not empty, aborting; use -f to override" exit 1 fi rm -rf /volume/* /volume/..?* /volume/.[!.]* - tar -C /volume/ -xjf /backup/$ARCHIVE + tar -C /volume/ "${TAROPTS[@]}" -xf $ARCHIVE_PATH } -# Needed because sometimes pty is not ready when executing docker-compose run -# See https://github.com/docker/compose/pull/4738 for more details -# TODO: remove after above pull request or equivalent is merged -sleep 1 +OPERATION=$1 -if [ $# -ne 2 ]; then - usage -fi +TAROPTS=() +COMPRESSION="bz2" +FORCE="" -OPERATION=$1 +OPTIND=2 + +while getopts "h?vfc:e:x:" OPTION; do + case "$OPTION" in + h|\?) + usage + exit 0 + ;; + c) + if [ -z "$OPTARG" ]; then + usage + exit 1 + fi + COMPRESSION=$OPTARG + ;; + e) + if [ -z "$OPTARG" -o "$OPERATION" != "backup" ]; then + usage + exit 1 + fi + TAROPTS+=(--exclude $OPTARG) + ;; + f) + if [ "$OPERATION" != "restore" ]; then + usage + exit 1 + fi + FORCE=1 + ;; + v) + TAROPTS+=(--checkpoint=.1000) + EOLN=1 + ;; + x) + if [ -z "$OPTARG" ]; then + usage + exit 1 + fi + # Note: it doesn't support nested quotes, e.g. -x '-I "zstd -10"' + OPTARR=($OPTARG) + TAROPTS=(${TAROPTS[@]} ${OPTARR[@]}) + ;; + esac +done + +shift $((OPTIND - 1)) + +case "$COMPRESSION" in +xz) + TAROPTS+=(-J) + EXTENSION=.tar.xz + ;; +bz2) + TAROPTS+=(-j) + EXTENSION=.tar.bz2 + ;; +gz) + TAROPTS+=(-z) + EXTENSION=.tar.gz + ;; +pigz) + TAROPTS+=(-I pigz) + EXTENSION=.tar.gz + ;; +zstd) + TAROPTS+=(-I zstd) + EXTENSION=.tar.zstd + ;; +none|0) + EXTENSION=.tar + ;; +*) + usage + exit 1 + ;; +esac -ARCHIVE=${2%%.tar.bz2}.tar.bz2 +if [ -z "$1" ] || [ "$1" == "-" ]; then + ARCHIVE="-" + ARCHIVE_PATH=$ARCHIVE +else + ARCHIVE=${1%%$EXTENSION}$EXTENSION + ARCHIVE_PATH=/backup/$ARCHIVE +fi case "$OPERATION" in "backup" ) @@ -44,3 +144,7 @@ restore usage ;; esac + +if ! [ -z "$EOLN" ]; then + >&2 echo +fi