Skip to content

Commit

Permalink
Add backup scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
matthijsln committed Apr 16, 2024
1 parent 36d6119 commit be96393
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM mcuadros/ofelia:v3.0.8 as ofelia

FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND noninteractive
ARG DOCKERIZE_VERSION=v0.7.0
ARG POSTGRES_CLIENT_VERSION=15

LABEL authors="matthijsln"

ENV TZ="Europe/Amsterdam"

# musl required for Ofelia when copying from Docker image, alternative is to download binary release from GitHub like
# dockerize but recent versions are not available

RUN apt-get update && \
apt install -y -q --no-install-recommends bash wget ca-certificates gnupg2 lsb-release musl openssh-client sshpass pv zstd pigz bzip2 pbzip2 xz-utils && \
echo "deb [signed-by=/usr/share/keyrings/apt.postgresql.org.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor --yes -o /usr/share/keyrings/apt.postgresql.org.gpg && \
wget -qO - https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar xzf - -C /usr/local/bin && \
apt-get update

RUN apt-get install -y -q --no-install-recommends bash postgresql-client-${POSTGRES_CLIENT_VERSION} && \
apt-get autoremove -yqq --purge wget ca-certificates gnupg2 && \
rm -rf /var/lib/apt/lists/*

WORKDIR /home/backup
COPY *.sh .
RUN chmod +x *.sh
COPY include include

COPY --from=ofelia /usr/bin/ofelia /usr/bin/ofelia
COPY ofelia.ini.tmpl .
RUN mkdir /etc/ofelia

RUN mkdir -p /backup/temp /backup/ofelia /backup/uploaded

ENTRYPOINT ["/home/backup/entrypoint.sh"]
CMD ["/usr/bin/ofelia", "daemon", "--config", "/etc/ofelia/ofelia.ini"]
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Docker backup container

## Introduction

This container can backup a PostgreSQL cluster and directories on a schedule, compress the backup and optionally copy
the backup to a SFTP server (for example a Hetzner Storage Box).

This container can be run separately or it can be merged with an existing Docker Compose stack.

Mainly based on https://github.com/azlux/borgbackup-docker, but also uses [dockerize](https://github.com/jwilder/dockerize).

At build time the PostgreSQL client version 15 is installed. On startup and when creating a backup the major version of
the PostgreSQL server to backup is checked and if this differs the corresponding PostgreSQL client packages are
installed, so database dumps are created with the same major client version as the server.

## Docs

TODO
40 changes: 40 additions & 0 deletions backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

source ./include/msg.sh
source ./include/utils.sh
source ./include/postgresql.sh

# Defaults

[[ "$1" == "--startup" ]] && STARTUP=1

BACKUP_PG=${BACKUP_PG:-true}
PGHOST=${PGHOST:-db}
PGDATABASE=${PGDATABASE:-all}

SFTP_PATH=${SFTP_PATH:-backup}
if [[ -n "$STORAGE_BOX" ]]; then
SFTP_HOST=${STORAGE_BOX}.your-storagebox.de
SFTP_USER=${STORAGE_BOX}
fi

PG_COMPRESS=${PG_COMPRESS:-zstd}
TAR_COMPRESS=${TAR_COMPRESS:-zstd}
# Use all cores for compression, when this is unset the default is a single core
export ZSTD_NBTHREADS=${ZSTD_NBTHREADS:-0}
export XZ_DEFAULTS="-T 0"

DIR_BACKUP=/backup/temp
DIR_UPLOADED=/backup/uploaded

echo
EXITCODE=0

backup_directory
backup_postgres_databases

if [[ ! $STARTUP ]]; then
upload_backup
fi

exit $EXITCODE
33 changes: 33 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Merge this with a Docker Compose configuration to add it to a stack, for example
# by adding it to COMPOSE_FILE in an env-file.

volumes:
backup:

services:
backup:
image: ghcr.io/b3partners/backup:latest
container_name: ${COMPOSE_PROJECT_NAME}-backup
restart: unless-stopped
build:
context: .
volumes:
- backup:/backup
environment:
- "SCHEDULE"
- "LOGGING"
- "BACKUP_PG"
- "BACKUP_DIR"
- "PGHOST=${PGHOST:-db}"
- "PGDATABASE"
- "PGUSER=${PGUSER:-postgres}"
- "PGPASSWORD=${PGPASSWORD:-postgres}"
- "STORAGE_BOX"
- "SFTP_HOST"
- "SFTP_USER"
- "SFTP_PATH=${SFTP_PATH:-.}"
- "SSHPASS"
- "PG_COMPRESS"
- "TAR_COMPRESS"
- "ZSTD_NBTHREADS"
- "XZ_DEFAULTS"
19 changes: 19 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

# Using 'docker run ghcr.io/b3partners/backup bash' you can just start a shell, to manually test whether commands/connections work
if [ "$1" == "bash" ] || [ "$1" == "sh" ]; then
exec "${@}"
fi

if [ -n "$ONESHOT" ] && [ "$ONESHOT" == "true" ]; then
exec ./backup.sh
else
# Just test connections and install the same psql client version as the server, don't make a backup yet
./backup.sh --startup
# Create Ofelia config from template
export LOGGING=${LOGGING:-true}
export SCHEDULE=${SCHEDULE:-"@midnight"}
dockerize -template ofelia.ini.tmpl:/etc/ofelia/ofelia.ini
# Execute standard CMD from Dockerfile
exec "$@"
fi
21 changes: 21 additions & 0 deletions include/msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

print_msg() {
local type="INFO";
local msg="$1";
if [[ -n "$2" ]]; then
type=$1
msg=$2
fi
echo "$(date +"%Y-%m-%dT%H:%M:%S") [$type] $msg"
}

print_error() {
print_msg ERROR $1
EXITCODE=1
}

print_error_and_exit() {
print_error $1
exit 1
}
73 changes: 73 additions & 0 deletions include/postgresql.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash

# Check if versions are different and install matching client if so
install_matching_postgresql_client() {
if [[ -n "$POSTGRES_SERVER_VERSION" && "$POSTGRES_SERVER_VERSION" -ne "$POSTGRES_CLIENT_VERSION" ]]; then
print_msg "Removing the default PostgreSQL client version $POSTGRES_CLIENT_VERSION provided by Debian $(lsb_release -cs)"

apt-get -qq update || print_error_and_exit "Failed to update package list"
apt-get -qq remove -y postgresql-client* || print_error_and_exit "Failed to remove existing PostgreSQL client"

print_msg "Installing the client for remote PostgreSQL server version $POSTGRES_SERVER_VERSION"

apt-get -qq install -y "postgresql-client-$POSTGRES_SERVER_VERSION" || print_error_and_exit "Failed to install PostgreSQL client $POSTGRES_SERVER_VERSION"

print_msg "PostgreSQL client installation completed successfully"
fi
}

backup_postgres_databases() {
if [[ $BACKUP_PG != "true" ]]; then
return
fi

if [[ -z "$PGHOST" && -z "$PGPASSWORD" && -z "$PGUSER" ]]; then
print_error_and_exit "PostgreSQL env vars not all set, set BACKUP_PG to false to disable PostgreSQL backup"
fi

[[ $STARTUP ]] && dockerize -wait tcp://$PGHOST:5432

# Extract PostgreSQL client version installed via apt
POSTGRES_CLIENT_VERSION=$(dpkg -l | awk '/^ii.*postgresql-client/ {if ($2 ~ /^postgresql-client-[0-9]/) print $2}' | cut -d'-' -f3)

# Extract PostgreSQL server version from the remote server
POSTGRES_SERVER_VERSION=$(psql -tA -c "SELECT current_setting('server_version_num')::integer / 10000;")
if [[ $? -ne 0 ]]; then
print_error_and_exit "Error testing the PostgreSQL connection; please check the PG* environment variables."
fi

[[ $STARTUP ]] && print_msg "PostgreSQL backup configured, current client: $POSTGRES_CLIENT_VERSION, server version: $POSTGRES_SERVER_VERSION"

install_matching_postgresql_client

if [[ $STARTUP ]]; then
return
fi

# Do not leave dumps from deleted databases (locally, will still remain at SFTP server)
rm -f $DIR_BACKUP/*.sql.*

pg_dumpall --globals-only > $DIR_BACKUP/postgres_globals.sql

if [[ "$PGDATABASE" != "all" ]]; then
backup_postgres_database
else
DBS=$(psql -tA -c "select datname from pg_database where not datistemplate and datname <> 'postgres'")
print_msg "Backing up all following databases: $(echo $DBS | sed 's/ /, /g' )"
for PGDATABASE in $DBS; do
backup_postgres_database
done
fi
}

backup_postgres_database() {
res=$(psql -tA -c "select pg_size_pretty(pg_database_size('$PGDATABASE'));")
if [[ $? -ne 0 ]]; then
print_error "Can't get database size for database to backup \"$PGDATABASE\"!"
else
print_msg "Backing up database \"$PGDATABASE\", db size $res"
# Continue with other databases in case of error
pg_dump --create -d "$PGDATABASE" | $PG_COMPRESS | pv -i 60 -f -F "%t %a %b" 2> /tmp/progress > $DIR_BACKUP/${PGDATABASE}.sql.$PG_COMPRESS || print_error "[ERR] Error backing up \"$PGDATABASE\"!"
[[ $? -ne 0 ]] || print_msg "Elapsed time, speed and compressed size: $(cat /tmp/progress | tr '\r' '\n' | tail -2 | head -1)"
fi
}
23 changes: 23 additions & 0 deletions include/utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

backup_directory() {
if [[ -n "$BACKUP_DIR" ]]; then
print_msg "Backing up directory $BACKUP_DIR, size `du -bhs $BACKUP_DIR | awk '{print $1}'`"
if [[ ! $STARTUP ]]; then
rm $DIR_BACKUP/files.tar.* 2>/dev/null
tar acP $BACKUP_DIR | $TAR_COMPRESS > $DIR_BACKUP/files.tar.$TAR_COMPRESS || print_error "Error backing up directory $BACKUP_DIR"
fi
fi
}

upload_backup() {
if [[ -z "$SFTP_HOST" || -z "$SSHPASS" ]]; then
print_msg WARN "No SFTP_HOST/SSHPASS defined, not copying backup"
return
fi
SFTP=$SFTP_USER@$SFTP_HOST:$SFTP_PATH
print_msg "Copying backup size `du -bhs $DIR_BACKUP/ | awk '{print $1}'` to SFTP $SFTP"
rm $DIR_UPLOADED/* 2>/dev/null
mkdir -p ~/.ssh && ssh-keyscan -H $SFTP_HOST 2> /dev/null > $HOME/.ssh/known_hosts
sshpass -e scp -p $DIR_BACKUP/* $SFTP && mv $DIR_BACKUP/* $DIR_UPLOADED/ || print_error "[ERR] Error copying to SFTP server"
}
8 changes: 8 additions & 0 deletions ofelia.ini.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{ if isTrue .Env.LOGGING }}
[global]
save-folder=/backup/ofelia
{{ end }}

[job-local "backup"]
schedule={{ .Env.SCHEDULE }}
command=/home/backup/backup.sh

0 comments on commit be96393

Please sign in to comment.