Skip to content

Instantly share code, notes, and snippets.

@huyz
Created April 17, 2026 11:33
Show Gist options
  • Select an option

  • Save huyz/d981888b28810aa3d68f1ceed36514d9 to your computer and use it in GitHub Desktop.

Select an option

Save huyz/d981888b28810aa3d68f1ceed36514d9 to your computer and use it in GitHub Desktop.
UptimeKuma back-up scripts. (Tip: use backup-warden to automatically delete old backups)
#!/bin/bash
# vim: set nomodifiable : Ansible-managed: Do NOT edit this file manually
# Given that I have a bind mount from `/srv/uptimekuma/data` on host to `/app/data` within the container
# 2024-12-21 per https://github.com/louislam/uptime-kuma/issues/1260#issuecomment-1427974555
#### Preamble (v2024-04-22)
set -euo pipefail
shopt -s failglob
# shellcheck disable=SC2317
function trap_err { echo "ERR signal on line $(caller)" >&2; }
trap trap_err ERR
trap exit INT # So that ^C will stop the entire script, not just the current subprocess
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# shellcheck disable=SC2034
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
script="${BASH_SOURCE[0]}"
while [[ -L "$script" ]]; do
script="$(readlink "$script")"
done
# shellcheck disable=SC2034
SCRIPT_DIR="$(dirname "$script")"
##############################################################################
#### Config
HOST=${HOST:-$(hostname -s)}
SERVICE=uptimekuma
BACKUPS_ROOT_DIR='/srv/var/backups'
DATA_DIR='data'
BACKUP_FILE_SUFFIX='backup.db'
CONTAINER_DATA_DIR='/app/data'
SERVICE_DIR="/srv/$SERVICE"
BACKUPS_DIR="$BACKUPS_ROOT_DIR/$SERVICE"
#### Init
zip_extra_post_args=()
zip_extra_args=()
if [[ ! -t 1 ]]; then
# If stdout is not a terminal, we want quieter logs
zip_extra_args+=(-q)
fi
now=$(date -Iseconds -u | sed 's/+00:00/Z/;s/://g')
mkdir -p "$BACKUPS_DIR"
# Create a symlink for CLI convenience
if [[ ! -L "$SERVICE_DIR/backups" ]]; then
ln -sfn "$BACKUPS_DIR" "$SERVICE_DIR/backups"
fi
cd "$SERVICE_DIR"
if [[ -z "$(docker compose ps -q --services --status=running)" ]]; then
DOCKER_RUN=(docker compose run -T --rm "$SERVICE")
else
DOCKER_RUN=(docker compose exec -T "$SERVICE")
fi
#### Main
echo "𐄫 Backing up ${SERVICE}…"
echo
target="$BACKUPS_DIR/${now}_${SERVICE}_${BACKUP_FILE_SUFFIX}"
echo "𐄬 Dumping $SERVICE SQLite DB…"
"${DOCKER_RUN[@]}" sqlite3 data/kuma.db ".backup '$CONTAINER_DATA_DIR/$BACKUP_FILE_SUFFIX'"
# NOTE: cannot `mv` because the directory ownership is different
cp "$SERVICE_DIR/$DATA_DIR/$BACKUP_FILE_SUFFIX" "$target"
"${DOCKER_RUN[@]}" rm -f "$CONTAINER_DATA_DIR/$BACKUP_FILE_SUFFIX"
echo
echo "𐄬 Archiving in ${target}.zip…"
zip --move --junk-paths "${zip_extra_args[@]}" "$target.zip" "$target" "${zip_extra_post_args[@]}"
echo
# For use by automated scripts
echo "$target.zip"
#!/bin/bash
# vim: set nomodifiable : Ansible-managed: Do NOT edit this file manually
# 2024-12-21 per https://github.com/louislam/uptime-kuma/issues/1260#issuecomment-1427974555
#### Preamble (v2024-04-22)
set -euo pipefail
shopt -s failglob extglob
# shellcheck disable=SC2317
function trap_err { echo "ERR signal on line $(caller)" >&2; }
trap trap_err ERR
trap exit INT # So that ^C will stop the entire script, not just the current subprocess
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# shellcheck disable=SC2034
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
script="${BASH_SOURCE[0]}"
while [[ -L "$script" ]]; do
script="$(readlink "$script")"
done
# shellcheck disable=SC2034
SCRIPT_DIR="$(dirname "$script")"
##############################################################################
#### Config
HOST=${HOST:-$(hostname -s)}
SERVICE=uptimekuma
BACKUPS_ROOT_DIR='/srv/var/backups'
DATA_DIR='data'
BACKUP_FILE_SUFFIX='backup.db'
CONTAINER_DATA_DIR='/app/data'
SERVICE_DIR="/srv/$SERVICE"
BACKUPS_DIR="$BACKUPS_ROOT_DIR/$SERVICE"
BACKUP_SCRIPT="$(realpath "$SCRIPT_DIR/backup.sh")"
#### Init
unzip_extra_post_args=()
now=$(date -Iseconds -u | sed 's/+00:00/Z/;s/://g')
target="$BACKUPS_DIR/${now}_${SERVICE}_${BACKUP_FILE_SUFFIX}"
if [[ $# -ne 1 || ("$1" != latest && ! -e "$1") ]]; then
echo "Usage: $0 [<backup_file>|latest]" >&2
echo " ex: $0 $target.zip" >&2
exit 1
fi
if [[ "$1" == latest ]]; then
backup_file="$(ls -1 "$BACKUPS_DIR"/20*.@(zip|tbz2) | grep -v 'KEEP-FROM-BEFORE-RESTORE' | tail -n 1)"
echo "ℹ️ Using latest backup file: $backup_file"
echo
else
backup_file="$(realpath "$1")"
fi
DOCKER_RUN=(docker compose run -T --rm "$SERVICE")
#### Prep
echo "𐄫 Shutting down ${SERVICE}…"
cd "$SERVICE_DIR"
docker compose stop 2>&1
echo
echo "𐄫 Backing up current state…"
last_backup=$("$BACKUP_SCRIPT" | tail -1)
echo
if [[ -n "$last_backup" ]]; then
ext=${last_backup##*.}
last_backup_new_name="${last_backup%.*}.KEEP-FROM-BEFORE-RESTORE.$ext"
echo "𐄬 Renaming this backup to ${last_backup_new_name}…"
mv -v "$last_backup" "$last_backup_new_name"
echo
fi
# Gotta make sure to be able to nuke dotfiles, including .srv-bind-mount,
# even if behind protected directories (e.g., pgdata/)
if sudo bash -c "shopt -s extglob; compgen -G '$SERVICE_DIR/$DATA_DIR/@(|.)*'" > /dev/null; then
echo "𐄫 Deleting current state…"
sudo bash -c "shopt -s extglob; eval rm -rf '$SERVICE_DIR/$DATA_DIR/@(|.)*'"
echo
fi
#### Restore
echo "𐄫 Unarchiving ${backup_file}…"
cd "$SERVICE_DIR/$DATA_DIR"
sudo unzip -Xo "$backup_file" "${unzip_extra_post_args[@]}"
echo
for i in *_$BACKUP_FILE_SUFFIX; do
echo "𐄫 Loading $SERVICE SQLite DB from ${i}…"
"${DOCKER_RUN[@]}" mkdir -p data
"${DOCKER_RUN[@]}" sqlite3 data/kuma.db ".restore '$CONTAINER_DATA_DIR/$i'"
break
done
echo
echo "Done. Service $SERVICE is ready to restart. Re-create DVB container too."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment