摘要(TL;DR)

大家好呀!我在飞牛NAS上遇到了一个头疼的问题,每次重启机器就会导致Docker的容器启动异常,这个问题是一定的

于是我自己写了个Bash脚本lsd来解决这个问题。这个脚本可以自动监控容器状态,发现停止就重启,还能记录事件、提供交互界面,甚至支持开机自启。不过在开发过程中我也踩了不少坑,比如脚本在非Bash环境报错、误把“all”当容器名、启动时网络不稳定导致误操作,还有担心长期运行内存占用过高。现在问题终于解决啦,我把经验整理成这篇文章,希望能帮到有同样困扰的小伙伴。

说明:

脚本通过 MONITORED 配置管理要监控的容器名或 “all”,以守护进程循环定期检测容器状态并在停止时尝试重启,提供交互菜单与 systemd 自启支持以便运维管理

操作

以下示例假设你把脚本保存为/usr/local/bin/lsd

  1. 写入脚本并赋予权限
sudo tee /usr/local/bin/lsd > /dev/null <<'EOF'
#!/bin/bash
# lsd - Docker 容器监控守护脚本 (优化版)
# 功能:监控指定容器,停止时自动重启;支持交互管理(添加/删除容器、启停守护进程、开机自启等)

CONFIG_DIR="/etc/lsd"
CONFIG_FILE="CONFIG_DIR/monitored.conf"
LOG_FILE="/var/log/lsd-monitor.log"
PID_FILE="/var/run/lsd-monitor.pid"
SERVICE_FILE="/etc/systemd/system/lsd-monitor.service"
INTERVAL=15      # 检测间隔(秒)
STARTUP_DELAY=30 # Docker启动后延迟时间(秒)

# 初始化配置目录
mkdir -p "CONFIG_DIR"

# 彩色输出函数
green() { echo -e "\033[32m1\033[0m"; }
red()   { echo -e "\033[31m1\033[0m"; }
bold()  { echo -e "\033[1m1\033[0m"; }

# 日志函数(同时输出到终端和日志文件)
log() {
    local msg="[(date '+%F %T')] 1"
    echo "msg" | tee -a "LOG_FILE"
}

# 加载监控配置(从文件读取监控的容器列表)
load_config() {
    if [[ -f "CONFIG_FILE" ]]; then
        # 去除多余空格和换行,确保格式为"container1 container2"或"all"
        MONITORED=(cat "CONFIG_FILE" | tr -d '\n' | tr -s ' ' | sed 's/^ *//;s/ *//')
    else
        MONITORED=""
    fi
}

# 保存监控配置(写入文件)
save_config() {
    echo "MONITORED" > "CONFIG_FILE"
}

# 获取所有容器(运行/停止的)的名称
get_all_containers() {
    docker ps -a --format "{{.Names}}" 2>/dev/null || {
        red "错误:无法连接Docker服务,请检查Docker是否运行。"
        return 1
    }
}

