🚀 运行: ...
IP: ...
ISP: 检测中...

WordPress 自动备份脚本:定时打包 + 异地备份到另一台 VPS

脚本功能

  • 一键打包:数据库(mysqldump,--single-transaction 不锁表)+ WordPress 文件(docker 数据卷里的 wp-content、主题、插件)+ compose 配置(docker-compose.yml、.env),合成一个 blog-full-YYYY-MM-DD.tgz
  • 异地备份:scp 传到另一台 VPS,传完对比两端文件大小,确认无损坏
  • 自动清理:本地和远程各保留最近 N 天(默认 14 天),旧包自动删除
  • 交互菜单:安装 / 定时 / 状态查看 / 立即备份 / 卸载
  • cron 定时:设好后每天凌晨自动跑

安装

  1. 把下面脚本保存为 blog-backup.sh 传到博客 VPS(例如 ~/blog-backup.sh),执行 chmod +x ~/blog-backup.sh
  2. 运行 bash ~/blog-backup.sh,选 1) 安装/初始化
  3. 按提示填写:compose 目录、保留天数、目标 VPS 地址 / 用户名 / SSH 端口 / 存放目录
  4. 登录方式(二选一):
    • 密钥登录:粘贴对方 VPS 的私钥全文
    • 密码登录:输一次对方密码,脚本自动生成密钥对并复制公钥,之后全程走密钥
  5. 选 4) 立即备份一次 验证全流程
  6. 选 2) 定时备份设置,例如每天 3:00

发文后自动备份(可选)

菜单 → 2) 定时备份设置 → 5) 开启发文触发备份:每 5 分钟检查一次数据库,发文 15 分钟后自动备份一次(15 分钟缓冲是给你改错别字的时间)。和每天定时备份不冲突,可以同时开。

完整脚本

#!/bin/bash
# blog-backup.sh — WordPress 博客备份管理(含交互菜单)
#
# 用法:
#   bash ~/blog-backup.sh            # 打开管理菜单(无参数时:终端里进菜单,cron 里自动跑备份)
#   bash ~/blog-backup.sh backup     # 直接备份一次(定时任务用这个)
#   bash ~/blog-backup.sh status     # 查看状态
#
# 首次使用:打开菜单选"1) 安装/初始化",按提示配置备份目标 VPS。
# 前提:用户已知远端 VPS 的密码或私钥,安装时自己选一种登录方式。
# 备份内容:数据库 + WordPress 文件 + compose 配置,打包成一个 tgz 传到目标 VPS。
# 密钥只存在本机 600 权限的文件里,绝不发送到别处。

set -euo pipefail

SCRIPT_PATH="$HOME/blog-backup.sh"
CONF_FILE="$HOME/.blog-backup.conf"
CRON_MARK="blog-backup.sh backup"
WATCH_MARK="blog-backup.sh check-publish"
PUBLISH_STATE="$HOME/.blog-backup-publish-state"   # 记录上次已备份的文章时间戳

# ---- 默认配置(安装后会存到 ~/.blog-backup.conf,下次自动加载) ----
BLOG_DIR="$HOME/my-wordpress"
BACKUP_LOCAL_DIR="$HOME/blog-backups"
RETENTION_DAYS=14
LOG_FILE="$HOME/blog-backup.log"

REMOTE_USER="root"
REMOTE_HOST="__BACKUP_VPS_IP__"      # ← 占位符,安装时填写备份目标 VPS 的真实地址
REMOTE_PORT="__SSH_PORT__"           # ← 占位符,安装时填写目标 VPS 的 SSH 端口
REMOTE_DIR="/home/wp-back"
SSH_KEY="$HOME/.ssh/id_ed25519"

if [ -f "$CONF_FILE" ]; then
  # shellcheck disable=SC1090
  source "$CONF_FILE"
fi

# 注意:ssh 的端口参数是小写 -p,scp 的是大写 -P
SSH_OPTS="-i $SSH_KEY -p $REMOTE_PORT -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"
SCP_OPTS="-i $SSH_KEY -P $REMOTE_PORT -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"

