refactor: v2 (#5)
Some checks failed
Build and Release Docker Image / Create and Push Multi-Arch Manifest (push) Has been cancelled
Build and Release Docker Image / Build and Push arm64 (push) Has been cancelled
Build and Release Docker Image / Build and Push amd64 (push) Has been cancelled

Reviewed-on: #5
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
This commit was merged in pull request #5.
This commit is contained in:
2025-02-07 20:56:02 +01:00
committed by t.behrendt
parent 07832050dc
commit 6ff6e8759e
8 changed files with 384 additions and 167 deletions

View File

@@ -1,28 +0,0 @@
kind: pipeline
type: docker
name: build-and-push-container
triggers:
branch:
- main
event:
- push
steps:
- name: build
image: docker:20
commands:
- docker build -t gitea.t000-n.de/t.behrendt/backupsidecar:${DRONE_COMMIT:0:8} .
- name: push
image: docker:20
settings:
username:
from_secret: REGISTRY_USERNAME
password:
from_secret: REGISTRY_PASSWORD
repo: gitea.t000-n.de/t.behrendt/backupsidecar
tags:
- latest
- ${DRONE_COMMIT:0:8}

83
.gitea/workflows/cd.yaml Normal file
View File

@@ -0,0 +1,83 @@
name: Build and Release Docker Image
on:
push:
branches:
- main
jobs:
build_amd64:
name: Build and Push amd64
runs-on: [ubuntu-latest, linux_amd64]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: gitea.t000-n.de
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker image for amd64
run: |
docker build \
--platform linux/amd64 \
-f Dockerfile \
-t gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-amd64 \
.
- name: Push Docker image for amd64
run: |
docker push gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-amd64
build_arm64:
name: Build and Push arm64
runs-on: [ubuntu-latest, linux_arm64]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: gitea.t000-n.de
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker image for arm64
run: |
docker build \
--platform linux/arm64 \
-f Dockerfile \
-t gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-arm64 \
.
- name: Push Docker image for arm64
run: |
docker push gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-arm64
manifest:
name: Create and Push Multi-Arch Manifest
needs: [build_amd64, build_arm64]
runs-on: ubuntu-latest
steps:
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: gitea.t000-n.de
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Create and push manifest with git SHA
run: |
docker manifest create gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }} \
gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-amd64 \
gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-arm64
docker manifest push gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}
docker manifest create gitea.t000-n.de/t.behrendt/backupsidecar:latest \
gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-amd64 \
gitea.t000-n.de/t.behrendt/backupsidecar:${{ github.sha }}-arm64
docker manifest push gitea.t000-n.de/t.behrendt/backupsidecar:latest

1
.gitignore vendored
View File

@@ -1 +0,0 @@
dockerBuildAndPush.sh

View File

@@ -1,14 +1,16 @@
FROM alpine:3.17 FROM alpine:3.21
# Setup correct TZ RUN apk update && apk add --no-cache \
RUN apk add alpine-conf bash \
RUN /sbin/setup-timezone -z UTC curl \
restic \
RUN apk add restic curl postgresql-client \
jq
WORKDIR /app WORKDIR /app
COPY ./src/entry.sh /app/ COPY src/backup.sh /app/backup.sh
COPY ./src/backup.sh /app/
CMD [ "/bin/sh", "entry.sh" ] RUN chmod +x /app/backup.sh
ENTRYPOINT ["/app/backup.sh"]

127
README.md
View File