# 守护进程主循环(监控容器状态)
daemon_loop() {
    local start_time=SECONDS
    log "[daemon] 初始化:等待Docker服务启动..."

    # 等待Docker服务启动(避免Docker未启动导致监控失败)
    until docker info >/dev/null 2>&1; do
        local wait_time=((SECONDS - start_time))
        log "[daemon] 等待Docker服务启动...(已等待{wait_time} 秒)"
        sleep 1
    done

    # 延迟等待Docker稳定(避免容器启动时网络未就绪)
    log "[daemon] Docker已启动,延迟 {STARTUP_DELAY} 秒等待稳定..."
    sleep "STARTUP_DELAY"

    # 开始监控
    log "[daemon] 监控开始:间隔={INTERVAL}秒,监控对象={MONITORED}"

    while true; do
        local containers
        if [[ "MONITORED" == "all" ]]; then
            containers=(get_all_containers)
        else
            containers="MONITORED"
        fi

        # 遍历所有监控的容器
        for c incontainers; do
            # 跳过空字符串(避免因空格导致的无效循环)
            [[ -z "c" ]] && continue

            # 1. 检查容器是否存在(防止容器被删除后仍监控)
            if [[ "MONITORED" != "all" ]] && ! docker inspect "c" >/dev/null 2>&1; then
                log "容器c 不存在,从监控列表中移除..."
                MONITORED=(echo "MONITORED" | sed "s/\bc\b//g" | tr -s ' ' | sed 's/^ *//;s/ *//')
                save_config
                continue
            fi

            # 2. 检查容器状态(是否在运行)
            local status=(docker ps --filter "name=^{c}" --format "{{.Status}}")
            if [[ -z "status" ]]; then
                log "容器 c 已停止,尝试重启..."
                if docker start "c" >/dev/null 2>&1; then
                    log "容器 c 重启成功"
                else
                    log "容器c 重启失败(请检查容器日志:docker logs c)"
                fi
            fi
        done

        sleep "INTERVAL"
    done
}

# 启动守护进程(后台运行)
start_daemon() {
    # 检查守护进程是否已运行
    if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
        red "错误:守护进程已在运行(PID:(cat "PID_FILE"))"
        return 1
    fi

    # 启动守护进程(后台运行)
    (daemon_loop&echo! > "PID_FILE") &

    # 等待PID文件创建(确保启动成功)
    sleep 1
    if [[ -f "PID_FILE" ]] && kill -0 (cat "PID_FILE") 2>/dev/null; then
        green "成功:守护进程已启动(PID: (cat "PID_FILE"))"
    else
        red "错误:守护进程启动失败,请检查日志(LOG_FILE)"
        rm -f "PID_FILE" # 清理无效PID文件
    fi
}

# 停止守护进程
stop_daemon() {
    if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
        kill(cat "PID_FILE")
        rm -f "PID_FILE"
        green "成功:守护进程已停止"
    else
        red "错误:守护进程未运行"
    fi
}

# 查看守护进程状态
status_daemon() {
    if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
        green "守护进程运行中(PID:(cat "PID_FILE"))"
    else
        red "守护进程未运行"
    fi
}

# 启用开机自启动(systemd)
enable_autostart() {
    # 生成systemd服务文件
    cat>"SERVICE_FILE" <<EOF
[Unit]
Description=lsd Docker 容器监控守护进程
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
ExecStartPre=/bin/sleep {STARTUP_DELAY}
ExecStart=/usr/local/bin/lsd monitor
Restart=always
User=root
Group=root

[Install]
WantedBy=multi-user.target
EOF

    # 重新加载systemd配置并启用服务
    systemctl daemon-reload
    systemctl enable --now lsd-monitor.service

    if systemctl is-active --quiet lsd-monitor.service; then
        green "成功:开机自启动已启用"
    else
        red "错误:开机自启动启用失败,请检查服务配置(SERVICE_FILE)"
    fi
}

# 取消开机自启动
disable_autostart() {
    systemctl disable --now lsd-monitor.service
    rm -f "SERVICE_FILE"
    systemctl daemon-reload
    green "成功:开机自启动已取消"
}