log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"; }
fail() { log "ERROR: $*"; exit 1; }

save_conf() {
  {
    echo "BLOG_DIR=$(printf %q "$BLOG_DIR")"
    echo "BACKUP_LOCAL_DIR=$(printf %q "$BACKUP_LOCAL_DIR")"
    echo "RETENTION_DAYS=$RETENTION_DAYS"
    echo "LOG_FILE=$(printf %q "$LOG_FILE")"
    echo "REMOTE_USER=$(printf %q "$REMOTE_USER")"
    echo "REMOTE_HOST=$(printf %q "$REMOTE_HOST")"
    echo "REMOTE_PORT=$REMOTE_PORT"
    echo "REMOTE_DIR=$(printf %q "$REMOTE_DIR")"
    echo "SSH_KEY=$(printf %q "$SSH_KEY")"
  } > "$CONF_FILE"
  chmod 600 "$CONF_FILE"
  echo "配置已保存到 $CONF_FILE(600 权限)"
}

# ================= 备份逻辑 =================
do_backup() {
  DATE=$(date +%F)
  TMPDIR=$(mktemp -d)
  BACKUP_NAME="blog-full-${DATE}.tgz"

  mkdir -p "$BACKUP_LOCAL_DIR"
  cd "$BLOG_DIR" || fail "找不到博客目录 $BLOG_DIR"

  log "开始备份 ..."

  # 1. 数据库(--single-transaction 保证 InnoDB 一致性,不锁表)
  log "导出数据库..."
  docker compose exec -T db sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction wordpress' > "$TMPDIR/blog-db.sql" \
    || fail "数据库导出失败"

  # 2. 文件卷(wp-content、主题、插件都在里面)
  #    直接从 wordpress 容器查挂到 /var/www/html 的卷/目录,不依赖卷名
  CID=$(docker compose ps -q wordpress 2>/dev/null | head -1 || true)
  MNT=""
  if [ -n "$CID" ]; then
    MNT=$(docker inspect "$CID" --format '{{range .Mounts}}{{if eq .Destination "/var/www/html"}}{{.Type}}|{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' 2>/dev/null || true)
  fi
  if [ -z "$MNT" ]; then
    GUESS=$(docker volume ls --format '{{.Name}}' | grep -i wp_data | head -1 || true)
    if [ -n "$GUESS" ]; then MNT="volume|$GUESS"; fi
  fi
  MTYPE="${MNT%%|*}"; MSRC="${MNT#*|}"
  if [ -z "$MSRC" ]; then fail "找不到 wordpress 挂载到 /var/www/html 的数据(卷或目录)"; fi
  log "打包文件数据 $MTYPE:$MSRC ..."
  docker run --rm -v "$MSRC:/data:ro" -v "$TMPDIR:/backup" alpine tar -czf /backup/blog-files.tgz -C /data . \
    || fail "文件打包失败"

  # 3. compose 配置(.env 含数据库密码和 tunnel token,妥善保管)
  tar -czf "$TMPDIR/blog-compose.tgz" -C "$BLOG_DIR" .env docker-compose.yml \
    || fail "配置打包失败"

  # 4. 合成一个总包
  tar -czf "$BACKUP_LOCAL_DIR/$BACKUP_NAME" -C "$TMPDIR" blog-db.sql blog-files.tgz blog-compose.tgz
  rm -rf "$TMPDIR"
  log "本地打包完成: $BACKUP_LOCAL_DIR/$BACKUP_NAME ($(du -h "$BACKUP_LOCAL_DIR/$BACKUP_NAME" | cut -f1))"

  # 5. 传到备份目标 VPS(占位符表示还没配置)
  if [ "$REMOTE_HOST" = "__BACKUP_VPS_IP__" ] || [ "$REMOTE_PORT" = "__SSH_PORT__" ]; then
    fail "还没配置备份目标 VPS,请先运行管理菜单做「安装/初始化」"
  fi
  log "传输到 $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR ..."
  # shellcheck disable=SC2086
  ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "mkdir -p $REMOTE_DIR" \
    || fail "SSH 连接目标 VPS 失败(检查地址/端口/密钥)"
  # shellcheck disable=SC2086
  scp $SCP_OPTS "$BACKUP_LOCAL_DIR/$BACKUP_NAME" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \
    || fail "scp 传输失败"

  LOCAL_SIZE=$(stat -c%s "$BACKUP_LOCAL_DIR/$BACKUP_NAME")
  # shellcheck disable=SC2086
  REMOTE_SIZE=$(ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "stat -c%s $REMOTE_DIR/$BACKUP_NAME")
  if [ "$LOCAL_SIZE" != "$REMOTE_SIZE" ]; then
    fail "远程文件大小不一致($LOCAL_SIZE vs $REMOTE_SIZE),传输可能损坏"
  fi
  log "远程校验通过,大小一致"

  find "$BACKUP_LOCAL_DIR" -name 'blog-full-*.tgz' -mtime +"$RETENTION_DAYS" -delete
  # shellcheck disable=SC2086
  ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "find $REMOTE_DIR -name 'blog-full-*.tgz' -mtime +$RETENTION_DAYS -delete"
  log "过期备份已清理(本地 + 远程各保留最近 $RETENTION_DAYS 天)"

  log "备份完成"
}

# ================= 1. 安装/初始化 =================
do_install() {
  echo "---- 安装/初始化 ----"
  v=""; read -rp "博客 compose 目录 [$BLOG_DIR]: " v || v=""
  if [ -n "$v" ]; then BLOG_DIR="$v"; fi
  v=""; read -rp "保留天数 [$RETENTION_DAYS]: " v || v=""
  if [ -n "$v" ]; then RETENTION_DAYS="$v"; fi
  v=""; read -rp "备份目标 VPS 地址 [$REMOTE_HOST]: " v || v=""
  if [ -n "$v" ]; then REMOTE_HOST="$v"; fi
  v=""; read -rp "目标用户名 [$REMOTE_USER]: " v || v=""
  if [ -n "$v" ]; then REMOTE_USER="$v"; fi
  v=""; read -rp "目标端口 [$REMOTE_PORT]: " v || v=""
  if [ -n "$v" ]; then REMOTE_PORT="$v"; fi
  v=""; read -rp "目标存放目录 [$REMOTE_DIR]: " v || v=""
  if [ -n "$v" ]; then REMOTE_DIR="$v"; fi

  # ---- 登录方式:用户已知远端密码或密钥,自己选一种 ----
  # 1) 密钥登录:粘贴对方 VPS 的私钥,之后全程走密钥
  # 2) 密码登录:输一次对方密码,脚本自动生成密钥对并把公钥拷过去,之后全程走密钥
  echo "登录方式:1) 密钥登录(粘贴对方 VPS 私钥)  2) 密码登录(输一次对方密码,自动配置密钥)"
  lm=""; read -rp "请选择 [1-2]: " lm || lm=""
  SSH_KEY="$HOME/.ssh/backup_remote_key"
  mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
  case "$lm" in
    2)
      if [ ! -f "$SSH_KEY" ]; then
        echo "正在生成密钥对..."
        ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" || { echo "密钥生成失败"; return 1; }
      fi
      echo "正在复制公钥到 $REMOTE_USER@$REMOTE_HOST(需要输入一次对方密码)..."
      if ! ssh-copy-id -i "$SSH_KEY.pub" -p "$REMOTE_PORT" -o ConnectTimeout=15 "$REMOTE_USER@$REMOTE_HOST"; then
        echo "公钥复制失败:请检查地址/端口/密码,或对方是否禁用了密码登录"
        return 1
      fi
      echo "公钥已复制,之后备份全程走密钥,不再需要密码"
      ;;
    *)
      echo "粘贴对方 VPS 的私钥全文(包括 BEGIN / END 行),结束后单独一行输入 END:"
      tmp=$(mktemp)
      while IFS= read -r line; do
        if [ "$line" = "END" ]; then break; fi
        printf '%s\n' "$line" >> "$tmp"
      done
      if ! grep -q "PRIVATE KEY" "$tmp"; then
        echo "内容不像私钥,已取消"; rm -f "$tmp"; return 1
      fi
      mv "$tmp" "$SSH_KEY"; chmod 600 "$SSH_KEY"
      echo "私钥已保存到 $SSH_KEY(600 权限),走密钥登录"
      ;;
  esac
  SSH_OPTS="-i $SSH_KEY -p $REMOTE_PORT -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"
  SCP_OPTS="-i $SSH_KEY -P $REMOTE_PORT -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"

  # 清掉该主机的旧 host key 记录(机器重装后 key 会变,否则校验失败连不上)
  ssh-keygen -R "$REMOTE_HOST" 2>/dev/null || true
  ssh-keygen -R "[$REMOTE_HOST]:$REMOTE_PORT" 2>/dev/null || true

  echo "测试连接..."
  # shellcheck disable=SC2086
  if ! ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "mkdir -p $REMOTE_DIR && echo OK"; then
    echo "连接失败:检查地址/端口/用户名/私钥是否正确(私钥若有密码,定时任务无法自动使用,请换无密码私钥)"
    return 1
  fi

  save_conf
  echo
  echo "初始化完成。建议选「4) 立即备份一次」验证全流程,再设定时。"
}

# ================= 发文触发备份(哨兵) =================
# 查最新已发布文章的时间戳(UTC 秒)。$0 技巧:把带单引号的 SQL
# 当作 sh 的 $0 参数传进容器,避免宿主机/容器两层引号打架。
newest_post_time() {
  local sql="SELECT IFNULL(UNIX_TIMESTAMP(MAX(post_date_gmt)),0) FROM wp_posts WHERE post_type='post' AND post_status='publish';"
  ( cd "$BLOG_DIR" && docker compose exec -T db sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -N -B wordpress -e "$0"' "$sql" 2>/dev/null | tr -d ' \r\n' ) || echo 0
}

# cron 每 5 分钟调一次:有 15 分钟前发表的新文章 → 备份一次
do_check_publish() {
  [ -f "$CONF_FILE" ] || exit 0
  P=$(newest_post_time)
  [[ "$P" =~ ^[0-9]+$ ]] || exit 0
  [ "$P" = "0" ] && exit 0
  LAST=$(cat "$PUBLISH_STATE" 2>/dev/null || echo 0)
  [[ "$LAST" =~ ^[0-9]+$ ]] || LAST=0
  NOW=$(date +%s)
  if [ "$P" -gt "$LAST" ] && [ $((NOW - P)) -ge 900 ]; then
    log "检测到 15 分钟前有新文章发表,开始备份..."
    if ( do_backup ); then
      echo "$P" > "$PUBLISH_STATE"
    fi
  fi
}

watch_current() { crontab -l 2>/dev/null | grep -F "$WATCH_MARK" || true; }

watch_remove() {
  crontab -l 2>/dev/null | grep -v -F "$WATCH_MARK" | crontab - 2>/dev/null || true
}

watch_enable() {
  # 先记下当前最新文章时间,避免把旧文章当成新文章触发一次
  P=$(newest_post_time)
  if [[ "$P" =~ ^[0-9]+$ ]] && [ "$P" != "0" ]; then
    echo "$P" > "$PUBLISH_STATE"
  fi
  watch_remove
  ( crontab -l 2>/dev/null; echo "*/5 * * * * /bin/bash $SCRIPT_PATH check-publish # $WATCH_MARK" ) | crontab -
  echo "已开启:每 5 分钟检查一次,发文 15 分钟后自动备份"
}

# ================= 2. 定时备份设置 =================
cron_current() { crontab -l 2>/dev/null | grep -F "$CRON_MARK" || true; }

cron_remove() {
  crontab -l 2>/dev/null | grep -v -F "$CRON_MARK" | crontab - 2>/dev/null || true
}

cron_set() {
  local exp="$1"
  cron_remove
  ( crontab -l 2>/dev/null; echo "$exp /bin/bash $SCRIPT_PATH backup # $CRON_MARK" ) | crontab -
  echo "已设置:$exp"
}

do_schedule() {
  echo "---- 定时备份设置 ----"
  cur=$(cron_current)
  if [ -n "$cur" ]; then echo "当前定时:$cur"; else echo "当前:未设置定时备份"; fi
  wc=$(watch_current)
  if [ -n "$wc" ]; then echo "发文触发:已开启(每5分钟检查,发文15分钟后备份)"; else echo "发文触发:未开启"; fi
  echo "1) 每天 3:00 定时备份"
  echo "2) 每周日 3:30 定时备份"
  echo "3) 自定义 cron 表达式"
  echo "4) 关闭定时备份"
  echo "5) 开启发文触发备份"
  echo "6) 关闭发文触发备份"
  echo "0) 返回"
  c=""; read -rp "请选择: " c || c=""
  case "$c" in
    1) cron_set "0 3 * * *" ;;
    2) cron_set "30 3 * * 0" ;;
    3) exp=""; read -rp "输入 cron 表达式(如 0 4 * * *): " exp || exp=""
       if [ -z "$exp" ]; then echo "未输入,已取消"; else cron_set "$exp"; fi ;;
    4) cron_remove; echo "已关闭定时备份" ;;
    5) watch_enable ;;
    6) watch_remove; echo "已关闭发文触发备份" ;;
    *) echo "已取消" ;;
  esac
}

