Skip to content

Commit 417b822

Browse files
committed
feat: comprehensive backup/restore for lux-mon
scripts/backup.sh now archives: - MariaDB dump (luxmon database) - InfluxDB v2 bucket backup via influx CLI or influxdb:2.7 container - Grafana provisioning files and dashboard JSON - .env configuration - Runtime settings from /api/settings scripts/restore.sh restores all of the above. Supports both native installs and Docker-based deployments. Add systemd service+timer for nightly 02:00 backups. Add backup env vars to .env.example and docker/.env.example, including optional LUX_BACKUP_REMOTE for off-device copies.
1 parent 8372ce0 commit 417b822

6 files changed

Lines changed: 265 additions & 5 deletions

File tree

‎.env.example‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,12 @@ LUX_ALERTS_EMAIL_TLS=true
8989
LUX_ALERTS_WEBHOOK_ENABLED=false
9090
LUX_ALERTS_WEBHOOK_URL=https://example.com/lux-mon-webhook
9191

92+
# Backup / restore
93+
LUX_BACKUP_DIR=/var/backups/lux-mon
94+
LUX_BACKUP_KEEP_DAYS=30
95+
#LUX_BACKUP_REMOTE=/mnt/usb/lux-mon-backups
96+
LUX_GRAFANA_PROVISIONING=/etc/grafana/provisioning
97+
LUX_GRAFANA_DASHBOARDS=/var/lib/grafana/dashboards
98+
9299
# Legacy single-storage option (kept for backwards compatibility)
93100
# LUX_STORAGE_TYPE=mariadb

‎docker/.env.example‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,16 @@ LUX_ALERTS_WEBHOOK_URL=https://example.com/lux-mon-webhook
6262
# --- Grafana ----------------------------------------------------------------
6363
LUX_GRAFANA_ADMIN_USER=admin
6464
LUX_GRAFANA_ADMIN_PASSWORD=change-me
65+
66+
# --- Backup / restore -------------------------------------------------------
67+
# Where to store nightly backups. Set LUX_BACKUP_REMOTE to copy the archive to
68+
# another host/path after each run. Examples:
69+
# LUX_BACKUP_REMOTE=/mnt/usb/lux-mon-backups
70+
# LUX_BACKUP_REMOTE=user@nas:/backups/lux-mon
71+
LUX_BACKUP_DIR=/var/backups/lux-mon
72+
LUX_BACKUP_KEEP_DAYS=30
73+
#LUX_BACKUP_REMOTE=
74+
75+
# Grafana paths for backup/restore (native installs only; Docker uses volumes)
76+
LUX_GRAFANA_PROVISIONING=/etc/grafana/provisioning
77+
LUX_GRAFANA_DASHBOARDS=/var/lib/grafana/dashboards

‎scripts/backup.sh‎

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#!/usr/bin/env bash
2-
# Backup lux-mon MariaDB + settings + .env
2+
# Backup lux-mon: MariaDB + InfluxDB v2 + Grafana provisioning/dashboards + .env + settings.
3+
# The resulting archive can be restored with scripts/restore.sh on a fresh install.
34
set -euo pipefail
45

56
SRC_DIR="${LUX_SRC_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
67

7-
# Load .env if present so DB credentials are available
8+
# Load .env if present so DB credentials and paths are available.
89
if [ -f "$SRC_DIR/.env" ]; then
910
set -a
1011
. "$SRC_DIR/.env"
@@ -13,29 +14,127 @@ fi
1314

1415
BACKUP_DIR="${LUX_BACKUP_DIR:-/var/backups/lux-mon}"
1516
KEEP_DAYS="${LUX_BACKUP_KEEP_DAYS:-30}"
17+
REMOTE_DEST="${LUX_BACKUP_REMOTE:-}"
18+
1619
DB_NAME="${LUX_MARIADB_DATABASE:-luxmon}"
1720
DB_USER="${LUX_MARIADB_USER:-luxmon}"
1821
DB_PASS="${LUX_MARIADB_PASSWORD:-}"
1922
DB_HOST="${LUX_MARIADB_HOST:-localhost}"
2023
DB_PORT="${LUX_MARIADB_PORT:-3306}"
24+
25+
INFLUX_URL="${LUX_INFLUX_URL:-http://localhost:8086}"
26+
INFLUX_TOKEN="${LUX_INFLUX_TOKEN:-}"
27+
INFLUX_ORG="${LUX_INFLUX_ORG:-luxmon}"
28+
INFLUX_BUCKET="${LUX_INFLUX_BUCKET:-luxmon}"
29+
30+
GRAFANA_PROVISIONING="${LUX_GRAFANA_PROVISIONING:-/etc/grafana/provisioning}"
31+
GRAFANA_DASHBOARDS="${LUX_GRAFANA_DASHBOARDS:-/var/lib/grafana/dashboards}"
32+
2133
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
2234
NAME="luxmon-backup-${TIMESTAMP}"
2335
TMP=$(mktemp -d)
2436
trap 'rm -rf "$TMP"' EXIT
2537

