Compare commits

..

No commits in common. "master" and "1.10.0" have entirely different histories.

12 changed files with 89 additions and 266 deletions

View file

@ -1,33 +0,0 @@
name: build docker image
on:
push:
tags:
- "**"
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout the code
uses: actions/checkout@v5
# 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 .

40
.github/workflows/image.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: build docker image
on:
workflow_dispatch:
push:
branches:
- "**"
tags:
- "**"
jobs:
test:
runs-on: ubuntu-20.04
steps:
- name: Checkout the code
uses: actions/checkout@v2
- name: Test Bash scripts
run: sudo apt-get -qq update && sudo apt-get install -y devscripts shellcheck && make test
build:
runs-on: ubuntu-20.04
needs: test
steps:
- name: Checkout the code
uses: actions/checkout@v2
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Get latest release version number
id: docker-tag
uses: yuya-takeyama/docker-tag-from-github-ref-action@v1
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- 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 .

View file

@ -1,26 +0,0 @@
name: build docker image
on:
workflow_dispatch:
push:
branches:
- "**"
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: Checkout the code
uses: actions/checkout@v5
- 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

1
.gitignore vendored
View file

@ -1 +0,0 @@
data

View file

@ -1,28 +1,27 @@
FROM golang:1.20.4-alpine3.18 AS binary FROM golang:1.15.8-alpine3.12 AS binary
RUN apk -U add openssl git RUN apk -U add openssl git
ARG DOCKERIZE_VERSION=v0.7.0 ARG DOCKERIZE_VERSION=v0.6.1
WORKDIR /go/src/github.com/jwilder WORKDIR /go/src/github.com/jwilder
RUN git clone https://github.com/jwilder/dockerize.git && \ RUN git clone https://github.com/jwilder/dockerize.git && \
cd dockerize && \ cd dockerize && \
git checkout ${DOCKERIZE_VERSION} git checkout ${DOCKERIZE_VERSION}
WORKDIR /go/src/github.com/jwilder/dockerize WORKDIR /go/src/github.com/jwilder/dockerize
ENV GO111MODULE=on RUN go get github.com/robfig/glock
RUN go mod tidy RUN glock sync -n < GLOCKFILE
RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -o /go/bin/dockerize . RUN go install
FROM alpine:3.20.3 FROM alpine:3.15.0
LABEL maintainer "Fco. Javier Delgado del Hoyo <frandelhoyo@gmail.com>" LABEL maintainer "Fco. Javier Delgado del Hoyo <frandelhoyo@gmail.com>"
RUN apk add --update \ RUN apk add --update \
tzdata \ tzdata \
bash \ bash \
gzip \ mysql-client \
openssl \ gzip \
mysql-client=~10.11 \ openssl \
mariadb-connector-c \ mariadb-connector-c && \
fdupes && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
COPY --from=binary /go/bin/dockerize /usr/local/bin COPY --from=binary /go/bin/dockerize /usr/local/bin
@ -33,16 +32,13 @@ ENV CRON_TIME="0 3 * * sun" \
TIMEOUT="10s" \ TIMEOUT="10s" \
MYSQLDUMP_OPTS="--quick" MYSQLDUMP_OPTS="--quick"
COPY ["run.sh", "backup.sh", "restore.sh", "/delete.sh", "/"] COPY ["run.sh", "backup.sh", "restore.sh", "/"]
RUN mkdir /backup && \ RUN mkdir /backup && \
chmod 777 /backup && \ chmod 777 /backup && \
chmod 755 /run.sh /backup.sh /restore.sh /delete.sh && \ chmod 755 /run.sh /backup.sh /restore.sh && \
touch /mysql_backup.log && \ touch /mysql_backup.log && \
chmod 666 /mysql_backup.log chmod 666 /mysql_backup.log
VOLUME ["/backup"] VOLUME ["/backup"]
HEALTHCHECK --interval=2s --retries=1800 \
CMD stat /HEALTHY.status || exit 1
CMD dockerize -wait tcp://${MYSQL_HOST}:${MYSQL_PORT} -timeout ${TIMEOUT} /run.sh CMD dockerize -wait tcp://${MYSQL_HOST}:${MYSQL_PORT} -timeout ${TIMEOUT} /run.sh

View file

@ -6,7 +6,7 @@ test:
# Checking for syntax errors # Checking for syntax errors
set -e; for SCRIPT in *.sh; \ set -e; for SCRIPT in *.sh; \
do \ do \
bash -n $$SCRIPT; \ sh -n $$SCRIPT; \
done done
# Checking for bashisms (currently not failing, but only listing) # Checking for bashisms (currently not failing, but only listing)

108
README.md
View file