# ================= 3. 查看状态 =================
do_status() {
  echo "---- 状态 ----"
  echo "博客目录:$BLOG_DIR"
  echo "保留天数:$RETENTION_DAYS 天"
  echo "备份目标:$REMOTE_USER@$REMOTE_HOST:$REMOTE_PORT:$REMOTE_DIR"
  echo
  cur=$(cron_current)
  if [ -n "$cur" ]; then echo "定时任务:$cur"; else echo "定时任务:未设置"; fi
  wc=$(watch_current)
  if [ -n "$wc" ]; then echo "发文触发:已开启"; else echo "发文触发:未开启"; fi
  echo
  echo "本地备份:"
  if [ -d "$BACKUP_LOCAL_DIR" ] && ls "$BACKUP_LOCAL_DIR"/blog-full-*.tgz >/dev/null 2>&1; then
    ls -lh "$BACKUP_LOCAL_DIR"/blog-full-*.tgz | awk '{print "  " $9 "  (" $5 ")"}'
    echo "  共占用:$(du -sh "$BACKUP_LOCAL_DIR" | cut -f1)"
  else
    echo "  (暂无)"
  fi
  echo
  echo -n "SSH 连通性:"
  # shellcheck disable=SC2086
  if ssh $SSH_OPTS -o ConnectTimeout=8 "$REMOTE_USER@$REMOTE_HOST" "echo OK" 2>/dev/null | grep -q OK; then
    echo "正常"
    echo "远程备份:"
    # shellcheck disable=SC2086
    ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "ls -lh $REMOTE_DIR/blog-full-*.tgz 2>/dev/null | awk '{print \"  \" \$9 \"  (\" \$5 \")\"}' || echo '  (暂无)'"
  else
    echo "连接失败(检查地址/端口/密钥)"
  fi
  echo
  echo "最近日志:"
  tail -n 12 "$LOG_FILE" 2>/dev/null || echo "  (暂无日志)"
}

# ================= 5. 卸载 =================
do_uninstall() {
  echo "---- 卸载 ----"
  echo "将执行:删除定时任务、删除配置文件"
  a=""; read -rp "同时删除本地备份文件吗?(y/N): " a || a=""
  b=""; read -rp "同时删除远程备份文件吗?(y/N): " b || b=""
  c=""; read -rp "确认卸载?(y/N): " c || c=""
  if [ "$c" != "y" ] && [ "$c" != "Y" ]; then echo "已取消"; return 0; fi
  cron_remove
  rm -f "$CONF_FILE"
  if [ "$a" = "y" ] || [ "$a" = "Y" ]; then rm -rf "$BACKUP_LOCAL_DIR"; echo "本地备份已删除"; fi
  if [ "$b" = "y" ] || [ "$b" = "Y" ]; then
    # shellcheck disable=SC2086
    ssh $SSH_OPTS "$REMOTE_USER@$REMOTE_HOST" "rm -rf $REMOTE_DIR" 2>/dev/null \
      && echo "远程备份已删除" || echo "远程清理失败或无法连接,请手动处理"
  fi
  echo "卸载完成。如需彻底删除脚本本身,请执行:rm $SCRIPT_PATH"
}

