2026.10.01 · Linux 与系统安全 · 约 7 分钟 · 1607 字
更新于 2026-10-03
Linux 容器无人值守运维:故障自愈、Logrotate 日志轮转与原子发布脚本 配置 Docker 容器开机自启与健康检查,使用宿主机 logrotate 配合 USR1 信号按日切分 Nginx 日志,并编写支持秒级回滚的 Bash 发布脚本。
把容器跑起来通常只需要几分钟,但要让站点长期稳定运行,还需要处理好三件事:宿主机重启后容器自动恢复、访问日志定期轮转压缩避免占满磁盘,以及发版脚本遇到错误时自动中止并回滚。
本文记录 Docker 重启策略配置、logrotate 日志切分,以及基于 ln -sfn 软链接切换的发布脚本。
容器自愈:restart: unless-stopped 与 docker.service 开机自启
在 compose.yaml 中配置 restart: unless-stopped 后,要让容器在云主机重启后自动恢复,还需要确认两点:
宿主机 docker.service 守护进程已设为开机自启 (而不是仅靠 docker.socket 等待客户端命令触发):
1 2 3 # 启用 docker.service 开机自启 sudo systemctl enable --now docker.service sudo systemctl is-enabled docker.service
Docker 四种重启策略的区别 :
策略值
容器内进程异常退出
宿主机重启后
手动 docker stop 后宿主机重启
适用场景
no
不重启
不拉起
不拉起
一次性构建或临时调试容器
on-failure[:max-retries]
仅退出码非 0 时重启
视退出状态而定
不拉起
批处理任务或队列 Worker
always
自动重启
总是拉起
仍会拉起
常驻基础代理服务
unless-stopped
自动重启
自动拉起
保持人工停机状态,不拉起
常规生产 Web 容器
通过 restart: unless-stopped 保障容器异常退出后自动拉起,通过宿主机 logrotate 发送 USR1 信号切分日志,通过软链接切换完成发布与回退。
宿主机日志轮转:切分文件后向 Nginx 发送 USR1 信号
由于容器内的 /var/log/nginx 已经挂载到宿主机的 /srv/blog/logs,可以直接使用宿主机自带的 logrotate 按日切分并压缩日志,无需在 nginx:alpine 容器内额外运行 cron。
需要注意的是,Linux 进程是通过已打开的文件描述符(File Descriptor)向底层 Inode 写入数据的,而不是每次写日志时重新按路径查找文件。如果 logrotate 只是把 access.log 重命名为 access.log.1 并新建一个空的 access.log,容器内的 Nginx Worker 进程仍然持有旧文件的 Inode,新产生的日志还会继续写进 access.log.1。
因此,在轮转配置的 postrotate 阶段,需要执行 docker kill --signal="USR1" blog-web,通知容器内的 Nginx Master 进程重新打开日志文件描述符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 创建宿主机日志轮转配置:/etc/logrotate.d/blog-nginx sudo tee /etc/logrotate.d/blog-nginx >/dev/null <<'EOF' /srv/blog/logs/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 root adm sharedscripts postrotate if docker ps --format '{{.Names}}' | grep -qx 'blog-web'; then docker kill --signal="USR1" blog-web >/dev/null 2>&1 || true fi endscript } EOF # 使用 -d (dry-run) 检查语法,再用 -f 手动执行一次测试轮转 sudo logrotate -d /etc/logrotate.d/blog-nginx sudo logrotate -f /etc/logrotate.d/blog-nginx ls -lh /srv/blog/logs/
编写支持自动回滚的 Bash 发布脚本
如果发版脚本缺少错误处理,遇到磁盘写满或同步中断时可能会把残缺目录直接暴露到线上。
发布脚本的关键保护措施:独立子进程执行、set -euo pipefail 错误中断、圆括号子 Shell 隔离目录切换,以及健康检查失败后自动回滚。
在 /srv/blog/bin/deploy-release.sh 中编写发布与回滚脚本,主要包含四步检查:
set -euo pipefail :命令返回非 0(-e)、引用未定义变量(-u)或管道中间环节失败(pipefail)时立即终止脚本。
发布前检查关键文件 :确认新版本目录中包含非空的 index.html、404.html、atom.xml 与 search.json 后,才执行软链接切换。
ln -sfn 切换与本机 HTTP 探活 :软链接切换后重启或重载容器,并用 curl 校验首页返回 200、不存在路径返回 404;若任一检查未通过,自动切回上一版本(PREV_TARGET)。
子 Shell 清理旧版本 :在圆括号 ( ... ) 子 Shell 中只保留最近 5 个历史版本目录,避免旧产物长期堆积占满磁盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #!/usr/bin/env bash # 保存为 /srv/blog/bin/deploy-release.sh && chmod +x /srv/blog/bin/deploy-release.sh set -euo pipefail SITE_ROOT="/srv/blog" RELEASES_DIR="${SITE_ROOT}/releases" CURRENT_LINK="${RELEASES_DIR}/current" CONTAINER_NAME="blog-web" NEW_VERSION="${1:-}" if [[ -z "${NEW_VERSION}" ]]; then echo "用法: $0 <新版本号目录名>" >&2 exit 1 fi TARGET_DIR="${RELEASES_DIR}/${NEW_VERSION}" # 1. 检查新版本核心静态产物是否齐全且非空 for required in "index.html" "404.html" "atom.xml" "search.json"; do if [[ ! -s "${TARGET_DIR}/${required}" ]]; then echo "[ERROR] 缺失或空文件: ${TARGET_DIR}/${required},已中止发布" >&2 exit 1 fi done # 2. 记录当前生效版本路径,以便探活失败时回退 PREV_TARGET="" if [[ -L "${CURRENT_LINK}" ]]; then PREV_TARGET="$(readlink -f "${CURRENT_LINK}" || true)" fi # 3. 切换 current 软链接并重启容器以刷新只读挂载目录 ln -sfn "${TARGET_DIR}" "${CURRENT_LINK}" docker restart "${CONTAINER_NAME}" >/dev/null # 4. 本机 HTTP 健康检查与 404 状态码验证 sleep 1 HTTP_HOME="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)" HTTP_404="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/__health_404_check__)" if [[ "${HTTP_HOME}" != "200" || "${HTTP_404}" != "404" ]]; then echo "[ERROR] 健康检查未通过 (home=${HTTP_HOME}, 404=${HTTP_404}),回退至上一版本" >&2 if [[ -n "${PREV_TARGET}" && -d "${PREV_TARGET}" ]]; then ln -sfn "${PREV_TARGET}" "${CURRENT_LINK}" docker restart "${CONTAINER_NAME}" >/dev/null fi exit 1 fi # 5. 在子 Shell 中清理超出 5 个的旧版本目录 ( cd "${RELEASES_DIR}" ls -1dt 20* 2>/dev/null | tail -n +6 | xargs -r rm -rf -- ) echo "[OK] 版本 ${NEW_VERSION} 已上线并通过健康检查。"
容器恢复与发布脚本自检表
测试项
模拟命令
预期结果
容器进程异常退出恢复
docker kill --signal="SIGKILL" blog-web
几秒内 docker ps 显示容器已由守护进程自动拉起(Up)
日志轮转句柄切换
sudo logrotate -f /etc/logrotate.d/blog-nginx
生成 .log.1 归档文件,新访问请求写入新的 .log
残缺版本拦截
传入一个缺失 index.html 的目录执行 deploy-release.sh
脚本在预检阶段报错退出(Exit 1),线上 current 软链接保持不变