2638
mkdir -p "$BACKUP_DIR"
2739

40+
echo "[backup] Starting lux-mon backup: ${NAME}"
41+
42+
# 1. MariaDB dump
2843
mysqldump -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${DB_PASS:+-p"$DB_PASS"} "$DB_NAME" \
2944
> "$TMP/luxmon.sql"
45+
echo "[backup] MariaDB dump OK"
46+
47+
# 2. InfluxDB v2 backup (works whether running natively or in Docker)
48+
if [ -n "$INFLUX_TOKEN" ]; then
49+
INFLUX_BACKUP_DIR="$TMP/influxdb-backup"
50+
mkdir -p "$INFLUX_BACKUP_DIR"
51+
52+
# Prefer a local influx CLI if installed.
53+
if command -v influx >/dev/null 2>&1; then
54+
influx backup \
55+
--host "$INFLUX_URL" \
56+
--org "$INFLUX_ORG" \
57+
--token "$INFLUX_TOKEN" \
58+
"$INFLUX_BACKUP_DIR"
59+
else
60+
# Fall back to running the official influxdb container against the live API.
61+
docker run --rm --network host \
62+
-v "$INFLUX_BACKUP_DIR:/out" \
63+
-e INFLUX_TOKEN="$INFLUX_TOKEN" \
64+
influxdb:2.7 influx backup \
65+
--host "$INFLUX_URL" \
66+
--org "$INFLUX_ORG" \
67+
--token "$INFLUX_TOKEN" \
68+
/out
69+
fi
70+
echo "[backup] InfluxDB backup OK"
71+
else
72+
echo "[backup] Skipping InfluxDB backup: LUX_INFLUX_TOKEN not set"
73+
fi
74+
75+
# 3. Grafana provisioning and dashboard JSON files
76+
if [ -d "$GRAFANA_PROVISIONING" ]; then
77+
if [ "$EUID" -eq 0 ]; then
78+
cp -a "$GRAFANA_PROVISIONING" "$TMP/grafana-provisioning"
79+
else
80+
sudo cp -a "$GRAFANA_PROVISIONING" "$TMP/grafana-provisioning" || true
81+
fi
82+
echo "[backup] Grafana provisioning OK"
83+
fi
84+
if [ -d "$GRAFANA_DASHBOARDS" ]; then
85+
if [ "$EUID" -eq 0 ]; then
86+
cp -a "$GRAFANA_DASHBOARDS" "$TMP/grafana-dashboards"
87+
else
88+
sudo cp -a "$GRAFANA_DASHBOARDS" "$TMP/grafana-dashboards" || true
89+
fi
90+
echo "[backup] Grafana dashboards OK"
91+
fi
3092

93+
# 4. lux-mon .env configuration
3194
if [ -f "$SRC_DIR/.env" ]; then
3295
cp "$SRC_DIR/.env" "$TMP/env"
96+
echo "[backup] .env OK"
3397
fi
3498

35-
curl -sf http://127.0.0.1:8080/api/settings > "$TMP/settings.json" || true
99+
# 5. Runtime settings from the API
100+
curl -sf "${LUX_API_URL:-http://127.0.0.1:8080}/api/settings" > "$TMP/settings.json" || true
36101

37-
tar -czf "$BACKUP_DIR/${NAME}.tar.gz" -C "$TMP" .
102+
# 6. Metadata
103+
{
104+
echo "NAME=${NAME}"
105+
echo "TIMESTAMP=${TIMESTAMP}"
106+
echo "HOSTNAME=$(hostname)"
107+
echo "SRC_DIR=${SRC_DIR}"
108+
echo "DB_HOST=${DB_HOST}"
109+
echo "DB_NAME=${DB_NAME}"
110+
echo "INFLUX_URL=${INFLUX_URL}"
111+
echo "INFLUX_ORG=${INFLUX_ORG}"
112+
echo "INFLUX_BUCKET=${INFLUX_BUCKET}"
113+
} > "$TMP/backup.meta"
38114

