Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
secrets/
|
||||||
|
ssh/id_*
|
||||||
|
ssh/known_hosts
|
||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
restic \
|
||||||
|
mariadb-client \
|
||||||
|
sqlite \
|
||||||
|
openssh-client \
|
||||||
|
bash \
|
||||||
|
&& mkdir -p /mnt/source /mnt/staging /hooks
|
||||||
|
|
||||||
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
COPY backup.sh /usr/local/bin/backup.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/backup.sh
|
||||||
|
|
||||||
|
ENV BACKUP_CRON="20 7 * * *" \
|
||||||
|
BACKUP_SOURCE="/mnt/source /mnt/staging" \
|
||||||
|
KEEP_DAILY=1 \
|
||||||
|
KEEP_WEEKLY=3 \
|
||||||
|
KEEP_MONTHLY=4 \
|
||||||
|
KEEP_YEARLY=1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# restic-backup-sidecar
|
||||||
|
|
||||||
|
Docker sidecar container for restic backups via SFTP. Runs on Alpine with restic, mariadb-client, and sqlite.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Dockerfile, entrypoint.sh, backup.sh # Shared image (copy to each service's restic/ dir)
|
||||||
|
mailcow/hooks/ # MariaDB dump via socket
|
||||||
|
vaultwarden/hooks/ # SQLite .backup for vaultwarden
|
||||||
|
gitea/hooks/ # SQLite .backup for gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup per service
|
||||||
|
|
||||||
|
1. Copy `Dockerfile`, `entrypoint.sh`, `backup.sh` into `<service>/restic/`
|
||||||
|
2. Copy the matching `hooks/` directory
|
||||||
|
3. Create `secrets/restic_password` with a strong passphrase
|
||||||
|
4. Create `ssh/config` (see `ssh/config.sample`), add your private key as `ssh/id_ed25519`, populate `known_hosts` via `ssh-keyscan`
|
||||||
|
5. Create the target directory on the NAS: `mkdir -p /mnt/data/backup/admiralackbar.de/<service>`
|
||||||
|
6. Add the sidecar service to the compose file (see compose snippets in each service dir)
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `RESTIC_REPOSITORY` | — | SFTP target (`sftp:user@host:/path`) |
|
||||||
|
| `RESTIC_PASSWORD_FILE` | — | Path to password file (use Docker secrets) |
|
||||||
|
| `BACKUP_CRON` | `20 7 * * *` | Cron schedule |
|
||||||
|
| `BACKUP_SOURCE` | `/mnt/source /mnt/staging` | Paths to back up |
|
||||||
|
| `KEEP_DAILY` | `1` | Daily snapshots to keep |
|
||||||
|
| `KEEP_WEEKLY` | `3` | Weekly snapshots to keep |
|
||||||
|
| `KEEP_MONTHLY` | `4` | Monthly snapshots to keep |
|
||||||
|
| `KEEP_YEARLY` | `1` | Yearly snapshots to keep |
|
||||||
|
| `MYSQL_*` | — | MariaDB credentials (mailcow only) |
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
Mount scripts to `/hooks/` in the container:
|
||||||
|
- `pre-backup.sh` — runs before `restic backup` (e.g. database dump)
|
||||||
|
- `post-backup.sh` — runs after `restic forget --prune` (e.g. cleanup)
|
||||||
|
|
||||||
|
## Manual backup / check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <container> /usr/local/bin/backup.sh
|
||||||
|
docker exec <container> restic snapshots
|
||||||
|
```
|
||||||
37
backup.sh
Normal file
37
backup.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Source environment vars (crond runs in a minimal env)
|
||||||
|
if [ -f /etc/restic-backup.env ]; then
|
||||||
|
set -a
|
||||||
|
. /etc/restic-backup.env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Backup started at $(date -Iseconds)"
|
||||||
|
|
||||||
|
# Run pre-backup hook
|
||||||
|
if [ -x /hooks/pre-backup.sh ]; then
|
||||||
|
echo "==> Running pre-backup hook"
|
||||||
|
/hooks/pre-backup.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run restic backup
|
||||||
|
echo "==> Running restic backup: $BACKUP_SOURCE"
|
||||||
|
restic backup $BACKUP_SOURCE
|
||||||
|
|
||||||
|
# Apply retention policy
|
||||||
|
echo "==> Applying retention policy: daily=$KEEP_DAILY weekly=$KEEP_WEEKLY monthly=$KEEP_MONTHLY yearly=$KEEP_YEARLY"
|
||||||
|
restic forget --prune \
|
||||||
|
--keep-daily="$KEEP_DAILY" \
|
||||||
|
--keep-weekly="$KEEP_WEEKLY" \
|
||||||
|
--keep-monthly="$KEEP_MONTHLY" \
|
||||||
|
--keep-yearly="$KEEP_YEARLY"
|
||||||
|
|
||||||
|
# Run post-backup hook
|
||||||
|
if [ -x /hooks/post-backup.sh ]; then
|
||||||
|
echo "==> Running post-backup hook"
|
||||||
|
/hooks/post-backup.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Backup finished at $(date -Iseconds)"
|
||||||
51
entrypoint.sh
Normal file
51
entrypoint.sh
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> Restic backup sidecar starting"
|
||||||
|
|
||||||
|
# Validate required env vars
|
||||||
|
for var in RESTIC_REPOSITORY RESTIC_PASSWORD_FILE BACKUP_CRON; do
|
||||||
|
if [ -z "${!var:-}" ]; then
|
||||||
|
echo "ERROR: $var is not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
|
||||||
|
echo "ERROR: Password file $RESTIC_PASSWORD_FILE does not exist" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export RESTIC_PASSWORD_FILE
|
||||||
|
|
||||||
|
# Initialize restic repo if it doesn't exist yet
|
||||||
|
if ! restic snapshots --no-lock >/dev/null 2>&1; then
|
||||||
|
echo "==> Initializing restic repository at $RESTIC_REPOSITORY"
|
||||||
|
restic init
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Repository ready: $RESTIC_REPOSITORY"
|
||||||
|
restic snapshots --no-lock --latest 1 || true
|
||||||
|
|
||||||
|
# Export all relevant env vars to a file so crond jobs can source them
|
||||||
|
# (crond runs jobs in a minimal environment without Docker env vars)
|
||||||
|
ENV_FILE=/etc/restic-backup.env
|
||||||
|
: > "$ENV_FILE"
|
||||||
|
for var in RESTIC_REPOSITORY RESTIC_PASSWORD_FILE BACKUP_SOURCE \
|
||||||
|
KEEP_DAILY KEEP_WEEKLY KEEP_MONTHLY KEEP_YEARLY \
|
||||||
|
MYSQL_HOST MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE PATH; do
|
||||||
|
if [ -n "${!var+x}" ]; then
|
||||||
|
printf '%s=%q\n' "$var" "${!var}" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Write crontab for root user
|
||||||
|
cat > /etc/crontabs/root <<CRON
|
||||||
|
$BACKUP_CRON /usr/local/bin/backup.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
|
||||||
|
CRON
|
||||||
|
|
||||||
|
echo "==> Cron schedule: $BACKUP_CRON"
|
||||||
|
|
||||||
|
# Run crond in foreground
|
||||||
|
echo "==> Starting crond"
|
||||||
|
exec crond -f -l 6
|
||||||
5
gitea/hooks/post-backup.sh
Executable file
5
gitea/hooks/post-backup.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [gitea] Cleaning up SQLite backup"
|
||||||
|
rm -f /mnt/staging/gitea.db
|
||||||
7
gitea/hooks/pre-backup.sh
Executable file
7
gitea/hooks/pre-backup.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [gitea] Creating SQLite backup"
|
||||||
|
mkdir -p /mnt/staging
|
||||||
|
sqlite3 /mnt/source/data/gitea/gitea.db ".backup /mnt/staging/gitea.db"
|
||||||
|
echo "==> [gitea] SQLite backup complete ($(du -h /mnt/staging/gitea.db | cut -f1))"
|
||||||
5
mailcow/hooks/post-backup.sh
Normal file
5
mailcow/hooks/post-backup.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [mailcow] Cleaning up database dump"
|
||||||
|
rm -f /mnt/staging/mailcow.sql
|
||||||
13
mailcow/hooks/pre-backup.sh
Normal file
13
mailcow/hooks/pre-backup.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [mailcow] Dumping MariaDB database"
|
||||||
|
mkdir -p /mnt/staging
|
||||||
|
mariadb-dump \
|
||||||
|
--socket=/var/run/mysqld/mysqld.sock \
|
||||||
|
--skip-ssl \
|
||||||
|
-u "$MYSQL_USER" \
|
||||||
|
-p"$MYSQL_PASSWORD" \
|
||||||
|
--single-transaction \
|
||||||
|
"$MYSQL_DATABASE" > /mnt/staging/mailcow.sql
|
||||||
|
echo "==> [mailcow] Database dump complete ($(du -h /mnt/staging/mailcow.sql | cut -f1))"
|
||||||
4
ssh/config.sample
Normal file
4
ssh/config.sample
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Host 192.168.0.10
|
||||||
|
User backup-admiralackbar
|
||||||
|
IdentityFile /root/.ssh/id_ed25519
|
||||||
|
StrictHostKeyChecking yes
|
||||||
5
vaultwarden/hooks/post-backup.sh
Normal file
5
vaultwarden/hooks/post-backup.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [vaultwarden] Cleaning up SQLite backup"
|
||||||
|
rm -f /mnt/staging/db.sqlite3
|
||||||
7
vaultwarden/hooks/pre-backup.sh
Normal file
7
vaultwarden/hooks/pre-backup.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> [vaultwarden] Creating SQLite backup"
|
||||||
|
mkdir -p /mnt/staging
|
||||||
|
sqlite3 /mnt/source/bw-data/db.sqlite3 ".backup /mnt/staging/db.sqlite3"
|
||||||
|
echo "==> [vaultwarden] SQLite backup complete ($(du -h /mnt/staging/db.sqlite3 | cut -f1))"
|
||||||
Reference in New Issue
Block a user