diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a8dc5a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + time: "04:00" + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 10 + - package-ecosystem: docker + directory: "/" + schedule: + interval: daily + time: "04:00" + target-branch: "master" + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 10 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fab409a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ + +name: build docker image + +on: + push: + tags: + - "**" + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: Checkout the code + uses: actions/checkout@v4 + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Get latest release version number + id: docker-tag + uses: yuya-takeyama/docker-tag-from-github-ref-action@v1 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: fradelg + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build multiarch image + run: | + docker buildx build --push \ + --tag fradelg/mysql-cron-backup:${{ steps.docker-tag.outputs.tag }} \ + --platform linux/amd64,linux/arm/v7,linux/arm64 . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4460983 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: build docker image + +on: + workflow_dispatch: + push: + branches: + - "**" + +jobs: + test: + runs-on: ubuntu-22.04 + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - name: Test Bash scripts + run: sudo apt-get -qq update && sudo apt-get install -y devscripts shellcheck && make test + - name: Test image + env: + VOLUME_PATH: /tmp/mariadb + DATABASE_NAME: foo + MARIADB_ROOT_PASSWORD: abcd + run: | + docker compose up -d mariadb + docker compose run backup /backup.sh + docker compose run backup /restore.sh /backup/latest.foo.sql.gz + docker compose stop diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6320cd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 19d4291..73a2588 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,48 @@ -FROM alpine -MAINTAINER Fco. Javier Delgado del Hoyo <frandelhoyo@gmail.com> +FROM golang:1.20.4-alpine3.18 AS binary +RUN apk -U add openssl git -RUN apk add --update bash mysql-client gzip && rm -rf /var/cache/apk/* && mkdir /backup +ARG DOCKERIZE_VERSION=v0.7.0 +WORKDIR /go/src/github.com/jwilder +RUN git clone https://github.com/jwilder/dockerize.git && \ + cd dockerize && \ + git checkout ${DOCKERIZE_VERSION} + +WORKDIR /go/src/github.com/jwilder/dockerize +ENV GO111MODULE=on +RUN go mod tidy +RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -o /go/bin/dockerize . + +FROM alpine:3.20.3 +LABEL maintainer "Fco. Javier Delgado del Hoyo <frandelhoyo@gmail.com>" + +RUN apk add --update \ + tzdata \ + bash \ + gzip \ + openssl \ + mysql-client=~10.11 \ + mariadb-connector-c \ + fdupes && \ + rm -rf /var/cache/apk/* + +COPY --from=binary /go/bin/dockerize /usr/local/bin ENV CRON_TIME="0 3 * * sun" \ MYSQL_HOST="mysql" \ - MYSQL_PORT="3306" + MYSQL_PORT="3306" \ + TIMEOUT="10s" \ + MYSQLDUMP_OPTS="--quick" -COPY run.sh /run.sh -COPY backup.sh /backup.sh -COPY restore.sh /restore.sh -RUN chmod +x /backup.sh /restore.sh +COPY ["run.sh", "backup.sh", "restore.sh", "/delete.sh", "/"] +RUN mkdir /backup && \ + chmod 777 /backup && \ + chmod 755 /run.sh /backup.sh /restore.sh /delete.sh && \ + touch /mysql_backup.log && \ + chmod 666 /mysql_backup.log VOLUME ["/backup"] -CMD ["/run.sh"] +HEALTHCHECK --interval=2s --retries=1800 \ + CMD stat /HEALTHY.status || exit 1 + +CMD dockerize -wait tcp://${MYSQL_HOST}:${MYSQL_PORT} -timeout ${TIMEOUT} /run.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca5d51c --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# Makefile + +all: test + +test: + # Checking for syntax errors + set -e; for SCRIPT in *.sh; \ + do \ + bash -n $$SCRIPT; \ + done + + # Checking for bashisms (currently not failing, but only listing) + SCRIPT="$$(which checkbashisms)"; if [ -n "$$SCRIPT" ] && [ -x "$$SCRIPT" ]; \ + then \ + $$SCRIPT *.sh || true; \ + else \ + echo "WARNING: skipping bashism test - you need to install checkbashism."; \ + fi + + SCRIPT="$$(which shellcheck)"; if [ -n "$$SCRIPT" ] && [ -x "$$SCRIPT" ]; \ + then \ + $$SCRIPT *.sh || true; \ + else \ + echo "WARNING: skipping shellcheck test - you need to install shellcheck."; \ + fi diff --git a/README.md b/README.md index f4d6865..16a2d65 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,258 @@ # mysql-cron-backup -This image runs mysqldump to backup database periodically using cron. Data is dumped to the container folder `/backup` +Run mysqldump to backup your databases periodically using the cron task manager in the container. Your backups are saved in `/backup`. You can mount any directory of your host or a docker volumes in /backup. Othwerwise, a docker volume is created in the default location. ## Usage: - docker run -d \ - --env MYSQL_USER=admin \ - --env MYSQL_PASS=password \ - --link mysql - --volume /path/to/my/host/folder:/backup - fradelg/mysql-backup +```bash +docker container run -d \ + --env MYSQL_USER=root \ + --env MYSQL_PASS=my_password \ + --link mysql + --volume /path/to/my/backup/folder:/backup + fradelg/mysql-cron-backup +``` + +### Healthcheck + + +Healthcheck is provided as a basic init control. +Container is **Healthy** after the database init phase, that is after `INIT_BACKUP` or `INIT_RESTORE_LATEST` happends without check if there is an error, **Starting** otherwise. Not other checks are actually provided. ## Variables - MYSQL_HOST the host/ip of your mysql database - MYSQL_PORT the port number of your mysql database - MYSQL_USER the username of your mysql database - MYSQL_PASS the password of your mysql database - MYSQL_DB the database name to dump. Default: `--all-databases` - CRON_TIME the interval of cron job to run mysqldump. `0 0 * * *` by default, which is every day at 00:00 - MAX_BACKUPS the number of backups to keep. When reaching the limit, the old backup will be discarded. No limit by default - INIT_BACKUP if set, create a backup when the container starts - INIT_RESTORE_LATEST if set, restores latest backup + +- `MYSQL_HOST`: The host/ip of your mysql database. +- `MYSQL_HOST_FILE`: The file in container where to find the host of your mysql database (cf. docker secrets). You should use either MYSQL_HOST_FILE or MYSQL_HOST (see examples below). +- `MYSQL_PORT`: The port number of your mysql database. +- `MYSQL_USER`: The username of your mysql database. +- `MYSQL_USER_FILE`: The file in container where to find the user of your mysql database (cf. docker secrets). You should use either MYSQL_USER_FILE or MYSQL_USER (see examples below). +- `MYSQL_PASS`: The password of your mysql database. +- `MYSQL_PASS_FILE`: The file in container where to find the password of your mysql database (cf. docker secrets). You should use either MYSQL_PASS_FILE or MYSQL_PASS (see examples below). +- `MYSQL_DATABASE`: The database name to dump. Default: `--all-databases`. +- `MYSQL_DATABASE_FILE`: The file in container where to find the database name(s) in your mysql database (cf. docker secrets). In that file, there can be several database names: one per line. You should use either MYSQL_DATABASE or MYSQL_DATABASE_FILE (see examples below). +- `MYSQLDUMP_OPTS`: Command line arguments to pass to mysqldump (see [mysqldump documentation](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html)). +- `MYSQL_SSL_OPTS`: Command line arguments to use [SSL](https://dev.mysql.com/doc/refman/5.6/en/using-encrypted-connections.html). +- `CRON_TIME`: The interval of cron job to run mysqldump. `0 3 * * sun` by default, which is every Sunday at 03:00. It uses UTC timezone. +- `MAX_BACKUPS`: The number of backups to keep. When reaching the limit, the old backup will be discarded. No limit by default. +- `INIT_BACKUP`: If set, create a backup when the container starts. +- `INIT_RESTORE_LATEST`: If set, restores latest backup. +- `EXIT_BACKUP`: If set, create a backup when the container stops. +- `TIMEOUT`: Wait a given number of seconds for the database to be ready and make the first backup, `10s` by default. After that time, the initial attempt for backup gives up and only the Cron job will try to make a backup. +- `GZIP_LEVEL`: Specify the level of gzip compression from 1 (quickest, least compressed) to 9 (slowest, most compressed), default is 6. +- `USE_PLAIN_SQL`: If set, back up and restore plain SQL files without gzip. +- `TZ`: Specify TIMEZONE in Container. E.g. "Europe/Berlin". Default is UTC. +- `REMOVE_DUPLICATES`: Use [fdupes](https://github.com/adrianlopezroche/fdupes) to remove duplicate database dumps + +If you want to make this image the perfect companion of your MySQL container, use [docker-compose](https://docs.docker.com/compose/). You can add more services that will be able to connect to the MySQL image using the name `my_mariadb`, note that you only expose the port `3306` internally to the servers and not to the host: + +### Docker-compose with MYSQL_PASS env var: + +```yaml +version: "2" +services: + mariadb: + image: mariadb + container_name: my_mariadb + expose: + - 3306 + volumes: + - data:/var/lib/mysql + # If there is not scheme, restore the last created backup (if exists) + - ${VOLUME_PATH}/backup/latest.${DATABASE_NAME}.sql.gz:/docker-entrypoint-initdb.d/database.sql.gz + environment: + - MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} + - MYSQL_DATABASE=${DATABASE_NAME} + restart: unless-stopped + + mysql-cron-backup: + image: fradelg/mysql-cron-backup + depends_on: + - mariadb + volumes: + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_HOST=my_mariadb + - MYSQL_USER=root + - MYSQL_PASS=${MARIADB_ROOT_PASSWORD} + - MAX_BACKUPS=15 + - INIT_BACKUP=0 + # Every day at 03:00 + - CRON_TIME=0 3 * * * + # Make it small + - GZIP_LEVEL=9 + # As of MySQL 8.0.21 this is needed + - MYSQLDUMP_OPTS=--no-tablespaces + restart: unless-stopped + +volumes: + data: +``` + +### Docker-compose using docker secrets: + +The database root password passed to docker container by using [docker secrets](https://docs.docker.com/engine/swarm/). + +In example below, docker is in classic 'docker engine mode' (iow. not swarm mode) and secret sources are local files on host filesystem. + +Alternatively, secrets can be stored in docker secrets engine (iow. not in host filesystem). + +```yaml +version: "3.7" + +secrets: + # Place your secret file somewhere on your host filesystem, with your password inside + mysql_root_password: + file: ./secrets/mysql_root_password + mysql_user: + file: ./secrets/mysql_user + mysql_password: + file: ./secrets/mysql_password + mysql_database: + file: ./secrets/mysql_database + +services: + mariadb: + image: mariadb:10 + container_name: my_mariadb + expose: + - 3306 + volumes: + - data:/var/lib/mysql + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password + - MYSQL_USER_FILE=/run/secrets/mysql_user + - MYSQL_PASSWORD_FILE=/run/secrets/mysql_password + - MYSQL_DATABASE_FILE=/run/secrets/mysql_database + secrets: + - mysql_root_password + - mysql_user + - mysql_password + - mysql_database + restart: unless-stopped + + backup: + build: . + image: fradelg/mysql-cron-backup + depends_on: + - mariadb + volumes: + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_HOST=my_mariadb + # Alternatively to MYSQL_USER_FILE, we can use MYSQL_USER=root to use root user instead + - MYSQL_USER_FILE=/run/secrets/mysql_user + # Alternatively, we can use /run/secrets/mysql_root_password when using root user + - MYSQL_PASS_FILE=/run/secrets/mysql_password + - MYSQL_DATABASE_FILE=/run/secrets/mysql_database + - MAX_BACKUPS=10 + - INIT_BACKUP=1 + - CRON_TIME=0 0 * * * + secrets: + - mysql_user + - mysql_password + - mysql_database + restart: unless-stopped + +volumes: + data: + +``` ## Restore from a backup -See the list of backups, you can run: +### List all available backups : - docker exec backup ls /backup +See the list of backups in your running docker container, just write in your favorite terminal: -To restore database from a certain backup, simply run: +```bash +docker container exec <your_mysql_backup_container_name> ls /backup +``` - docker exec backup /restore.sh /backup/2015.08.06.171901 +### Restore using a compose file + +To restore a database from a certain backup you may have to specify the database name in the variable MYSQL_DATABASE: + +```YAML +mysql-cron-backup: + image: fradelg/mysql-cron-backup + command: "/restore.sh /backup/201708060500.${DATABASE_NAME}.sql.gz" + depends_on: + - mariadb + volumes: + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_HOST=my_mariadb + - MYSQL_USER=root + - MYSQL_PASS=${MARIADB_ROOT_PASSWORD} + - MYSQL_DATABASE=${DATABASE_NAME} +``` +### Restore using a docker command + +```bash +docker container exec <your_mysql_backup_container_name> /restore.sh /backup/<your_sql_backup_gz_file> +``` + +if no database name is specified, `restore.sh` will try to find the database name from the backup file. + +### Automatic backup and restore on container starts and stops + +Set `INIT_RESTORE_LATEST` to automatic restore the last backup on startup. +Set `EXIT_BACKUP` to automatic create a last backup on shutdown. + +```yaml + mysql-cron-backup: + image: fradelg/mysql-cron-backup + depends_on: + - mariadb + volumes: + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_HOST=my_mariadb + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASS=${MYSQL_PASSWORD} + - MAX_BACKUPS=15 + - INIT_RESTORE_LATEST=1 + - EXIT_BACKUP=1 + # Every day at 03:00 + - CRON_TIME=0 3 * * * + # Make it small + - GZIP_LEVEL=9 + restart: unless-stopped + +volumes: + data: +``` + +Docker database image could expose a directory you could add files as init sql script. + +```yaml + mysql: + image: mysql + expose: + - 3306 + volumes: + - data:/var/lib/mysql + # If there is not scheme, restore using the init script (if exists) + - ./init-script.sql:/docker-entrypoint-initdb.d/database.sql.gz + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${DATABASE_NAME} + restart: unless-stopped +``` + +```yaml + mariadb: + image: mariadb + expose: + - 3306 + volumes: + - data:/var/lib/mysql + # If there is not scheme, restore using the init script (if exists) + - ./init-script.sql:/docker-entrypoint-initdb.d/database.sql.gz + environment: + - MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} + - MYSQL_DATABASE=${DATABASE_NAME} + restart: unless-stopped +``` \ No newline at end of file diff --git a/backup.sh b/backup.sh old mode 100644 new mode 100755 index 4f90970..499575d --- a/backup.sh +++ b/backup.sh @@ -1,29 +1,67 @@ #!/bin/bash -[ -z "${MYSQL_USER}" ] && { echo "=> MYSQL_USER cannot be empty" && exit 1; } -[ -z "${MYSQL_PASS}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; } -DATE=`date +%Y%m%d%H%M` -echo "=> Backup started at $DATE" -databases=`mysql -u $MYSQL_USER -p$MYSQL_PASS -e "SHOW DATABASES;" | tr -d "| " | grep -v Database` -for db in $databases; do - if [[ "$db" != "information_schema" ]] && [[ "$db" != "performance_schema" ]] && [[ "$db" != "mysql" ]] && [[ "$db" != _* ]] ; then - echo "Dumping database: $db" +# Get hostname: try read from file, else get from env +[ -z "${MYSQL_HOST_FILE}" ] || { MYSQL_HOST=$(head -1 "${MYSQL_HOST_FILE}"); } +[ -z "${MYSQL_HOST}" ] && { echo "=> MYSQL_HOST cannot be empty" && exit 1; } +# Get username: try read from file, else get from env +[ -z "${MYSQL_USER_FILE}" ] || { MYSQL_USER=$(head -1 "${MYSQL_USER_FILE}"); } +[ -z "${MYSQL_USER}" ] && { echo "=> MYSQL_USER cannot be empty" && exit 1; } +# Get password: try read from file, else get from env, else get from MYSQL_PASSWORD env +[ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); } +[ -z "${MYSQL_PASS:=$MYSQL_PASSWORD}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; } +# Get database name(s): try read from file, else get from env +# Note: when from file, there can be one database name per line in that file +[ -z "${MYSQL_DATABASE_FILE}" ] || { MYSQL_DATABASE=$(cat "${MYSQL_DATABASE_FILE}"); } +# Get level from env, else use 6 +[ -z "${GZIP_LEVEL}" ] && { GZIP_LEVEL=6; } + +DATE=$(date +%Y%m%d%H%M) +echo "=> Backup started at $(date "+%Y-%m-%d %H:%M:%S")" +DATABASES=${MYSQL_DATABASE:-${MYSQL_DB:-$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" $MYSQL_SSL_OPTS -e "SHOW DATABASES;" | tr -d "| " | grep -v Database)}} +for db in ${DATABASES} +do + if [[ "$db" != "information_schema" ]] \ + && [[ "$db" != "performance_schema" ]] \ + && [[ "$db" != "mysql" ]] \ + && [[ "$db" != "sys" ]] \ + && [[ "$db" != _* ]] + then + echo "==> Dumping database: $db" FILENAME=/backup/$DATE.$db.sql - if mysqldump -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS $db > $FILENAME ;then - gzip -f $FILENAME + LATEST=/backup/latest.$db.sql + BASIC_OPTS="--single-transaction" + if [ -n "$REMOVE_DUPLICATES" ] + then + BASIC_OPTS="$BASIC_OPTS --skip-dump-date" + fi + if mysqldump $BASIC_OPTS $MYSQLDUMP_OPTS -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" $MYSQL_SSL_OPTS "$db" > "$FILENAME" + then + EXT= + if [ -z "${USE_PLAIN_SQL}" ] + then + echo "==> Compressing $db with LEVEL $GZIP_LEVEL" + gzip "-$GZIP_LEVEL" -n -f "$FILENAME" + EXT=.gz + FILENAME=$FILENAME$EXT + LATEST=$LATEST$EXT + fi + BASENAME=$(basename "$FILENAME") + echo "==> Creating symlink to latest backup: $BASENAME" + rm "$LATEST" 2> /dev/null + cd /backup || exit && ln -s "$BASENAME" "$(basename "$LATEST")" + if [ -n "$REMOVE_DUPLICATES" ] + then + echo "==> Removing duplicate database dumps" + fdupes -idN /backup/ + fi + if [ -n "$MAX_BACKUPS" ] + then + # Execute the delete script, delete older backup or other custom delete script + /delete.sh "$db" $EXT + fi else - rm -rf $FILENAME + rm -rf "$FILENAME" fi fi done - -if [ -n "$MAX_BACKUPS" ]; then - while [ `ls -1 /backup | wc -l` -gt "$MAX_BACKUPS" ]; - do - TARGET=`ls -1 /backup | sort | head -n 1` - echo "Backup ${TARGET} is deleted" - rm -rf /backup/${TARGET} - done -fi - -echo "=> Backup done" +echo "=> Backup process finished at $(date "+%Y-%m-%d %H:%M:%S")" diff --git a/delete.sh b/delete.sh new file mode 100755 index 0000000..929ed7b --- /dev/null +++ b/delete.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +db=$1 +EXT=$2 + +# This file could be customized to create custom delete strategy + +while [ "$(find /backup -maxdepth 1 -name "*.$db.sql$EXT" -type f | wc -l)" -gt "$MAX_BACKUPS" ] +do + TARGET=$(find /backup -maxdepth 1 -name "*.$db.sql$EXT" -type f | sort | head -n 1) + echo "==> Max number of ($MAX_BACKUPS) backups reached. Deleting ${TARGET} ..." + rm -rf "${TARGET}" + echo "==> Backup ${TARGET} deleted" +done \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5ac0e9d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,40 @@ +services: + mariadb: + image: mariadb:10.11 + container_name: my_mariadb + security_opt: + - seccomp:unconfined + expose: + - 3306 + volumes: + - data:/var/lib/mysql + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_DATABASE=${DATABASE_NAME} + - MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} + - MYSQL_ALLOW_EMPTY_ROOT_PASSWORD=yes + restart: unless-stopped + healthcheck: + test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect" ] + timeout: 5s + retries: 10 + + backup: + build: . + image: fradelg/mysql-cron-backup + depends_on: + mariadb: + condition: service_healthy + volumes: + - ${VOLUME_PATH}/backup:/backup + environment: + - MYSQL_HOST=my_mariadb + - MYSQL_USER=root + - MYSQL_PASS=${MARIADB_ROOT_PASSWORD} + - MAX_BACKUPS=1 + - INIT_BACKUP=1 + - CRON_TIME=0 0 * * * + restart: unless-stopped + +volumes: + data: diff --git a/restore.sh b/restore.sh old mode 100644 new mode 100755 index d9dd5c4..6c03634 --- a/restore.sh +++ b/restore.sh @@ -1,6 +1,42 @@ #!/bin/bash -echo "=> Restore database from \$1" -if mysql -h $MYSQL_HOST -P $MYSQL_POR -u $MYSQL_USER -p$MYSQL_PASS < \$1 ;then + +# Get hostname: try read from file, else get from env +[ -z "${MYSQL_HOST_FILE}" ] || { MYSQL_HOST=$(head -1 "${MYSQL_HOST_FILE}"); } +[ -z "${MYSQL_HOST}" ] && { echo "=> MYSQL_HOST cannot be empty" && exit 1; } +# Get username: try read from file, else get from env +[ -z "${MYSQL_USER_FILE}" ] || { MYSQL_USER=$(head -1 "${MYSQL_USER_FILE}"); } +[ -z "${MYSQL_USER}" ] && { echo "=> MYSQL_USER cannot be empty" && exit 1; } +# Get password: try read from file, else get from env, else get from MYSQL_PASSWORD env +[ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); } +[ -z "${MYSQL_PASS:=$MYSQL_PASSWORD}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; } + +if [ "$#" -ne 1 ] +then + echo "You must pass the path of the backup file to restore" + exit 1 +fi + +set -o pipefail + +if [ -z "${USE_PLAIN_SQL}" ] +then + SQL=$(gunzip -c "$1") +else + SQL=$(cat "$1") +fi + +DB_NAME=${MYSQL_DATABASE:-${MYSQL_DB}} +if [ -z "${DB_NAME}" ] +then + echo "=> Searching database name in $1" + DB_NAME=$(echo "$SQL" | grep -oE '(Database: (.+))' | cut -d ' ' -f 2) +fi +[ -z "${DB_NAME}" ] && { echo "=> Database name not found" && exit 1; } + +echo "=> Restore database $DB_NAME from $1" + +if echo "$SQL" | mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" $MYSQL_SSL_OPTS "$DB_NAME" +then echo "=> Restore succeeded" else echo "=> Restore failed" diff --git a/run.sh b/run.sh index 1d3fefa..17a420c 100755 --- a/run.sh +++ b/run.sh @@ -1,21 +1,42 @@ #!/bin/bash -touch /mysql_backup.log tail -F /mysql_backup.log & -if [ -n "${INIT_BACKUP}" ]; then +if [ "${INIT_BACKUP:-0}" -gt "0" ]; then echo "=> Create a backup on the startup" /backup.sh elif [ -n "${INIT_RESTORE_LATEST}" ]; then echo "=> Restore latest backup" - until nc -z $MYSQL_HOST $MYSQL_PORT + until nc -z "$MYSQL_HOST" "$MYSQL_PORT" do echo "waiting database container..." sleep 1 done - ls -d -1 /backup/* | tail -1 | xargs /restore.sh + # Needed to exclude the 'latest.<database>.sql.gz' file, consider only filenames starting with number + # Only data-tagged backups, eg. '202212250457.database.sql.gz', must be trapped by the regex + find /backup -maxdepth 1 -name '[0-9]*.*.sql.gz' | sort | tail -1 | xargs /restore.sh fi -echo "${CRON_TIME} /backup.sh >> /mysql_backup.log 2>&1" > /crontab.conf -crontab /crontab.conf -echo "=> Running cron task manager" -exec crond -f +function final_backup { + echo "=> Captured trap for final backup" + echo "=> Requested last backup at $(date "+%Y-%m-%d %H:%M:%S")" + exec /backup.sh + exit 0 +} + +if [ -n "${EXIT_BACKUP}" ]; then + echo "=> Listening on container shutdown gracefully to make last backup before close" + trap final_backup SIGHUP SIGINT SIGTERM +fi + +touch /HEALTHY.status + +echo "${CRON_TIME} /backup.sh >> /mysql_backup.log 2>&1" > /tmp/crontab.conf +crontab /tmp/crontab.conf +echo "=> Running cron task manager in foreground" +crond -f -l 8 -L /mysql_backup.log & + +echo "Listening on crond, and wait..." + +tail -f /dev/null & wait $! + +echo "Script is shutted down." \ No newline at end of file