一台服务器上跑十几个 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/ 下:
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 目录、容器名、端口、所属层级:
# 格式: "服务名|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 多个容器)会自动展开子行,一眼就能看出哪个子容器掉了。
依赖顺序启动/停止
启动和停止不是随机顺序,而是按依赖拓扑:
# 启动顺序:基础设施 → 数据层 → 业务层
START_ORDER=(traefik authelia mariadb redis worker web ...)
# 停止顺序:反向
STOP_ORDER=(web worker ... redis mariadb authelia traefik)
这样确保数据库先于应用启动、反向代理先于业务服务启动,避免启动时连接失败。
日志查看
./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:按依赖顺序重建
解决「改了配置要重建十几个服务」的问题。支持三种用法:
./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(严格模式),里面有计数器递增:
PASS_CHECKS=0
# ... 某个检查通过后
((PASS_CHECKS++)) # ← 这里出问题了
((expr)) 算术扩展的退出码取决于求值结果——结果为 0 返回退出码 1。((PASS_CHECKS++)) 是后置递增:先返回原值(此时为 0),再自增。原值 0 → 退出码 1 → set -e 判定为错误 → 立即退出脚本。
修复:改用赋值形式,赋值操作恒成功:
# ❌ 危险: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 测试字符串是否为空,但变量未初始化时在算术上下文中展开会报语法错误:
# 原始写法有问题
fmt_duration() {
local sec="$1"
if [[ -z "$sec" ]]; then ... # ← 当 $1 为空时,后续算术运算会炸
}
修复:加默认值 + 正则校验,确保输入是合法数字:
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 里加别名:
alias svc='/srv/stacks/scripts/service-manager.sh'
alias health='/srv/stacks/scripts/health-check.sh'
alias deploy='/srv/stacks/scripts/quick-deploy.sh'
日常操作变成:
svc status # 看状态
health # 跑健康检查
deploy --pull web # 拉新镜像重建 Web 服务
小结
三个脚本的设计原则:
- 注册表驱动:服务清单集中在数组里,增删服务只改注册表,不改逻辑。
- 层级感知:启动/停止/展示都按依赖层级排序,避免启动顺序错误。
- 严格模式 + 优雅降级:
set -euo pipefail保证脚本不静默失败,但单个服务的检查失败用|| warn降级,不拖垮整体。 - 输出面向人:彩色、表格、emoji,一眼看出哪里有问题,不需要 grep 日志。
这三个脚本配合多层安全加固,基本就能覆盖日常运维的主要场景。