# 交互菜单(核心交互逻辑)
menu() {
    while true; do
        load_config
        clear

        # 菜单头部(显示当前状态)
        bold "== lsd Docker 容器监控守护进程 =="
        echo "守护进程状态:(status_daemon | awk '{print NF}')" # 提取状态(运行中/未运行)
        echo -n "当前监控对象:"
        if [[ -z "MONITORED" ]]; then
            echo "(red '(空)')"
        elif [[ "MONITORED" == "all" ]]; then
            echo "(green '所有容器')"
        else
            echo "(echo "MONITORED" | tr ' ' ', ')" # 用逗号分隔,更清晰
        fi
        echo "----------------------------------------"

        # 菜单选项
        echo "操作选项:"
        echo "  1) 添加监控容器(支持批量添加)"
        echo "  2) 删除监控容器(支持批量删除)"
        echo "  3) 启动守护进程(后台运行)"
        echo "  4) 停止守护进程"
        echo "  5) 前台运行监控(调试用,Ctrl+C停止)"
        echo "  6) 查看守护进程状态"
        echo "  7) 启用开机自启动(systemd)"
        echo "  8) 取消开机自启动"
        echo "  9) 手动重启指定容器"
        echo "  0) 退出菜单"
        echo -n "请选择操作(0-9):"
        read -r choice

        # 处理用户选择
        case "choice" in
            1) # 添加监控容器
                echo "----------------------------------------"
                echo "可用容器列表(运行/停止的):"
                get_all_containers | while read -r c; do
                    echo "  c"
                done
                echo "提示:输入容器名(多个用空格分隔),输入\"all\"监控所有容器,输入\"q\"取消。"
                echo -n "请输入:"
                read -r names

                [[ "names" == "q" ]] && continue

                # 处理输入(分割为多个容器名)
                for name in names; do
                    # 验证容器是否存在(除了"all")
                    if [[ "name" != "all" ]] && ! docker inspect "name" >/dev/null 2>&1; then
                        red "容器name 不存在,跳过添加。"
                        continue
                    fi

                    # 避免重复添加
                    if [[ "name" == "all" ]]; then
                        MONITORED="all"
                        break # 选择"all"后忽略其他输入
                    elif echo "MONITORED" | grep -q "\bname\b"; then
                        red "容器name 已在监控列表中,跳过添加。"
                        continue
                    fi

                    # 添加到监控列表
                    MONITORED="MONITOREDname"
                done

                # 保存配置(去除多余空格)
                MONITORED=(echo "MONITORED" | tr -s ' ' | sed 's/^ *//;s/ *//')
                save_config
                green "成功:监控列表已更新(当前监控:MONITORED)"
                read -p "按回车键继续..."
                ;;

            2) # 删除监控容器
                echo "----------------------------------------"
                if [[ "MONITORED" == "all" ]]; then
                    red "当前监控所有容器,无法删除单个容器(请先切换到监控特定容器)。"
                    read -p "按回车键继续..."
                    continue
                elif [[ -z "MONITORED" ]]; then
                    red "当前没有监控任何容器,无法删除。"
                    read -p "按回车键继续..."
                    continue
                fi

                echo "当前监控的容器:"
                echo "MONITORED" | tr ' ' '\n' | while read -r c; do
                    echo "c"
                done
                echo "提示:输入容器名(多个用空格分隔),输入\"q\"取消。"
                echo -n "请输入:"
                read -r names

                [[ "names" == "q" ]] && continue

                # 处理输入(分割为多个容器名)
                for name innames; do
                    # 验证容器是否在监控列表中
                    if ! echo "MONITORED" | grep -q "\bname\b"; then
                        red "容器 name 不在监控列表中,跳过删除。"
                        continue
                    fi

                    # 从监控列表中删除
                    MONITORED=(echo "MONITORED" | sed "s/\bname\b//g" | tr -s ' ' | sed 's/^ *//;s/ *//')
                done

                # 保存配置
                save_config
                green "成功:监控列表已更新(当前监控:MONITORED)"
                read -p "按回车键继续..."
                ;;

            3) # 启动守护进程
                echo "----------------------------------------"
                start_daemon
                read -p "按回车键继续..."
                ;;

            4) # 停止守护进程
                echo "----------------------------------------"
                stop_daemon
                read -p "按回车键继续..."
                ;;

            5) # 前台运行监控(调试用)
                echo "----------------------------------------"
                echo "提示:前台运行监控,按Ctrl+C停止。"
                read -p "按回车键开始..."
                daemon_loop
                ;;

            6) # 查看守护进程状态
                echo "----------------------------------------"
                status_daemon
                read -p "按回车键继续..."
                ;;

            7) # 启用开机自启动
                echo "----------------------------------------"
                enable_autostart
                read -p "按回车键继续..."
                ;;

            8) # 取消开机自启动
                echo "----------------------------------------"
                disable_autostart
                read -p "按回车键继续..."
                ;;

            9) # 手动重启容器
                echo "----------------------------------------"
                echo "可用容器列表:"
                get_all_containers | while read -r c; do
                    echo "  c"
                done
                echo "提示:输入容器名(多个用空格分隔),输入\"q\"取消。"
                echo -n "请输入:"
                read -r names

                [[ "names" == "q" ]] && continue

                # 处理输入(分割为多个容器名)
                for name in names; do
                    # 验证容器是否存在
                    if ! docker inspect "name" >/dev/null 2>&1; then
                        red "容器 name 不存在,跳过重启。"
                        continue
                    fi

                    # 重启容器
                    if docker restart "name" >/dev/null 2>&1; then
                        green "容器 name 重启成功"
                    else
                        red "容器name 重启失败(请检查日志:docker logs name)"
                    fi
                done
                read -p "按回车键继续..."
                ;;

            10) # 退出菜单
                echo "----------------------------------------"
                green "退出菜单。"
                exit 0
                ;;

            *) # 无效选择
                red "无效选择,请输入0-9之间的数字。"
                read -p "按回车键继续..."
                ;;
        esac
    done
}