@ -13,36 +13,23 @@ docker container run -d \
fradelg/mysql-cron-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 ## Variables
- `MYSQL_HOST`: The host/ip of your mysql database. - `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_PORT`: The port number of your mysql database.
- `MYSQL_USER`: The username 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`: 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_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`: 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)). - `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. - `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. - `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_BACKUP`: If set, create a backup when the container starts.
- `INIT_RESTORE_LATEST`: If set, restores latest backup. - `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. - `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. - `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. - `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. - `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: 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:
@ -81,8 +68,6 @@ services:
- CRON_TIME=0 3 * * * - CRON_TIME=0 3 * * *
# Make it small # Make it small
- GZIP_LEVEL=9 - GZIP_LEVEL=9
# As of MySQL 8.0.21 this is needed
- MYSQLDUMP_OPTS=--no-tablespaces
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@ -93,23 +78,17 @@ volumes:
The database root password passed to docker container by using [docker secrets](https://docs.docker.com/engine/swarm/). 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. In example below, docker is in classic 'docker engine mode' (iow. not swarm mode) and secret source is a local file on host filesystem.
Alternatively, secrets can be stored in docker secrets engine (iow. not in host filesystem). Alternatively, secret can be stored in docker secrets engine (iow. not in host filesystem).
```yaml ```yaml
version: "3.7" version: "3.7"
secrets: secrets:
# Place your secret file somewhere on your host filesystem, with your password inside
mysql_root_password: mysql_root_password:
# Place your secret file somewhere on your host filesystem, with your password inside
file: ./secrets/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: services:
mariadb: mariadb:
@ -121,15 +100,10 @@ services:
- data:/var/lib/mysql - data:/var/lib/mysql
- ${VOLUME_PATH}/backup:/backup - ${VOLUME_PATH}/backup:/backup
environment: environment:
- MYSQL_DATABASE=${DATABASE_NAME}
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password - 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: secrets:
- mysql_root_password - mysql_root_password
- mysql_user
- mysql_password
- mysql_database
restart: unless-stopped restart: unless-stopped
backup: backup:
@ -141,18 +115,13 @@ services:
- ${VOLUME_PATH}/backup:/backup - ${VOLUME_PATH}/backup:/backup
environment: environment:
- MYSQL_HOST=my_mariadb - MYSQL_HOST=my_mariadb
# Alternatively to MYSQL_USER_FILE, we can use MYSQL_USER=root to use root user instead - MYSQL_USER=root
- MYSQL_USER_FILE=/run/secrets/mysql_user - MYSQL_PASS_FILE=/run/secrets/mysql_root_password
# 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 - MAX_BACKUPS=10
- INIT_BACKUP=1 - INIT_BACKUP=1
- CRON_TIME=0 0 * * * - CRON_TIME=0 0 * * *
secrets: secrets:
- mysql_user - mysql_root_password
- mysql_password
- mysql_database
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@ -194,65 +163,4 @@ mysql-cron-backup:
docker container exec <your_mysql_backup_container_name> /restore.sh /backup/<your_sql_backup_gz_file> 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. 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
```

View file

