# 用三个 Bash 脚本收敛日常运维：状态总览、健康检查与依赖顺序重建

> 作者: Lazybones | 日期: 2026-09-07

> 一台服务器上跑十几个 Docker 容器加几个裸机服务时，日常运维不能靠记忆敲命令。本文讲如何用三个 Bash 脚本把「看状态、查健康、重建服务」收敛成三条命令，以及过程中容易踩到的 Bash 坑。

## 问题：命令散落、记忆依赖

容器化完成后，服务散在 `/srv/stacks/` 下十几个子目录里，每个都有自己的 `docker-compose.yml`。日常运维面临三个痛点：

- **状态查看**：要依次 `cd` 到各目录跑 `docker compose ps`，或者 `docker ps -a` 看满屏输出，分不清哪些是业务容器、哪些是基础设施。
- **健康检查**：端口是否在监听、域名是否可达、SSL 证书还剩几天、磁盘有没有快满——全靠人肉逐项查，容易漏。
- **服务重建**：改了配置或代码后要 `cd /srv/stacks/xxx && docker compose up -d --build`，十几个服务敲十几遍，还不能搞错依赖顺序。

于是可以把这些操作收敛成三个脚本，放在 `/srv/stacks/scripts/` 下：

```text
scripts/
├── service-manager.sh    # 统一服务管理（status/start/stop/restart/logs）
├── health-check.sh       # 一键健康检查（46 项）
├── quick-deploy.sh       # 快速部署/重建
├── backup.sh             # 自动备份
└── restore.sh            # 从备份恢复
```

## service-manager.sh：统一服务管理

核心思路是建立一个**服务注册表**——用 Bash 数组描述每个服务的 compose 目录、容器名、端口、所属层级：

```bash
# 格式: "服务名|compose目录|容器名(逗号分隔)|端口|层级"
# 层级: 0=基础设施  1=数据层  2=业务层
DOCKER_SERVICES=(
    "traefik|/srv/stacks/traefik|traefik|80,443|0"
    "authelia|/srv/stacks/authelia|authelia|443|0"
    "mariadb|/srv/stacks/app|app_mariadb|3306|1"
    "redis|/srv/stacks/app|app_redis|6379|1"
    "web|/srv/stacks/web|web|443|2"
    "worker|/srv/stacks/web|worker,worker-cron|443|2"
    # ... 共 14 个 Docker 服务 + 3 个裸机服务
)
```

### 按层级分组展示

`status` 命令按 **基础设施层 → 数据层 → 业务层** 三层分组展示，输出一张彩色表格：

```
╔══════════════════════════════════════════════════════════════════╗
║                    服务器服务状态总览                            ║
╚══════════════════════════════════════════════════════════════════╝

+----------------+----------+--------------+----------+----------------------+
| 服务           | 类型     | 状态         | 端口     | 运行时间             |
+----------------+----------+--------------+----------+----------------------+
|── 基础设施层 ────────────────────────────────────────────────────
| traefik        | Docker   | ● 运行中     | 80,443   | 2d 5h 30m            |
| authelia       | Docker   | ● 运行中     | 443      | 2d 5h 30m            |
|── 数据层 ────────────────────────────────────────────────────────
| mariadb        | Docker   | ● 运行中     | 3306     | 2d 5h 30m            |
| redis          | Docker   | ● 运行中     | 6379     | 2d 5h 30m            |
|── 业务层 ────────────────────────────────────────────────────────
| web            | Docker   | ● 运行中     | 443      | 2d 5h 30m            |
| worker         | Docker   | ● 运行中     | 443      | 2d 5h 30m            |
|   └worker      |          | ● 运行中     |          |                      |
|   └worker-cron |          | ● 运行中     |          |                      |
+----------------+----------+--------------+----------+----------------------+
```

多容器服务（如某个应用有 web / worker / cron 多个容器）会自动展开子行，一眼就能看出哪个子容器掉了。

### 依赖顺序启动/停止

启动和停止不是随机顺序，而是**按依赖拓扑**：

```bash
# 启动顺序：基础设施 → 数据层 → 业务层
START_ORDER=(traefik authelia mariadb redis worker web ...)

# 停止顺序：反向
STOP_ORDER=(web worker ... redis mariadb authelia traefik)
```

这样确保数据库先于应用启动、反向代理先于业务服务启动，避免启动时连接失败。

### 日志查看

```bash
./service-manager.sh logs traefik      # 最近 100 行
./service-manager.sh logs -f web       # 实时跟随
```

Docker 服务走 `docker compose logs`，裸机服务走 `journalctl`，接口统一。

## health-check.sh：46 项一键健康检查

这是三个脚本里最「值钱」的一个。完整模式跑 **46 项检查**，分 10 个检查组：

| 检查组 | 项目数 | 检查内容 |
|--------|--------|----------|
| Docker 容器状态 | 16 | 逐个检查所有容器是否 running |
| 裸机服务状态 | 3 | 例如几个尚未容器化的 systemd 服务 |
| 关键端口监听 | 6 | 22、80、443 与三个自定义高位端口 |
| 域名可达性 | 9 | 所有域名 HTTPS 返回码 |
| 磁盘使用率 | 2 | 根分区 + 各挂载点（阈值 85%） |
| 内存使用率 | 1 | 总体使用率（阈值 85%） |
| 数据库连接 | 2 | MariaDB ping + Redis PONG |
| 备份状态 | 2 | 24h 内配置备份 + 数据卷备份 |
| SSL 证书有效期 | 9 | 所有域名证书剩余天数（30天告警） |
| 快速模式 | 2 | `--quick` 仅查容器/服务状态 |