115+
# 7. Create archive
116+
ARCHIVE="$BACKUP_DIR/${NAME}.tar.gz"
117+
tar -czf "$ARCHIVE" -C "$TMP" .
118+
echo "[backup] Archive created: ${ARCHIVE}"
119+
120+
# 8. Rotate local backups
39121
find "$BACKUP_DIR" -maxdepth 1 -name 'luxmon-backup-*.tar.gz' -mtime +"$KEEP_DAYS" -delete || true
40122

41-
echo "$BACKUP_DIR/${NAME}.tar.gz"
123+
# 9. Optional remote copy
124+
if [ -n "$REMOTE_DEST" ]; then
125+
echo "[backup] Copying to remote: ${REMOTE_DEST}"
126+
if [[ "$REMOTE_DEST" == rsync://* ]]; then
127+
rsync -a --mkpath "$ARCHIVE" "${REMOTE_DEST#rsync://}"
128+
else
129+
# Treat as scp-style destination, e.g. user@host:/path or /mnt/usb
130+
if [[ "$REMOTE_DEST" == *@*:* ]]; then
131+
scp "$ARCHIVE" "$REMOTE_DEST/"
132+
else
133+
mkdir -p "$REMOTE_DEST"
134+
cp -a "$ARCHIVE" "$REMOTE_DEST/"
135+
fi
136+
fi
137+
echo "[backup] Remote copy OK"
138+
fi
139+
140+
echo "[backup] Done: ${ARCHIVE}"

‎scripts/lux-mon-backup.service‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Unit]
2+
Description=lux-mon backup
3+
After=network.target mariadb.service influxdb.service
4+
5+
[Service]
6+
Type=oneshot
7+
User=root
8+
WorkingDirectory=/home/jmewing/src/lux-mon
9+
Environment=LUX_SRC_DIR=/home/jmewing/src/lux-mon
10+
Environment=LUX_BACKUP_DIR=/var/backups/lux-mon
11+
Environment=LUX_BACKUP_KEEP_DAYS=30
12+
EnvironmentFile=-/home/jmewing/src/lux-mon/.env
13+
ExecStart=/home/jmewing/src/lux-mon/scripts/backup.sh

‎scripts/lux-mon-backup.timer‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[Unit]
2+
Description=Daily lux-mon backup
3+
4+
[Timer]
5+
OnCalendar=*-*-* 02:00:00
6+
Persistent=true
7+
8+
[Install]
9+
WantedBy=timers.target

‎scripts/restore.sh‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
# Restore lux-mon from a backup archive created by scripts/backup.sh.
3+
# Usage: LUX_BACKUP=/var/backups/lux-mon/luxmon-backup-YYYYMMDD-HHMMSS.tar.gz ./scripts/restore.sh
4+
#
5+
# WARNING: This will overwrite the existing lux-mon MariaDB and InfluxDB data.
6+
# Stop the lux-mon collector/API before running this.
7+
set -euo pipefail
8+
9+
SRC_DIR="${LUX_SRC_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
10+
11+
if [ -f "$SRC_DIR/.env" ]; then
12+
set -a
13+
. "$SRC_DIR/.env"
14+
set +a
15+
fi
16+
17+
BACKUP_FILE="${LUX_BACKUP:-}"
18+
if [ -z "$BACKUP_FILE" ] || [ ! -f "$BACKUP_FILE" ]; then
19+
echo "Usage: LUX_BACKUP=/path/to/luxmon-backup-YYYYMMDD-HHMMSS.tar.gz ./scripts/restore.sh"
20+
exit 1
21+
fi
22+
23+
DB_NAME="${LUX_MARIADB_DATABASE:-luxmon}"
24+
DB_USER="${LUX_MARIADB_USER:-luxmon}"
25+
DB_PASS="${LUX_MARIADB_PASSWORD:-}"
26+
DB_HOST="${LUX_MARIADB_HOST:-localhost}"
27+
DB_PORT="${LUX_MARIADB_PORT:-3306}"
28+
29+
INFLUX_URL="${LUX_INFLUX_URL:-http://localhost:8086}"
30+
INFLUX_TOKEN="${LUX_INFLUX_TOKEN:-}"
31+
INFLUX_ORG="${LUX_INFLUX_ORG:-luxmon}"
32+
INFLUX_ADMIN_USER="${LUX_INFLUX_USERNAME:-}"
33+
INFLUX_ADMIN_PASS="${LUX_INFLUX_ADMIN_PASSWORD:-}"
34+
35+
GRAFANA_PROVISIONING="${LUX_GRAFANA_PROVISIONING:-/etc/grafana/provisioning}"
36+
GRAFANA_DASHBOARDS="${LUX_GRAFANA_DASHBOARDS:-/var/lib/grafana/dashboards}"
37+
38+
TMP=$(mktemp -d)
39+
trap 'rm -rf "$TMP"' EXIT
40+
41+
echo "[restore] Extracting ${BACKUP_FILE}"
42+
tar -xzf "$BACKUP_FILE" -C "$TMP"
43+
44+
cat "$TMP/backup.meta" || true
45+
46+
# 1. Restore MariaDB
47+
if [ -f "$TMP/luxmon.sql" ]; then
48+
echo "[restore] Restoring MariaDB database ${DB_NAME}"
49+
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${DB_PASS:+-p"$DB_PASS"} -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME};" || true
50+
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${DB_PASS:+-p"$DB_PASS"} "$DB_NAME" < "$TMP/luxmon.sql"
51+
echo "[restore] MariaDB restore OK"
52+
fi
53+
54+
# 2. Restore InfluxDB v2 bucket data
55+
if [ -d "$TMP/influxdb-backup" ] && [ -n "$INFLUX_TOKEN" ]; then
56+
echo "[restore] Restoring InfluxDB bucket ${INFLUX_BUCKET}"
57+
58+
influx_restore() {
59+
local backup_path="$1"
60+
if command -v influx >/dev/null 2>&1; then
61+
influx restore \
62+
--host "$INFLUX_URL" \
63+
--token "$INFLUX_TOKEN" \
64+
--bucket "$INFLUX_BUCKET" \
65+
--new-bucket "$INFLUX_BUCKET" \
66+
"$backup_path"
67+
else
68+
docker run --rm --network host \
69+
-v "$backup_path:/backup:ro" \
70+
-e INFLUX_TOKEN="$INFLUX_TOKEN" \
71+
influxdb:2.7 influx restore \
72+
--host "$INFLUX_URL" \
73+
--token "$INFLUX_TOKEN" \
74+
--bucket "$INFLUX_BUCKET" \
75+
--new-bucket "$INFLUX_BUCKET" \
76+
/backup
77+
fi
78+
}
79+
80+
# InfluxDB restore cannot overwrite an existing bucket. Delete it first if present.
81+
if influx bucket list --host "$INFLUX_URL" --org "$INFLUX_ORG" --token "$INFLUX_TOKEN" 2>/dev/null | grep -qw "$INFLUX_BUCKET"; then
82+
echo "[restore] Deleting existing InfluxDB bucket ${INFLUX_BUCKET} before restore"
83+
influx bucket delete --host "$INFLUX_URL" --org "$INFLUX_ORG" --token "$INFLUX_TOKEN" --name "$INFLUX_BUCKET" || true
84+
fi
85+
86+
influx_restore "$TMP/influxdb-backup"
87+
echo "[restore] InfluxDB restore OK"
88+
else
89+
echo "[restore] Skipping InfluxDB restore (no backup dir or LUX_INFLUX_TOKEN not set)"
90+
fi
91+
92+
# 3. Restore Grafana provisioning and dashboards
93+
if [ -d "$TMP/grafana-provisioning" ]; then
94+
echo "[restore] Restoring Grafana provisioning"
95+
sudo rm -rf "$GRAFANA_PROVISIONING"
96+
sudo mkdir -p "$GRAFANA_PROVISIONING"
97+
sudo cp -a "$TMP/grafana-provisioning/"* "$GRAFANA_PROVISIONING/"
98+
echo "[restore] Grafana provisioning restore OK"
99+
fi
100+
if [ -d "$TMP/grafana-dashboards" ]; then
101+
echo "[restore] Restoring Grafana dashboards"
102+
sudo rm -rf "$GRAFANA_DASHBOARDS"
103+
sudo mkdir -p "$GRAFANA_DASHBOARDS"
104+
sudo cp -a "$TMP/grafana-dashboards/"* "$GRAFANA_DASHBOARDS/"
105+
echo "[restore] Grafana dashboards restore OK"
106+
fi
107+
108+
# 4. Restore .env if no local .env exists
109+
if [ -f "$TMP/env" ] && [ ! -f "$SRC_DIR/.env" ]; then
110+
cp "$TMP/env" "$SRC_DIR/.env"
111+
echo "[restore] .env restored to ${SRC_DIR}/.env"
112+
fi
113+
114+
# 5. Runtime settings can be re-applied via the API
115+
if [ -f "$TMP/settings.json" ] && [ -f "$SRC_DIR/.env" ]; then
116+
echo "[restore] Runtime settings backed up; re-apply manually via /api/settings if needed."
117+
fi
118+
119+
echo "[restore] Done. Restart lux-mon services if they were stopped."

0 commit comments

Comments
 (0)