fix(deploy): Docker 感知 MySQL 备份与 nx cron 安装
升级/定时备份通过 docker exec 1Panel MySQL 容器 dump,不再依赖宿主机 mysqldump;nx 菜单新增巡检与备份 crontab 安装。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+28
-85
@@ -1,102 +1,45 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Nexus — MySQL Auto-Backup Script
|
# Nexus — MySQL 定时备份(Docker 1Panel / 宿主机)
|
||||||
# Usage: Add to crontab for daily 3AM backup
|
|
||||||
# 0 3 * * * /path/to/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
|
||||||
#
|
#
|
||||||
# Reads DATABASE_URL from .env for connection parameters.
|
# 0 3 * * * NEXUS_ROOT=/opt/nexus /opt/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||||
# Retains backups for 30 days, auto-deletes older ones.
|
#
|
||||||
|
# 亦可通过: sudo bash deploy/install_ops_cron.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
NEXUS_ROOT="${NEXUS_ROOT:-$(dirname "$SCRIPT_DIR")}"
|
||||||
ENV_FILE="$PROJECT_DIR/.env"
|
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||||||
BACKUP_DIR="$PROJECT_DIR/backups"
|
RETENTION_DAYS="${NEXUS_BACKUP_RETENTION_DAYS:-30}"
|
||||||
RETENTION_DAYS=30
|
DUMP_SH="${SCRIPT_DIR}/mysql_dump_to_file.sh"
|
||||||
|
|
||||||
# ── Load .env ──
|
if [[ ! -x "$DUMP_SH" ]]; then
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
echo "[$(date)] ERROR: missing $DUMP_SH"
|
||||||
echo "[$(date)] ERROR: .env not found at $ENV_FILE"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Parse DATABASE_URL from .env
|
|
||||||
# Format: mysql+asyncmy://user:password@host:port/dbname
|
|
||||||
DB_URL=$(grep -E '^NEXUS_DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
|
||||||
if [ -z "$DB_URL" ]; then
|
|
||||||
DB_URL=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$DB_URL" ]; then
|
|
||||||
echo "[$(date)] ERROR: DATABASE_URL not found in .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse mysql URL: mysql+asyncmy://user:pass@host:port/dbname → user pass host port dbname
|
|
||||||
# Strip scheme
|
|
||||||
CONN="${DB_URL#*://}"
|
|
||||||
# Extract user:pass
|
|
||||||
USERPASS="${CONN%%@*}"
|
|
||||||
DB_USER="${USERPASS%%:*}"
|
|
||||||
DB_PASS="${USERPASS#*:}"
|
|
||||||
# Extract host:port/dbname
|
|
||||||
HOSTDB="${CONN#*@}"
|
|
||||||
DB_HOST="${HOSTDB%%:*}"
|
|
||||||
PORTDB="${HOSTDB#*:}"
|
|
||||||
DB_PORT="${PORTDB%%/*}"
|
|
||||||
DB_NAME="${PORTDB#*/}"
|
|
||||||
# Remove query params
|
|
||||||
DB_NAME="${DB_NAME%%\?*}"
|
|
||||||
|
|
||||||
if [ -z "$DB_NAME" ] || [ -z "$DB_HOST" ] || [ -z "$DB_USER" ]; then
|
|
||||||
echo "[$(date)] ERROR: Failed to parse DATABASE_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Create backup directory ──
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
RAW_FILE="${BACKUP_DIR}/nexus_daily_${TIMESTAMP}.sql"
|
||||||
|
BACKUP_FILE="${RAW_FILE}.gz"
|
||||||
|
|
||||||
# ── Backup filename ──
|
echo "[$(date)] Starting backup → $BACKUP_FILE"
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
if ! NEXUS_ROOT="$NEXUS_ROOT" bash "$DUMP_SH" --output "$RAW_FILE" --root "$NEXUS_ROOT"; then
|
||||||
BACKUP_FILE="$BACKUP_DIR/nexus_${DB_NAME}_${TIMESTAMP}.sql.gz"
|
echo "[$(date)] ERROR: mysqldump failed"
|
||||||
|
rm -f "$RAW_FILE"
|
||||||
# ── Run mysqldump ──
|
|
||||||
echo "[$(date)] Starting backup: $DB_NAME @ $DB_HOST"
|
|
||||||
|
|
||||||
if [ -n "$DB_PASS" ]; then
|
|
||||||
MYSQL_PWD="$DB_PASS" mysqldump \
|
|
||||||
-h "$DB_HOST" \
|
|
||||||
-P "${DB_PORT:-3306}" \
|
|
||||||
-u "$DB_USER" \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--set-gtid-purged=OFF \
|
|
||||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
|
||||||
else
|
|
||||||
mysqldump \
|
|
||||||
-h "$DB_HOST" \
|
|
||||||
-P "${DB_PORT:-3306}" \
|
|
||||||
-u "$DB_USER" \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--set-gtid-purged=OFF \
|
|
||||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Verify backup ──
|
|
||||||
if [ -f "$BACKUP_FILE" ]; then
|
|
||||||
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
|
||||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
|
||||||
else
|
|
||||||
echo "[$(date)] ERROR: Backup file not created!"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Cleanup old backups (retain 30 days) ──
|
gzip -f "$RAW_FILE"
|
||||||
DELETED=$(find "$BACKUP_DIR" -name "nexus_*.sql.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
|
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||||
if [ "$DELETED" -gt 0 ]; then
|
echo "[$(date)] ERROR: backup file not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIZE="$(du -h "$BACKUP_FILE" | cut -f1)"
|
||||||
|
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||||
|
|
||||||
|
DELETED="$(find "$BACKUP_DIR" -name 'nexus_*.sql.gz' -mtime +"${RETENTION_DAYS}" -delete -print 2>/dev/null | wc -l)"
|
||||||
|
if [[ "$DELETED" -gt 0 ]]; then
|
||||||
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
|
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Nexus — 安装/更新 host crontab:health_monitor + db_backup
|
||||||
|
#
|
||||||
|
# sudo bash deploy/install_ops_cron.sh
|
||||||
|
# NEXUS_ROOT=/opt/nexus sudo bash deploy/install_ops_cron.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||||
|
DEPLOY_DIR="${NEXUS_ROOT}/deploy"
|
||||||
|
HEALTH_MARKER="nexus-health-monitor"
|
||||||
|
BACKUP_MARKER="nexus-mysql-backup"
|
||||||
|
HEALTH_LOG="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||||
|
BACKUP_LOG="${NEXUS_BACKUP_LOG:-/var/log/nexus_backup.log}"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
error "请使用 root 运行: sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${DEPLOY_DIR}/health_monitor.sh" "${DEPLOY_DIR}/db_backup.sh"; do
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
error "缺少脚本: $f"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod +x "$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
touch "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||||
|
chmod 644 "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||||
|
|
||||||
|
HEALTH_LINE="* * * * * NEXUS_DEPLOY_DIR=${NEXUS_ROOT} ${DEPLOY_DIR}/health_monitor.sh >> ${HEALTH_LOG} 2>&1 # ${HEALTH_MARKER}"
|
||||||
|
BACKUP_LINE="0 3 * * * NEXUS_ROOT=${NEXUS_ROOT} ${DEPLOY_DIR}/db_backup.sh >> ${BACKUP_LOG} 2>&1 # ${BACKUP_MARKER}"
|
||||||
|
|
||||||
|
existing="$(crontab -l 2>/dev/null || true)"
|
||||||
|
filtered="$(printf '%s\n' "$existing" | grep -v "$HEALTH_MARKER" | grep -v "$BACKUP_MARKER" | grep -v 'deploy/health_monitor.sh' | grep -v 'deploy/db_backup.sh' || true)"
|
||||||
|
{
|
||||||
|
printf '%s\n' "$filtered" | sed '/^[[:space:]]*$/d'
|
||||||
|
echo "$HEALTH_LINE"
|
||||||
|
echo "$BACKUP_LINE"
|
||||||
|
} | crontab -
|
||||||
|
|
||||||
|
info "已写入 crontab:"
|
||||||
|
crontab -l | grep -E "$HEALTH_MARKER|$BACKUP_MARKER" || true
|
||||||
|
info "健康巡检日志: $HEALTH_LOG"
|
||||||
|
info "MySQL 备份日志: $BACKUP_LOG"
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Nexus — MySQL dump(Docker 1Panel / 宿主机 mysqldump 通用)
|
||||||
|
#
|
||||||
|
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql [--root /opt/nexus]
|
||||||
|
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql --db-url 'mysql+aiomysql://...'
|
||||||
|
#
|
||||||
|
# 策略(按序):
|
||||||
|
# 1. DB host 为运行中容器名 → docker exec 该容器内 mysqldump(连 127.0.0.1)
|
||||||
|
# 2. 宿主机有 mysqldump 且 host 可解析 → 本机 mysqldump
|
||||||
|
# 3. 1panel-network 存在 → docker run --rm mysql:8 客户端
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||||
|
OUTPUT=""
|
||||||
|
DB_URL=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法: mysql_dump_to_file.sh --output FILE.sql [--root /opt/nexus] [--db-url URL]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--output|-o) OUTPUT="$2"; shift 2 ;;
|
||||||
|
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||||
|
--db-url) DB_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
echo "缺少 --output" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_db_url() {
|
||||||
|
if [[ -n "$DB_URL" ]]; then
|
||||||
|
echo "$DB_URL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local cname url
|
||||||
|
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||||
|
if [[ -n "$cname" ]]; then
|
||||||
|
url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||||
|
[[ -n "$url" ]] && echo "$url" && return 0
|
||||||
|
fi
|
||||||
|
for f in \
|
||||||
|
"${NEXUS_ROOT}/docker/.env.prod" \
|
||||||
|
"${NEXUS_ROOT}/.env" \
|
||||||
|
; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
url="$(grep -E '^NEXUS_DATABASE_URL=' "$f" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||||
|
[[ -n "$url" ]] && echo "$url" && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DB_URL="$(resolve_db_url || true)"
|
||||||
|
if [[ -z "$DB_URL" ]]; then
|
||||||
|
echo "未找到 NEXUS_DATABASE_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$DB_URL" "$OUTPUT" <<'PY'
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
|
||||||
|
def docker_names() -> set[str]:
|
||||||
|
r = subprocess.run(
|
||||||
|
["docker", "ps", "--format", "{{.Names}}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return {ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def network_exists(name: str) -> bool:
|
||||||
|
return (
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "network", "inspect", name],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
).returncode
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_dump(cmd: list[str], env: dict[str, str], out: str) -> int:
|
||||||
|
r = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
||||||
|
return r.returncode
|
||||||
|
with open(out, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(r.stdout)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def dump_flags(db: str) -> list[str]:
|
||||||
|
return [
|
||||||
|
"--single-transaction",
|
||||||
|
"--routines",
|
||||||
|
"--triggers",
|
||||||
|
"--set-gtid-purged=OFF",
|
||||||
|
db,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
||||||
|
url = url.replace("mysql+asyncmy://", "mysql://", 1)
|
||||||
|
out = sys.argv[2]
|
||||||
|
p = urllib.parse.urlparse(url)
|
||||||
|
user = urllib.parse.unquote(p.username or "")
|
||||||
|
password = urllib.parse.unquote(p.password or "")
|
||||||
|
host = p.hostname or "127.0.0.1"
|
||||||
|
if host == "host.docker.internal":
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = str(p.port or 3306)
|
||||||
|
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
||||||
|
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
||||||
|
names = docker_names()
|
||||||
|
|
||||||
|
# 1) MySQL 跑在 Docker 容器内(1Panel 常见:URL host = 容器名)
|
||||||
|
if host in names:
|
||||||
|
cmd = [
|
||||||
|
"docker",
|
||||||
|
"exec",
|
||||||
|
"-e",
|
||||||
|
f"MYSQL_PWD={password}",
|
||||||
|
host,
|
||||||
|
"mysqldump",
|
||||||
|
"-h",
|
||||||
|
"127.0.0.1",
|
||||||
|
"-P",
|
||||||
|
port,
|
||||||
|
"-u",
|
||||||
|
user,
|
||||||
|
*dump_flags(db),
|
||||||
|
]
|
||||||
|
rc = write_dump(cmd, env, out)
|
||||||
|
if rc == 0:
|
||||||
|
sys.exit(0)
|
||||||
|
sys.stderr.write(f"docker exec {host} mysqldump 失败,尝试其它方式…\n")
|
||||||
|
|
||||||
|
# 2) 宿主机 mysqldump
|
||||||
|
if shutil.which("mysqldump"):
|
||||||
|
cmd = [
|
||||||
|
"mysqldump",
|
||||||
|
"-h",
|
||||||
|
host,
|
||||||
|
"-P",
|
||||||
|
port,
|
||||||
|
"-u",
|
||||||
|
user,
|
||||||
|
*dump_flags(db),
|
||||||
|
]
|
||||||
|
rc = write_dump(cmd, env, out)
|
||||||
|
if rc == 0:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 3) 临时 mysql 客户端容器(1panel-network)
|
||||||
|
for net in ("1panel-network", "bridge"):
|
||||||
|
if not network_exists(net):
|
||||||
|
continue
|
||||||
|
cmd = [
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--network",
|
||||||
|
net,
|
||||||
|
"-e",
|
||||||
|
f"MYSQL_PWD={password}",
|
||||||
|
"mysql:8.0",
|
||||||
|
"mysqldump",
|
||||||
|
"-h",
|
||||||
|
host,
|
||||||
|
"-P",
|
||||||
|
port,
|
||||||
|
"-u",
|
||||||
|
user,
|
||||||
|
*dump_flags(db),
|
||||||
|
]
|
||||||
|
rc = write_dump(cmd, env, out)
|
||||||
|
if rc == 0:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
sys.stderr.write("所有 mysqldump 策略均失败(无宿主机客户端且 Docker 回退不可用)\n")
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
+6
-40
@@ -1051,55 +1051,21 @@ show_pending() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backup_mysql() {
|
backup_mysql() {
|
||||||
local root="$1" ts file db_url cname
|
local root="$1" ts file dump_sh
|
||||||
if [[ "$NO_BACKUP" == true ]]; then
|
if [[ "$NO_BACKUP" == true ]]; then
|
||||||
warn "已跳过备份 (--no-backup)"
|
warn "已跳过备份 (--no-backup)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! command -v mysqldump >/dev/null 2>&1; then
|
dump_sh="${root}/deploy/mysql_dump_to_file.sh"
|
||||||
warn "未安装 mysqldump,跳过 MySQL 备份"
|
if [[ ! -x "$dump_sh" ]]; then
|
||||||
return 0
|
warn "缺少 $dump_sh,跳过 MySQL 备份"
|
||||||
fi
|
|
||||||
cname="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
|
||||||
db_url=""
|
|
||||||
if [[ -n "$cname" ]]; then
|
|
||||||
db_url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "$db_url" ]]; then
|
|
||||||
warn "未找到 NEXUS_DATABASE_URL(外置 MySQL 需先完成安装向导),跳过备份"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
mkdir -p "$NEXUS_BACKUP_DIR"
|
mkdir -p "$NEXUS_BACKUP_DIR"
|
||||||
ts="$(date +%Y%m%d-%H%M%S)"
|
ts="$(date +%Y%m%d-%H%M%S)"
|
||||||
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
|
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
|
||||||
step "备份外置 MySQL → $file"
|
step "备份 MySQL → $file"
|
||||||
if ! python3 - "$db_url" "$file" <<'PY'
|
if ! NEXUS_ROOT="$root" bash "$dump_sh" --output "$file" --root "$root"; then
|
||||||
import sys
|
|
||||||
import urllib.parse
|
|
||||||
from subprocess import run
|
|
||||||
|
|
||||||
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
|
||||||
out = sys.argv[2]
|
|
||||||
p = urllib.parse.urlparse(url)
|
|
||||||
user = urllib.parse.unquote(p.username or "")
|
|
||||||
password = urllib.parse.unquote(p.password or "")
|
|
||||||
host = p.hostname or "127.0.0.1"
|
|
||||||
if host == "host.docker.internal":
|
|
||||||
host = "127.0.0.1"
|
|
||||||
port = str(p.port or 3306)
|
|
||||||
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
|
||||||
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
|
||||||
cmd = [
|
|
||||||
"mysqldump", "-h", host, "-P", port, "-u", user,
|
|
||||||
"--single-transaction", "--routines", "--triggers", "--set-gtid-purged=OFF", db,
|
|
||||||
]
|
|
||||||
r = run(cmd, env=env, capture_output=True, text=True)
|
|
||||||
if r.returncode != 0:
|
|
||||||
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
|
||||||
sys.exit(r.returncode)
|
|
||||||
open(out, "w", encoding="utf-8").write(r.stdout)
|
|
||||||
PY
|
|
||||||
then
|
|
||||||
if [[ "$REQUIRE_BACKUP" == true ]]; then
|
if [[ "$REQUIRE_BACKUP" == true ]]; then
|
||||||
error "mysqldump 失败,已中止(--require-backup)"
|
error "mysqldump 失败,已中止(--require-backup)"
|
||||||
rm -f "$file"
|
rm -f "$file"
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ ops_backup_now() {
|
|||||||
backup_mysql "$NEXUS_ROOT"
|
backup_mysql "$NEXUS_ROOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ops_install_cron() {
|
||||||
|
bash "${NX_DIR}/install_ops_cron.sh"
|
||||||
|
}
|
||||||
|
|
||||||
ops_update() {
|
ops_update() {
|
||||||
local args=()
|
local args=()
|
||||||
PRUNE_IMAGES=false
|
PRUNE_IMAGES=false
|
||||||
@@ -265,6 +269,7 @@ menu_main() {
|
|||||||
[7] 启动 / 停止 Compose 栈
|
[7] 启动 / 停止 Compose 栈
|
||||||
[8] 重建镜像并启动(--build nexus)
|
[8] 重建镜像并启动(--build nexus)
|
||||||
[9] 备份 MySQL
|
[9] 备份 MySQL
|
||||||
|
[e] 安装巡检/备份 cron(health_monitor + db_backup)
|
||||||
|
|
||||||
── 其它 ──
|
── 其它 ──
|
||||||
[a] 检查 Git 是否有更新
|
[a] 检查 Git 是否有更新
|
||||||
@@ -338,6 +343,11 @@ EOF
|
|||||||
ops_backup_now
|
ops_backup_now
|
||||||
pause_enter
|
pause_enter
|
||||||
;;
|
;;
|
||||||
|
e|E)
|
||||||
|
require_root
|
||||||
|
ops_install_cron
|
||||||
|
pause_enter
|
||||||
|
;;
|
||||||
a|A)
|
a|A)
|
||||||
require_root
|
require_root
|
||||||
cmd_check
|
cmd_check
|
||||||
@@ -399,6 +409,10 @@ nx_main() {
|
|||||||
require_root
|
require_root
|
||||||
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
||||||
;;
|
;;
|
||||||
|
cron)
|
||||||
|
require_root
|
||||||
|
ops_install_cron
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
用法: nx [子命令]
|
用法: nx [子命令]
|
||||||
@@ -408,6 +422,7 @@ nx_main() {
|
|||||||
install-fresh 全新安装(/app/install.html)
|
install-fresh 全新安装(/app/install.html)
|
||||||
install-auto 非交互安装(默认 2c8g)
|
install-auto 非交互安装(默认 2c8g)
|
||||||
health 健康检查
|
health 健康检查
|
||||||
|
cron 安装 health_monitor + db_backup crontab
|
||||||
|
|
||||||
god / ops 已合并进主菜单(兼容别名)
|
god / ops 已合并进主菜单(兼容别名)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# nx 上帝菜单巡检第四轮 — Docker MySQL 备份 + 运维 cron
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|----|------|
|
||||||
|
| 日期 | 2026-06-06 |
|
||||||
|
| 动机 | 升级时 `backup_mysql` 因宿主机无 `mysqldump` 且 URL host 为 1Panel 容器名而静默跳过;`db_backup.sh` 仍读 Supervisor 时代 `.env` |
|
||||||
|
|
||||||
|
## 变更摘要
|
||||||
|
|
||||||
|
1. **mysql_dump_to_file.sh**(新):统一 mysqldump 策略 — `docker exec` MySQL 容器 → 宿主机客户端 → `docker run` on `1panel-network`。
|
||||||
|
2. **backup_mysql**:改调共享脚本,不再要求宿主机预装 `mysqldump`。
|
||||||
|
3. **db_backup.sh**:Docker 生产路径,从 Nexus 容器/volume 解析 `NEXUS_DATABASE_URL`,默认 `/var/backups/nexus`。
|
||||||
|
4. **install_ops_cron.sh**(新):幂等写入 `health_monitor`(每分钟)与 `db_backup`(每日 03:00)crontab。
|
||||||
|
5. **nx**:菜单 `[e]` / 子命令 `nx cron` 安装上述 cron。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `deploy/mysql_dump_to_file.sh`
|
||||||
|
- `deploy/install_ops_cron.sh`
|
||||||
|
- `deploy/db_backup.sh`
|
||||||
|
- `deploy/nexus-1panel.sh`
|
||||||
|
- `deploy/nx`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
- 无需 DB 迁移;仅需 `git pull` 更新 host 脚本。
|
||||||
|
- 可选:`sudo nx cron` 或菜单 `[e]` 安装 crontab。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n deploy/mysql_dump_to_file.sh deploy/install_ops_cron.sh deploy/db_backup.sh
|
||||||
|
sudo bash deploy/mysql_dump_to_file.sh --output /tmp/nexus-test.sql --root /opt/nexus
|
||||||
|
ls -lh /var/backups/nexus/
|
||||||
|
sudo nx cron && crontab -l | grep nexus
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user