# 命令行参数处理(支持非交互模式)
case "1" in
    monitor) # 启动守护进程(非交互模式)
        load_config
        daemon_loop
        ;;
    start) # 启动守护进程(非交互模式)
        start_daemon
        ;;
    stop) # 停止守护进程(非交互模式)
        stop_daemon
        ;;
    status) # 查看守护进程状态(非交互模式)
        status_daemon
        ;;
    enable-autostart) # 启用开机自启动(非交互模式)
        enable_autostart
        ;;
    disable-autostart) # 取消开机自启动(非交互模式)
        disable_autostart
        ;;
    *) # 默认进入交互菜单
        menu
        ;;
esac

EOF
sudo chmod +x /usr/local/bin/lsd
  1. 第一次配置(添加要监控的容器或输入all)
sudo /usr/local/bin/lsd
# 在交互菜单里选择“添加监控容器”,输入容器名或all,然后保存
  1. 立即后台运行(临时测试)
sudo /usr/local/bin/lsd start   # 脚本会把PID写到/var/run/lsd-monitor.pid
  1. 建议用systemd开机自启
sudo /usr/local/bin/lsd enable-autostart
# 或手动操作:
sudo tee /etc/systemd/system/lsd-monitor.service > /dev/null <<'UNIT'
[Unit]
Description=lsd Docker container watchdog
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/bin/lsd monitor
Restart=always
RestartSec=5
User=root
StandardOutput=syslog
StandardError=syslog

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now lsd-monitor.service
  1. 查看日志
sudo tail -f /var/log/lsd-monitor.log
# 若启用了systemd,也可以用:
sudo journalctl -u lsd-monitor.service -f
  1. 添加日志轮转(避免日志过大)
sudo tee /etc/logrotate.d/lsd-monitor > /dev/null <<'ROTATE'
/var/log/lsd-monitor.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    create 0640 root adm
}
ROTATE

最终感想

在飞牛NAS这种定制系统上,开机流程和服务依赖有很多细节需要注意。直接在启动早期运行自动重启脚本很容易出问题。通过强制使用Bash、正确处理“all”配置、等待网络和Docker稳定,以及合理配置systemd,这个问题基本解决了。Bash脚本对于几十个容器来说足够用了,但如果需要更复杂的功能或者监控更多容器,建议用更专业的语言开发。希望这篇文章能帮到你,有问题欢迎留言讨论!