# ================= 菜单 =================
show_menu() {
  while true; do
    echo
    echo "==== 博客备份管理 ===="
    echo "1) 安装/初始化"
    echo "2) 定时备份设置"
    echo "3) 查看状态"
    echo "4) 立即备份一次"
    echo "5) 卸载"
    echo "0) 退出"
    choice=""; read -rp "请选择 [0-5]: " choice || { echo; break; }
    case "$choice" in
      1) do_install || true ;;
      2) do_schedule || true ;;
      3) do_status || true ;;
      4) ( do_backup ) || true ;;   # 子 shell 跑,失败返回菜单不退出
      5) do_uninstall || true ;;
      0) break ;;
      *) echo "无效选择" ;;
    esac
  done
}

case "${1:-}" in
  backup) do_backup ;;
  check-publish) do_check_publish ;;
  status) do_status ;;
  menu)   show_menu ;;
  "")
    if [ -t 0 ]; then show_menu; else do_backup; fi ;;  # cron 无参数调用也兼容
  *) echo "用法: $0 [menu|backup|status]"; exit 1 ;;
esac

恢复(灾难时用,尽量详细)

以下在新机器(root)上执行,假设备份包名是 blog-full-2026-10-08.tgz,实际用哪个日期就换哪个。

步骤 1:取回备份包并解压

从备份 VPS 把备份包取回来,解压后得到三个文件:blog-db.sql(数据库)、blog-files.tgz(网站文件)、blog-compose.tgz(compose 配置)。


# 从备份 VPS 取回(把 <SSH端口> <备份VPS地址> 换成实际值)
scp -P <SSH端口> root@<备份VPS地址>:/home/wp-back/blog-full-2026-10-08.tgz ~/
cd ~
# 解压总包
tar -xzf blog-full-2026-10-08.tgz
ls -lh blog-db.sql blog-files.tgz blog-compose.tgz

步骤 2:恢复 compose 配置并启动服务

先恢复 .env 和 docker-compose.yml,把数据库跑起来。注意:如果换了新环境,.env 里的 tunnel token 要按新隧道的改。


mkdir -p ~/my-wordpress
tar -xzf ~/blog-compose.tgz -C ~/my-wordpress
cd ~/my-wordpress
docker compose up -d

步骤 3:导入数据库

等数据库就绪后再导入(看到 db ready 再执行下一行)。


# 等待数据库就绪
docker compose exec -T db sh -c 'while ! mysqladmin -u root -p"$MYSQL_ROOT_PASSWORD" ping --silent 2>/dev/null; do sleep 2; done; echo "db ready"'
# 导入到 wordpress 库
cd ~
docker compose -f ~/my-wordpress/docker-compose.yml exec -T db sh -c 'mysql -u root -p"$MYSQL_ROOT_PASSWORD" wordpress' < ~/blog-db.sql

步骤 4:恢复网站文件到数据卷

把 blog-files.tgz 解压覆盖到 wordpress 的数据卷(/var/www/html,即 wp-content、主题、插件)。


# 查数据卷名(compose 会给卷名加项目前缀,直接查最稳)
VOL=$(docker volume ls --format '{{.Name}}' | grep -i wp_data | head -1)
echo "数据卷:$VOL"
# 解压覆盖进去(只读挂载数据卷,安全)
docker run --rm -v "$VOL:/data" -v "$HOME:/backup" alpine tar -xzf /backup/blog-files.tgz -C /data

步骤 5:重启并验证

重启 wordpress 让它加载恢复的数据,然后验证首页。


cd ~/my-wordpress && docker compose restart wordpress
sleep 10
curl -s -o /dev/null -w "%{http_code}\n" http://localhost
# 返回 200 即正常;如果是 302 跳转到域名也算正常