Add OpenClaw, backup automation, health checks, Telegram notifications
Phase 6: OpenClaw docker-compose (port 3100) with health check Phase 9: Enhanced backup.sh with full DB/config coverage + Telegram alerts Phase 10: Health checks on all compose files, /api/system/notify endpoint
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# Telegram notifications (used by backup.sh and notify.sh)
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
@@ -1,63 +1,73 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Configuration
|
# NAS Backup Script — runs daily via cron
|
||||||
|
# Crontab: 0 3 * * * /volume1/docker/nas-tools/backup.sh >> /volume1/backups/nas-tools/backup.log 2>&1
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
BACKUP_DIR="/volume1/backups/nas-tools"
|
BACKUP_DIR="/volume1/backups/nas-tools"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
RETENTION_DAYS=7
|
RETENTION_DAYS=7
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
[ -f "$SCRIPT_DIR/.env" ] && source "$SCRIPT_DIR/.env"
|
||||||
|
|
||||||
# Ensure backup directory exists
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
log() {
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
|
||||||
|
notify() {
|
||||||
|
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-d chat_id="$TELEGRAM_CHAT_ID" -d text="$1" -d parse_mode="Markdown" > /dev/null
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
log "Starting backup..."
|
run_backup() {
|
||||||
|
local desc="$1"; shift
|
||||||
# 1. Database Dumps
|
log "Backing up $desc..."
|
||||||
log "Dumping databases..."
|
if "$@"; then
|
||||||
|
log "$desc OK"
|
||||||
# Gitea DB
|
|
||||||
docker exec gitea-db pg_dump -U gitea gitea > "$BACKUP_DIR/gitea_db_$DATE.sql"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log "Gitea DB dumped successfully."
|
|
||||||
else
|
else
|
||||||
log "Error dumping Gitea DB!"
|
log "FAILED: $desc"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "=== Backup started ==="
|
||||||
|
|
||||||
|
# Database dumps
|
||||||
|
run_backup "Gitea DB" docker exec gitea-db pg_dump -U gitea -f /tmp/gitea_dump.sql gitea
|
||||||
|
docker cp gitea-db:/tmp/gitea_dump.sql "$BACKUP_DIR/gitea_db_$DATE.sql" 2>/dev/null
|
||||||
|
|
||||||
|
run_backup "Immich DB" docker exec immich-db pg_dump -U postgres -f /tmp/immich_dump.sql immich
|
||||||
|
docker cp immich-db:/tmp/immich_dump.sql "$BACKUP_DIR/immich_db_$DATE.sql" 2>/dev/null
|
||||||
|
|
||||||
|
# Config backups
|
||||||
|
CONFIGS=()
|
||||||
|
for p in \
|
||||||
|
/volume1/docker/nas-dashboard/.env \
|
||||||
|
/volume1/docker/nas-dashboard/auth.json \
|
||||||
|
/volume1/docker/gitea/conf \
|
||||||
|
/volume1/docker/immich/.env \
|
||||||
|
/volume1/docker/navidrome/data/navidrome.db \
|
||||||
|
/volume1/docker/openclaw/data; do
|
||||||
|
[ -e "$p" ] && CONFIGS+=("$p")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#CONFIGS[@]} -gt 0 ]; then
|
||||||
|
run_backup "Configs" tar -czf "$BACKUP_DIR/configs_$DATE.tar.gz" "${CONFIGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Immich DB
|
# Cleanup old backups
|
||||||
docker exec immich-db pg_dump -U postgres immich > "$BACKUP_DIR/immich_db_$DATE.sql"
|
log "Cleaning backups older than $RETENTION_DAYS days..."
|
||||||
if [ $? -eq 0 ]; then
|
find "$BACKUP_DIR" -type f \( -name "*.sql" -o -name "*.tar.gz" \) -mtime +$RETENTION_DAYS -delete
|
||||||
log "Immich DB dumped successfully."
|
|
||||||
|
# Summary
|
||||||
|
log "=== Backup finished ($ERRORS errors) ==="
|
||||||
|
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
notify "🔴 *NAS Backup FAILED* ($DATE)
|
||||||
|
$ERRORS error(s) — check logs"
|
||||||
else
|
else
|
||||||
log "Error dumping Immich DB!"
|
notify "🟢 *NAS Backup OK* ($DATE)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. File Backups (Configs & Data)
|
|
||||||
# Exclude heavy media files if handled separately, but for now backing up key configs
|
|
||||||
log "Compressing configuration files..."
|
|
||||||
|
|
||||||
# Dashboard & Gitea & Immich configs
|
|
||||||
# Assuming /volume1/docker structure
|
|
||||||
tar -czf "$BACKUP_DIR/configs_$DATE.tar.gz" \
|
|
||||||
-C /volume1/docker \
|
|
||||||
nas-dashboard/backend/config.py \
|
|
||||||
nas-dashboard/.env \
|
|
||||||
gitea/conf \
|
|
||||||
immich/.env \
|
|
||||||
--exclude='gitea/data' \
|
|
||||||
--exclude='immich/upload' \
|
|
||||||
--exclude='immich/thumbs'
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
log "Configs compressed successfully."
|
|
||||||
else
|
|
||||||
log "Error compressing configs!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Cleanup Old Backups
|
|
||||||
log "Cleaning up backups older than $RETENTION_DAYS days..."
|
|
||||||
find "$BACKUP_DIR" -type f -name "*_db_*.sql" -mtime +$RETENTION_DAYS -delete
|
|
||||||
find "$BACKUP_DIR" -type f -name "configs_*.tar.gz" -mtime +$RETENTION_DAYS -delete
|
|
||||||
|
|
||||||
log "Backup completed."
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
GITEA_TOKEN=your_gitea_api_token_here
|
GITEA_TOKEN=your_gitea_api_token_here
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import psutil
|
import psutil
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
|
import httpx
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/notify")
|
||||||
|
async def send_notification(body: dict):
|
||||||
|
"""Send a Telegram notification. Body: {"message": "text"}"""
|
||||||
|
msg = body.get("message", "")
|
||||||
|
if not msg or not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
||||||
|
return {"ok": False, "error": "missing message or Telegram config"}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.post(
|
||||||
|
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
|
||||||
|
data={"chat_id": TELEGRAM_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
|
||||||
|
)
|
||||||
|
return {"ok": r.status_code == 200}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
@router.get("/stats")
|
||||||
def system_stats():
|
def system_stats():
|
||||||
|
|||||||
@@ -32,9 +32,16 @@ services:
|
|||||||
- VOLUME_ROOT=/volume1
|
- VOLUME_ROOT=/volume1
|
||||||
- SSH_HOST=host.docker.internal
|
- SSH_HOST=host.docker.internal
|
||||||
- SSH_USER=zjgump
|
- SSH_USER=zjgump
|
||||||
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
||||||
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
||||||
volumes:
|
volumes:
|
||||||
- /volume1:/volume1
|
- /volume1:/volume1
|
||||||
- ./ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
- ./ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/system/stats')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ services:
|
|||||||
DB_DATABASE_NAME: ${DB_DATABASE_NAME}
|
DB_DATABASE_NAME: ${DB_DATABASE_NAME}
|
||||||
REDIS_HOSTNAME: immich-redis
|
REDIS_HOSTNAME: immich-redis
|
||||||
IMMICH_MACHINE_LEARNING_ENABLED: "false"
|
IMMICH_MACHINE_LEARNING_ENABLED: "false"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:2283/api/server/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-redis
|
- immich-redis
|
||||||
- immich-db
|
- immich-db
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ services:
|
|||||||
ND_SCANSCHEDULE: 1h
|
ND_SCANSCHEDULE: 1h
|
||||||
ND_LOGLEVEL: info
|
ND_LOGLEVEL: info
|
||||||
ND_BASEURL: ""
|
ND_BASEURL: ""
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4533/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- /volume1/music:/music:ro
|
- /volume1/music:/music:ro
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# notify.sh — Send Telegram notification
|
||||||
|
# Usage: ./notify.sh "message text"
|
||||||
|
# Requires TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars (or source from .env)
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
[ -f "$SCRIPT_DIR/.env" ] && source "$SCRIPT_DIR/.env"
|
||||||
|
|
||||||
|
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||||
|
echo "TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MSG="${1:-No message}"
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-d chat_id="$TELEGRAM_CHAT_ID" \
|
||||||
|
-d text="$MSG" \
|
||||||
|
-d parse_mode="Markdown" > /dev/null
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
OPENCLAW_GATEWAY_TOKEN=change-me-to-a-long-random-token
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
openclaw-gateway:
|
||||||
|
image: ghcr.io/openclaw/openclaw:latest
|
||||||
|
container_name: openclaw-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3100:18789"
|
||||||
|
environment:
|
||||||
|
HOME: /home/node
|
||||||
|
TERM: xterm-256color
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
|
volumes:
|
||||||
|
- ./data:/home/node/.openclaw
|
||||||
|
init: true
|
||||||
|
command:
|
||||||
|
[
|
||||||
|
"node",
|
||||||
|
"dist/index.js",
|
||||||
|
"gateway",
|
||||||
|
"--bind",
|
||||||
|
"lan",
|
||||||
|
"--port",
|
||||||
|
"18789",
|
||||||
|
]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "fetch('http://localhost:18789').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
Reference in New Issue
Block a user