### 检查结果格式

每项检查结果用 emoji 标记状态，底部输出汇总：

```
  ── Docker 容器状态 ──
  ✅ traefik                          运行中
  ✅ authelia                         运行中
  ✅ web                              运行中

  ── 磁盘使用率 ──
  ✅ 磁盘使用率                       59%
  ✅ 分区 /home                       12%

  ── SSL 证书有效期 ──
  ✅ www.example.com                  剩余 72 天
  ⚠️ proxy.example.com               剩余 18 天（30 天内告警）

  ════════════════════════════════════════
  检查汇总
  ════════════════════════════════════════
  总计: 46 项
  通过: 43
  告警: 2
  失败: 1
```

### 快速模式

`--quick` 模式只检查容器和服务状态，跳过端口探测、域名 curl、SSL 证书检查等网络操作，适合频繁巡检或网络不通时使用。

## quick-deploy.sh：按依赖顺序重建

解决「改了配置要重建十几个服务」的问题。支持三种用法：

```bash
./quick-deploy.sh web              # 重建指定服务
./quick-deploy.sh all              # 按依赖顺序重建全部
./quick-deploy.sh --pull all       # 拉取最新镜像 + 重建全部
```

`--pull` 适合版本升级场景：先 `docker compose pull` 拉新镜像，再 `up -d --build` 重建。

## 踩坑一：Bash `((var++))` 在 var=0 时中断脚本

这是写这些脚本时遇到的最隐蔽的坑。

**现象**：`health-check.sh` 跑到某处就莫名退出，退出码非零，但没有报错信息。

**根因**：脚本启用了 `set -euo pipefail`（严格模式），里面有计数器递增：

```bash
PASS_CHECKS=0
# ... 某个检查通过后
((PASS_CHECKS++))   # ← 这里出问题了
```

`((expr))` 算术扩展的退出码取决于**求值结果**——结果为 0 返回退出码 1。`((PASS_CHECKS++))` 是后置递增：先返回原值（此时为 0），再自增。原值 0 → 退出码 1 → `set -e` 判定为错误 → 立即退出脚本。

**修复**：改用赋值形式，赋值操作恒成功：

```bash
# ❌ 危险：var=0 时 ((var++)) 退出码为 1
((PASS_CHECKS++))

# ✅ 安全：赋值恒成功
PASS_CHECKS=$((PASS_CHECKS + 1))

# ✅ 也可以用前置递增：((++var)) 先自增再返回，var=0→1，退出码 0
((++PASS_CHECKS))
```

**可复用经验**：`set -e` 脚本里绝不用 `((var++))` 做计数器递增。这是 Bash 的一个经典陷阱——算术扩展的退出码语义与直觉相反。

## 踩坑二：时长格式化函数的字符串测试

**现象**：`fmt_duration` 函数在某些容器名不存在时输出乱码。

**根因**：函数内部用 `-z` 测试字符串是否为空，但变量未初始化时在算术上下文中展开会报语法错误：

```bash
# 原始写法有问题
fmt_duration() {
    local sec="$1"
    if [[ -z "$sec" ]]; then ...  # ← 当 $1 为空时，后续算术运算会炸
}
```

**修复**：加默认值 + 正则校验，确保输入是合法数字：

```bash
fmt_duration() {
    local sec="${1:-0}"
    if ! [[ "$sec" =~ ^[0-9]+$ ]] || [[ "$sec" -le 0 ]]; then
        echo "< 1m"; return
    fi
    # ... 正常计算
}
```

## 踩坑三：systemd 时间戳解析兼容性

获取裸机服务的运行时长时，`systemctl show -p ActiveEnterTimestamp --value` 返回的时间戳格式是 `Mon 2026-09-07 16:54:10 CST`。这个格式在不同 locale 下可能变化，`date -d` 解析偶尔失败。

**修复**：始终加 `2>/dev/null || echo 0` 降级，解析失败就返回 0（显示 "< 1m"），不让一个服务的时间解析失败拖垮整个脚本。

## 别名与软链接

为了让日常使用更顺手，可以在 `~/.bashrc` 里加别名：

```bash
alias svc='/srv/stacks/scripts/service-manager.sh'
alias health='/srv/stacks/scripts/health-check.sh'
alias deploy='/srv/stacks/scripts/quick-deploy.sh'
```

日常操作变成：

```bash
svc status          # 看状态
health              # 跑健康检查
deploy --pull web   # 拉新镜像重建 Web 服务
```

## 小结

三个脚本的设计原则：

1. **注册表驱动**：服务清单集中在数组里，增删服务只改注册表，不改逻辑。
2. **层级感知**：启动/停止/展示都按依赖层级排序，避免启动顺序错误。
3. **严格模式 + 优雅降级**：`set -euo pipefail` 保证脚本不静默失败，但单个服务的检查失败用 `|| warn` 降级，不拖垮整体。
4. **输出面向人**：彩色、表格、emoji，一眼看出哪里有问题，不需要 grep 日志。

这三个脚本配合多层安全加固，基本就能覆盖日常运维的主要场景。