@ -1,23 +1,15 @@
#!/bin/bash #!/bin/bash
# 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; } [ -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 # If provided, take password from file
[ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); } [ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); }
# Alternatively, take it from env var
[ -z "${MYSQL_PASS:=$MYSQL_PASSWORD}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; } [ -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; } [ -z "${GZIP_LEVEL}" ] && { GZIP_LEVEL=6; }
DATE=$(date +%Y%m%d%H%M) DATE=$(date +%Y%m%d%H%M)
echo "=> Backup started at $(date "+%Y-%m-%d %H:%M:%S")" 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)}} DATABASES=${MYSQL_DATABASE:-${MYSQL_DB:-$(mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" -e "SHOW DATABASES;" | tr -d "| " | grep -v Database)}}
for db in ${DATABASES} for db in ${DATABASES}
do do
if [[ "$db" != "information_schema" ]] \ if [[ "$db" != "information_schema" ]] \
@ -29,18 +21,13 @@ do
echo "==> Dumping database: $db" echo "==> Dumping database: $db"
FILENAME=/backup/$DATE.$db.sql FILENAME=/backup/$DATE.$db.sql
LATEST=/backup/latest.$db.sql LATEST=/backup/latest.$db.sql
BASIC_OPTS="--single-transaction" if mysqldump --single-transaction "$MYSQLDUMP_OPTS" -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" "$db" > "$FILENAME"
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 then
EXT= EXT=
if [ -z "${USE_PLAIN_SQL}" ] if [ -z "${USE_PLAIN_SQL}" ]
then then
echo "==> Compressing $db with LEVEL $GZIP_LEVEL" echo "==> Compressing $db with LEVEL $GZIP_LEVEL"
gzip "-$GZIP_LEVEL" -n -f "$FILENAME" gzip "-$GZIP_LEVEL" -f "$FILENAME"
EXT=.gz EXT=.gz
FILENAME=$FILENAME$EXT FILENAME=$FILENAME$EXT
LATEST=$LATEST$EXT LATEST=$LATEST$EXT
@ -49,15 +36,15 @@ do
echo "==> Creating symlink to latest backup: $BASENAME" echo "==> Creating symlink to latest backup: $BASENAME"
rm "$LATEST" 2> /dev/null rm "$LATEST" 2> /dev/null
cd /backup || exit && ln -s "$BASENAME" "$(basename "$LATEST")" 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" ] if [ -n "$MAX_BACKUPS" ]
then then
# Execute the delete script, delete older backup or other custom delete script while [ "$(find /backup -maxdepth 1 -name "*.$db.sql$EXT" -type f | wc -l)" -gt "$MAX_BACKUPS" ]
/delete.sh "$db" $EXT 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
fi fi
else else
rm -rf "$FILENAME" rm -rf "$FILENAME"

View file

@ -1,14 +0,0 @@
#!/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

View file

@ -1,9 +1,8 @@
version: "2"
services: services:
mariadb: mariadb:
image: mariadb:10.11 image: mariadb:10
container_name: my_mariadb container_name: my_mariadb
security_opt:
- seccomp:unconfined
expose: expose:
- 3306 - 3306
volumes: volumes:
@ -12,19 +11,13 @@ services:
environment: environment:
- MYSQL_DATABASE=${DATABASE_NAME} - MYSQL_DATABASE=${DATABASE_NAME}
- MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
- MYSQL_ALLOW_EMPTY_ROOT_PASSWORD=yes
restart: unless-stopped restart: unless-stopped
healthcheck:
test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect" ]
timeout: 5s
retries: 10
backup: backup:
build: . build: .
image: fradelg/mysql-cron-backup image: fradelg/mysql-cron-backup
depends_on: depends_on:
mariadb: - mariadb
condition: service_healthy
volumes: volumes:
- ${VOLUME_PATH}/backup:/backup - ${VOLUME_PATH}/backup:/backup
environment: environment:
@ -35,6 +28,6 @@ services:
- INIT_BACKUP=1 - INIT_BACKUP=1
- CRON_TIME=0 0 * * * - CRON_TIME=0 0 * * *
restart: unless-stopped restart: unless-stopped
volumes: volumes:
data: data:

View file

@ -1,25 +1,20 @@
#!/bin/bash #!/bin/bash
# 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; } [ -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 # If provided, take password from file
[ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); } [ -z "${MYSQL_PASS_FILE}" ] || { MYSQL_PASS=$(head -1 "${MYSQL_PASS_FILE}"); }
[ -z "${MYSQL_PASS:=$MYSQL_PASSWORD}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; } # Alternatively, take it from env var
[ -z "${MYSQL_PASS}" ] && { echo "=> MYSQL_PASS cannot be empty" && exit 1; }
if [ "$#" -ne 1 ] if [ "$#" -ne 1 ]
then then
echo "You must pass the path of the backup file to restore" echo "You must pass the path of the backup file to restore"
exit 1
fi fi
set -o pipefail set -o pipefail
if [ -z "${USE_PLAIN_SQL}" ] if [ -z "${USE_PLAIN_SQL}" ]
then then
SQL=$(gunzip -c "$1") SQL=$(gunzip -c "$1")
else else
SQL=$(cat "$1") SQL=$(cat "$1")
@ -35,7 +30,7 @@ fi
echo "=> Restore database $DB_NAME from $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" if echo "$SQL" | mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" "$DB_NAME"
then then
echo "=> Restore succeeded" echo "=> Restore succeeded"
else else

28
run.sh
View file

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
tail -F /mysql_backup.log & tail -F /mysql_backup.log &
if [ "${INIT_BACKUP:-0}" -gt "0" ]; then if [ "${INIT_BACKUP}" -gt "0" ]; then
echo "=> Create a backup on the startup" echo "=> Create a backup on the startup"
/backup.sh /backup.sh
elif [ -n "${INIT_RESTORE_LATEST}" ]; then elif [ -n "${INIT_RESTORE_LATEST}" ]; then
@ -11,32 +11,10 @@ elif [ -n "${INIT_RESTORE_LATEST}" ]; then
echo "waiting database container..." echo "waiting database container..."
sleep 1 sleep 1
done done
# Needed to exclude the 'latest.<database>.sql.gz' file, consider only filenames starting with number find /backup -maxdepth 1 -name '*.sql.gz' | tail -1 | xargs /restore.sh
# 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 fi
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 echo "${CRON_TIME} /backup.sh >> /mysql_backup.log 2>&1" > /tmp/crontab.conf
crontab /tmp/crontab.conf crontab /tmp/crontab.conf
echo "=> Running cron task manager in foreground" echo "=> Running cron task manager in foreground"
crond -f -l 8 -L /mysql_backup.log & exec crond -f -l 8 -L /mysql_backup.log
echo "Listening on crond, and wait..."
tail -f /dev/null & wait $!
echo "Script is shutted down."