first commit of bot config

This commit is contained in:
2025-10-20 15:47:20 -07:00
parent c8cc8f25bd
commit 39d5ad4b52
7 changed files with 251 additions and 50 deletions

View File

@@ -8,7 +8,7 @@ on:
jobs:
build:
runs-on: prodesk
runs-on: prodesk
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -30,6 +30,5 @@ jobs:
file: ./Dockerfile
push: true
tags: |
gitea.calahilstudios.com/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest
gitea.calahilstudios.com/${{ github.repository_owner }}/${{ github.event.repository.name }}:develop
gitea.calahilstudios.com/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.sha }}

View File

@@ -1,37 +1,35 @@
# Use LinuxServer.io Duplicati base
FROM ghcr.io/linuxserver/duplicati:2.1.0
ENV DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg \
lsb-release \
btrfs-progs \
#&& rm -rf /var/lib/apt/lists/* \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends \
cron \
# Install Docker CLI, bash, python3
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
bash \
docker-ce-cli \
postgresql-client \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /backups
python3 \
python3-pip \
docker.io \
ca-certificates curl && \
rm -rf /var/lib/apt/lists/*
# Create directories for backup scripts and logs
RUN mkdir -p /usr/local/bin /config/log /config/web /etc/services/backupbot
# Copy backup script
COPY backup.sh /usr/local/bin/backup.sh
RUN chmod +x /usr/local/bin/backup.sh \
&& mkdir -p /etc/services.d/backupbot
COPY services/backupbot/run /etc/services.d/backupbot/run
RUN chmod +x /etc/services.d/backupbot/run
RUN chmod +x /usr/local/bin/backup.sh
# Copy the environment variables for the config
COPY backupbot.env /config/backupbot.env
# Copy s6 service for backupbot
COPY services/backupbot/run /etc/services/backupbot/run
RUN chmod +x /etc/services/backupbot/run
# Copy web frontend
COPY web/ /config/web/
RUN chmod +x /config/web/backupbot.cgi
# Expose web frontend port
EXPOSE 8080
# Keep duplicati entrypoint
ENTRYPOINT ["/init"]

23
backupbot.env Normal file
View File

@@ -0,0 +1,23 @@
# === BackupBot Environment Configuration ===
# Used by backup_scheduler.sh and web frontend
# Do not store sensitive credentials in world-readable locations unless necessary
# Timezone for scheduling backups (affects 3 AM backup)
TZ=America/Los_Angeles
# Directory to store backup SQL files
BACKUP_DIR=/backups/postgres
# Log file path
LOG_FILE=/config/log/pgbackup.log
# Number of retry attempts on failure
MAX_RETRIES=3
# Gotify notification configuration (optional)
GOTIFY_URL=http://gotify.example.com
GOTIFY_TOKEN=your_gotify_token_here
# Backup time (24-hour format) defaults to 03:00 local time
BACKUP_HOUR=03
BACKUP_MINUTE=00

View File

@@ -6,20 +6,18 @@ services:
environment:
- PUID=0
- PGID=0
- TZ=Etc/UTC
- TZ=America/Los_Angeles
- SETTINGS_ENCRYPTION_KEY=${KEY}
- CLI_ARGS= #optional
- DUPLICATI__WEBSERVICE_PASSWORD=${PASSWORD}
volumes:
# Config dir for duplicati
- /srv/appdata/duplicati/config:/config
# Backup folder to store dumps/backups
# Backup folder to store dumps/backups/snapshots
- /srv/backups:/backups
# Local docker config dirs
- /srv/appdata:/source/appdata:rshared
# Docker socket to list containers
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 8200:8200
- 8201:8080
restart: unless-stopped

View File

@@ -1,27 +1,72 @@
#!/usr/bin/with-contenv bash
set -e
# Source env if available
if [[ -f /config/backupbot.env ]]; then
export $(grep -v '^#' /config/backupbot.env | xargs)
fi
echo "[BACKUPBOT_INFO] Starting PostgreSQL backup loop service..."
# Start Python HTTP server for web config in background
cd /config/web
nohup python3 -m http.server 8080 --cgi >/config/log/web.log 2>&1 &
INTERVAL_HOURS="${INTERVAL_HOURS:-24}"
# Start backup scheduler
STATE_FILE="/config/last_backup_date"
LOG_FILE="/config/log/pgbackup.log"
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$LOG_FILE")"
# TZ
: "${TZ:=UTC}"
export TZ
# Retry config
RETRIES=3
GOTIFY_URL="${GOTIFY_URL:-}"
GOTIFY_TOKEN="${GOTIFY_TOKEN:-}"
# Helper: seconds until next 3AM
seconds_until_next_3am() {
local now next_3am
now=$(date +%s)
next_3am=$(date -d "today 03:00" +%s)
((now >= next_3am)) && next_3am=$(date -d "tomorrow 03:00" +%s)
echo $((next_3am - now))
}
# Run backup with retries
run_backup() {
local attempt=1
while ((attempt <= RETRIES)); do
echo "[INFO] Backup attempt $attempt"
if /usr/local/bin/backup.sh >>"$LOG_FILE" 2>&1; then
echo "[SUCCESS] Backup completed"
return 0
else
echo "[WARN] Backup failed on attempt $attempt"
((attempt++))
sleep 5
fi
done
# Send Gotify notification if configured
if [[ -n "$GOTIFY_URL" && -n "$GOTIFY_TOKEN" ]]; then
curl -s -X POST "$GOTIFY_URL/message?token=$GOTIFY_TOKEN" \
-F "title=Backup Failed" \
-F "message=PostgreSQL backup failed after $RETRIES attempts" \
-F "priority=5"
fi
return 1
}
# Main loop
while true; do
TODAY=$(date +%F)
# Check if a backup already ran today
if [[ -f "$STATE_FILE" && "$(cat "$STATE_FILE")" == "$TODAY" ]]; then
echo "[BACKUPBOT_INFO] Backup already completed today ($TODAY). Skipping."
echo "[INFO] Backup already done for $TODAY"
else
echo "[BACKUPBOT_INFO] Triggering backup.sh at $(date)"
/usr/local/bin/backup.sh "$LOG_FILE"
echo "$TODAY" >"$STATE_FILE"
echo "[BACKUPBOT_INFO] Backup completed and date recorded."
echo "[INFO] Running backup for $TODAY"
if run_backup; then
echo "$TODAY" >"$STATE_FILE"
fi
fi
echo "[BACKUPBOT_INFO] Sleeping for $INTERVAL_HOURS hours..."
sleep "${INTERVAL_HOURS}h"
SECONDS_TO_WAIT=$(seconds_until_next_3am)
sleep "$SECONDS_TO_WAIT"
done

47
web/backupbot.cgi Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import cgi
import cgitb
import os
import json
cgitb.enable()
print("Content-Type: application/json\n")
ENV_FILE = "/config/web/backupbot.env"
def read_env():
env = {}
if os.path.exists(ENV_FILE):
with open(ENV_FILE) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, val = line.split("=", 1)
env[key.strip()] = val.strip()
return env
def write_env(env):
with open(ENV_FILE, "w") as f:
for key, val in env.items():
f.write(f"{key}={val}\n")
form = cgi.FieldStorage()
action = form.getvalue("action")
if action == "get":
env = read_env()
print(json.dumps(env))
elif action == "set":
try:
raw = os.environ.get("CONTENT_LENGTH")
length = int(raw) if raw else 0
data = json.loads(os.read(0, length))
write_env(data)
print(json.dumps({"status": "ok", "message": "Configuration saved."}))
except Exception as e:
print(json.dumps({"status": "error", "message": str(e)}))
else:
print(json.dumps({"status": "error", "message": "Invalid action"}))

91
web/index.html Normal file
View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BackupBot Configuration</title>
<style>
body {
font-family: sans-serif;
margin: 2rem;
background: #f4f4f4;
}
label {
display: block;
margin-top: 1rem;
}
input {
width: 200px;
}
button {
margin-top: 1rem;
padding: 0.5rem 1rem;
}
</style>
</head>
<body>
<h1>BackupBot Configuration</h1>
<form id="configForm">
<label>Timezone:
<input type="text" name="TZ">
</label>
<label>Backup Directory:
<input type="text" name="BACKUP_DIR">
</label>
<label>Log File:
<input type="text" name="LOG_FILE">
</label>
<label>Backup Hour:
<input type="number" name="BACKUP_HOUR" min="0" max="23">
</label>
<label>Backup Minute:
<input type="number" name="BACKUP_MINUTE" min="0" max="59">
</label>
<label>Max Retries:
<input type="number" name="MAX_RETRIES" min="1" max="10">
</label>
<label>Gotify URL:
<input type="text" name="GOTIFY_URL">
</label>
<label>Gotify Token:
<input type="text" name="GOTIFY_TOKEN">
</label>
<button type="submit">Save Configuration</button>
</form>
<p id="status"></p>
<script>
async function loadConfig() {
const res = await fetch('/config/web/backupbot.cgi?action=get');
const data = await res.json();
const form = document.getElementById('configForm');
Object.keys(data).forEach(key => {
if (form.elements[key]) form.elements[key].value = data[key];
});
}
async function saveConfig(e) {
e.preventDefault();
const formData = new FormData(document.getElementById('configForm'));
const obj = Object.fromEntries(formData.entries());
const res = await fetch('/config/web/backupbot.cgi?action=set', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(obj)
});
const result = await res.json();
document.getElementById('status').innerText = result.message;
}
document.getElementById('configForm').addEventListener('submit', saveConfig);
loadConfig();
</script>
</body>
</html>