@@ -1,20 +1,113 @@
# backupsidecar # BackupSidecar
Backup sidecar that automatically creates backups of one PVC and saves it to another PVC via restic BackupSidecar is a lightweight backup solution designed to run as a cron job in Kubernetes. It automates backups using Restic and supports both directory and PostgreSQL database backups. Notifications are sent via Gotify to keep you informed of backup results.
## Function ## Configuration
A cronjob inside the container runs in the configured interval creating the backup and purging old backups.
A notification is sent to gotify on completion of the backup or on error of either the backup or purge.
## Environment Variables BackupSidecar is configured through environment variables. Below is a breakdown of the available settings.
| ENV Variable | Required |Description| Example Value
|--------------|----------|--------------|- ### General Settings
|INTERVAL|yes|cronjob interval string|15 14 * * *|
|RESTIC_REPOSITORY|yes|path of the restic repository|/mnt/backups/gitea| These variables apply to both directory and PostgreSQL backups.
|SOURCEDIR |yes|path of the path to backup|/mnt/toBackup/|
|KEEPLAST|yes|number of increments to keep (keep in mind that the number of backups to keep correlates with the interval in which they are created) |10| - **`BACKUP_MODE`** _(optional)_ - Defines the backup type (`directory` or `postgres`). Defaults to `directory`.
|RESTIC_PASSWORD|yes|password for the restic repository|******| - **`RESTIC_PASSWORD`** _(required)_ - The encryption password for Restic.
|RUNONSTART|no|set to true to force a backup at the start of the container|true| - **`RESTIC_REPOSITORY`** _(required)_ - The URI of the Restic repository (e.g., `rest:http://your-rest-server:8000/backup`).
|GOTIFYHOST|yes|URL of the gotify server (without trailing slash)|https://gotify.example.com| - **`RESTIC_REST_USERNAME`** _(optional)_ - The username for REST server authentication.
|GOTIFYTOKEN|yes|gotify app token|******| - **`RESTIC_REST_PASSWORD`** _(optional)_ - The password for REST server authentication.
|GOTIFYTOPIC|yes|gotify topic to include in the notification|gotify| - **`GOTIFYHOST`** _(required)_ - The Gotify server URL.
- **`GOTIFYTOKEN`** _(required)_ - The API token for Gotify.
- **`GOTIFYTOPIC`** _(required)_ - The topic under which backup notifications will be sent.
### Directory Backup
When running in `directory` mode, the following variable must be set:
- **`SOURCEDIR`** _(required)_ - The path of the directory to be backed up.
### PostgreSQL Backup
For `postgres` mode, the following database-related variables are required:
- **`PGHOST`** _(required)_ - The hostname of the PostgreSQL server.
- **`PGDATABASE`** _(required)_ - The name of the database to back up.
- **`PGUSER`** _(required)_ - The PostgreSQL username.
- **`PGPORT`** _(optional)_ - The port for PostgreSQL (defaults to `5432`).
- **`PGPASSWORD`** _(optional)_ - The password for authentication. Setting this prevents interactive prompts.
- **`PG_DUMP_ARGS`** _(optional)_ - Additional flags for `pg_dump`.
## Dependencies
Ensure the following commands are available in the container:
- `restic`
- `curl`
- `jq`
- `pg_dump` _(only required for `postgres` mode)_
## Usage
Example Kubernetes CronJob manifest for running BackupSidecar as a cron job for directory backups in minimal configuration:
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: backupsidecar-cron
namespace: authentik
spec:
schedule: "0 7 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 3
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: OnFailure
containers:
- name: backupsidecar
image: backupsidecar:latest
env:
- name: RESTIC_REPOSITORY
value: "rest:http://rest-server:8000/backup"
- name: RESTIC_PASSWORD
valueFrom:
secretKeyRef:
name: backupsidecar-secret
key: restic_password
- name: BACKUP_MODE
value: "directory" # or "postgres"
- name: SOURCEDIR
value: "/data/source"
- name: GOTIFYHOST
value: "http://gotify.example.com"
- name: GOTIFYTOKEN
valueFrom:
secretKeyRef:
name: backupsidecar-secret
key: gotify_token
- name: GOTIFYTOPIC
value: "Backup Notification"
# (For PostgreSQL mode, add PGHOST, PGDATABASE, PGUSER, PGPORT, PGPASSWORD)
volumeMounts:
- name: source-data
mountPath: /data/source
restartPolicy: OnFailure
volumes:
- name: source-data
persistentVolumeClaim:
claimName: source-data-pvc
```
## Notifications
The script sends success or failure notifications via Gotify.
Example success notification:
```
Backup successful. Snapshot 56ff6a909a44e01f67d2d88f9a76aa713d437809d7ed14a2361e28893f38befb: files new: 1, files changed: 0, data added: 1019 bytes in 0.277535184 sec
```

View File

@@ -1,62 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backup-sidecar
spec:
selector:
matchLabels:
app: backup-sidecar
template:
metadata:
labels:
app: backup-sidecar
spec:
containers:
- name: backup-sidecar
image: gitea.t000-n.de/t.behrendt/backupsidecar:latest
resources:
requests:
cpu: 128m
memory: 128Mi
limits:
cpu: 256m
memory: 1024Mi
volumeMounts:
- mountPath: /mnt/toBackup
name: data
readOnly: true
- mountPath: /mnt/backups/gitea
name: nfs-backup
subPath: gitea
env:
- name: INTERVAL
value: "15 14 * * *"
- name: RESTIC_REPOSITORY
value: /mnt/backups/gitea
- name: SOURCEDIR
value: "/mnt/toBackup/"
- name: KEEPLAST
value: "100"
- name: RESTIC_PASSWORD
valueFrom:
secretKeyRef:
name: restic-credentials
key: password
- name: RUNONSTART
value: "false"
- name: GOTIFYHOST
value: "https://<gotify-URL>"
- name: GOTIFYTOKEN
valueFrom:
secretKeyRef:
name: gotify-credentials
key: token
- name: GOTIFYTOPIC
value: "gitea"
volumes:
- name: data
persistentVolumeClaim:
claimName: data
- name: nfs-backup
persistentVolumeClaim:
claimName: backup-nfs

View File

@@ -1,40 +1,188 @@
#!/bin/sh #!/bin/bash
set -euo pipefail
GOTIFYURL="$GOTIFYHOST/message?token=$GOTIFYTOKEN" #######################################
# Determine backup mode from the environment only.
# Valid values: "directory" or "postgres".
# Default to "directory" if not provided.
#######################################
BACKUP_MODE="${BACKUP_MODE:-directory}"
echo "$(date +"%Y-%m-%dT%T") - Starting backup" #######################################
# Check for required external commands.
restic backup \ #######################################
--verbose \ REQUIRED_CMDS=(restic curl jq)
$SOURCEDIR if [ "$BACKUP_MODE" = "postgres" ]; then
REQUIRED_CMDS+=(pg_dump)
RESTIC_BACKUP_RETURN=$?
if [ $RESTIC_BACKUP_RETURN -eq 0 ]; then
MSG_BACKUP_SUCCESS="Backup successful"
echo "$(date +"%Y-%m-%dT%T") - $MSG_BACKUP_SUCCESS"
curl -s -X POST "$GOTIFYURL" -F "title=$GOTIFYTOPIC" -F "message=$MSG_BACKUP_SUCCESS"
else
MSG_BACKUP_ERR="Backup failed with error code $RESTIC_BACKUP_RETURN"
echo "$(date +"%Y-%m-%dT%T") - MSG_BACKUP_ERR"
curl -s -X POST "$GOTIFYURL" -F "title=$GOTIFYTOPIC" -F "message=$MSG_BACKUP_ERR"
exit $RESTIC_BACKUP_RETURN
fi fi
for cmd in "${REQUIRED_CMDS[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: Required command '$cmd' is not installed." >&2
exit 1
fi
done
MSG_PURGE_START="$(date +"%Y-%m-%dT%T") - Removing old backups" #######################################
echo $MSG_PURGE_START # Validate common required environment variables.
#######################################
# Gotify notification settings.
: "${GOTIFYHOST:?Environment variable GOTIFYHOST is not set}"
: "${GOTIFYTOKEN:?Environment variable GOTIFYTOKEN is not set}"
: "${GOTIFYTOPIC:?Environment variable GOTIFYTOPIC is not set}"
restic forget --keep-last $KEEPLAST --prune # Restic encryption password.
RESTIC_PURGE_RETURN=$? : "${RESTIC_PASSWORD:?Environment variable RESTIC_PASSWORD is not set}"
if [ $RESTIC_PURGE_RETURN -eq 0 ]; then # Use the repository URI directly from the environment.
echo "$(date +"%Y-%m-%dT%T") - Purge successful" # Example: export RESTIC_REPOSITORY="rest:http://your-rest-server:8000/backup"
else : "${RESTIC_REPOSITORY:?Environment variable RESTIC_REPOSITORY is not set}"
MSG_PURGE_ERR="Purge failed with error code $MSG_PURGE_ERR"
echo "$(date +"%Y-%m-%dT%T") - $MSG_PURGE_ERR"
curl -s -X POST "$GOTIFYURL" -F "title=$GOTIFYTOPIC" -F "message=$MSG_PURGE_ERR"
exit $RESTIC_PURGE_RETURN
fi
echo "$(date +"%Y-%m-%dT%T") - Going back to sleep..." #######################################
# Validate mode-specific environment variables.
#######################################
case "$BACKUP_MODE" in
directory)
: "${SOURCEDIR:?Environment variable SOURCEDIR is not set (required for directory backup mode)}"
;;
postgres)
: "${PGHOST:?Environment variable PGHOST is not set (required for PostgreSQL backup mode)}"
: "${PGDATABASE:?Environment variable PGDATABASE is not set (required for PostgreSQL backup mode)}"
: "${PGUSER:?Environment variable PGUSER is not set (required for PostgreSQL backup mode)}"
# Optional: default PGPORT to 5432.
: "${PGPORT:=5432}"
if [ -z "${PGPASSWORD:-}" ]; then
echo "Warning: Environment variable PGPASSWORD is not set. pg_dump may fail if authentication is required."
fi
;;
*)
echo "Error: Unknown backup mode '$BACKUP_MODE'. Valid modes are 'directory' and 'postgres'." >&2
exit 1
;;
esac
#######################################
# Build the Gotify URL.
#######################################
GOTIFYURL="${GOTIFYHOST}/message?token=${GOTIFYTOKEN}"
#######################################
# Date format for logging.
#######################################
LOG_DATE_FORMAT="%Y-%m-%dT%T"
#######################################
# Log a message with a timestamp.
# Arguments:
# Message to log.
#######################################
log() {
echo "$(date +"$LOG_DATE_FORMAT") - $*"
}
#######################################
# Send a notification via Gotify.
# Arguments:
# message: The message to send.
#######################################
send_notification() {
local message="$1"
if ! curl -s -X POST "$GOTIFYURL" -F "title=${GOTIFYTOPIC}" -F "message=${message}" >/dev/null; then
log "Warning: Failed to send notification with message: ${message}"
fi
}
#######################################
# Run the backup using restic.
# The --no-cache flag disables local caching.
# Arguments:
# $1 - The source directory to back up.
#######################################
run_restic_backup() {
local source_dir="$1"
cd "${source_dir}"
log "Starting backup of '${source_dir}' to repository ${RESTIC_REPOSITORY}"
# Capture both stdout and stderr in a variable
backup_output=$(restic -r "${RESTIC_REPOSITORY}" backup --no-cache --json --verbose . 2>&1)
# Optionally, also print the output to the console:
echo "$backup_output"
# Parse the JSON lines output for the summary message
summary=$(echo "$backup_output" | jq -r 'select(.message_type=="summary") | "Snapshot " + (.snapshot_id // "none") + ": " + "files new: " + (.files_new|tostring) + ", files changed: " + (.files_changed|tostring) + ", data added: " + (.data_added|tostring) + " bytes in " + (.total_duration|tostring) + " sec"')
# Check exit code of restic backup (assuming restic exits non-zero on error)
if [ $? -eq 0 ]; then
msg="Backup successful. $summary"
log "$msg"
send_notification "$msg"
else
exit_code=$?
msg="Backup failed with error code ${exit_code}. $backup_output"
log "$msg"
send_notification "$msg"
exit "$exit_code"
fi
}
#######################################
# Backup a directory (regular mode).
#######################################
backup_directory() {
run_restic_backup "${SOURCEDIR}"
}
#######################################
# Backup a PostgreSQL database.
# Dumps the database to a temporary directory and then backs it up.
#######################################
backup_postgres() {
log "Starting PostgreSQL backup for database '${PGDATABASE}' on host '${PGHOST}'"
# Create a temporary directory for the database dump.
TEMP_BACKUP_DIR=$(mktemp -d)
log "Created temporary directory: ${TEMP_BACKUP_DIR}"
local dump_file="${TEMP_BACKUP_DIR}/dump.sql"
log "Dumping PostgreSQL database to ${dump_file}..."
if pg_dump -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" ${PG_DUMP_ARGS:-} "${PGDATABASE}" > "${dump_file}"; then
log "Database dump created successfully."
else
local exit_code=$?
local msg="PostgreSQL dump failed with error code ${exit_code}"
log "$msg"
send_notification "$msg"
exit "$exit_code"
fi
# Back up the directory containing the dump.
run_restic_backup "${TEMP_BACKUP_DIR}"
}
#######################################
# Cleanup temporary resources.
#######################################
cleanup() {
if [ -n "${TEMP_BACKUP_DIR:-}" ] && [ -d "${TEMP_BACKUP_DIR}" ]; then
rm -rf "${TEMP_BACKUP_DIR}"
log "Removed temporary directory ${TEMP_BACKUP_DIR}"
fi
}
trap cleanup EXIT
#######################################
# Main routine.
#######################################
main() {
case "$BACKUP_MODE" in
directory)
backup_directory
;;
postgres)
backup_postgres
;;
esac
}
# Trap termination signals to log and exit cleanly.
trap 'log "Script interrupted. Exiting."; exit 1' SIGINT SIGTERM
main

View File

@@ -1,18 +0,0 @@
#!/bin/sh
mkdir /etc/cron.d
touch /etc/cron.d/backup
echo "$INTERVAL /bin/sh /app/backup.sh" > /etc/cron.d/backup
# change ownership and make the cron known to crontab
chmod 0644 /etc/cron.d/backup && crontab /etc/cron.d/backup
if [ $RUNONSTART = 'true' ]; then
echo $(date +"%Y-%m-%dT%T") "- Running initial backup"
/bin/sh /app/backup.sh
fi
# Wait until infinity
echo $(date +"%Y-%m-%dT%T") "- Starting cron"
crond -f