1. 从一次“磁盘已满”的警报说起

那天早上,我像往常一样打开群晖DSM的后台,一个醒目的红色警告弹了出来:“存储空间已满”。这让我心里咯噔一下,我的NAS里存着不少重要资料,空间一直管理得还算可以,怎么会突然满了?点开存储管理器一看,罪魁祸首是Docker应用所在的存储卷,使用率已经飙到了98%。我立刻想到了Docker,这个在群晖上帮我跑着Home Assistant、Jellyfin、Bitwarden等一堆服务的好帮手,同时也是个潜在的“磁盘空间吞噬者”。

问题的根源,十有八九是那些不断增长的容器日志文件。Docker默认的日志驱动(通常是 json-file )会把容器内应用输出的所有标准输出(stdout)和标准错误(stderr)信息,以JSON格式记录在宿主机上。对于长期运行、且日志输出频繁的容器(比如网络爬虫、媒体服务器、数据库),这些日志文件会像滚雪球一样越积越大,悄无声息地占满你的硬盘。这不仅仅是群晖用户会遇到的问题,任何使用Docker的环境都可能面临这个“甜蜜的负担”。今天,我就结合在群晖DSM上的实战经验,把Docker日志清理这件事掰开揉碎了讲清楚,从查看、清理到预防,给你一套完整的“瘦身”方案。

2. 定位元凶:如何查看Docker容器的日志占用

在动手清理之前,我们得先搞清楚到底是哪个“家伙”吃掉了最多的空间。盲目删除可能会误删重要日志,甚至影响容器运行。在群晖上,我们有几种互补的方法来定位问题。

2.1 通过群晖DSM图形界面快速概览

对于不习惯命令行的用户,DSM的Docker套件提供了最直观的入口。打开“套件中心”安装的“Docker”应用,在“容器”页面,你可以看到所有运行中和已停止的容器。虽然这里不会直接显示日志大小,但它是一个很好的起点,让你知道有哪些容器在运行。通常,运行时间最长、且业务逻辑复杂的容器(如数据库、Nextcloud)产生日志的可能性更大。

更进一步的检查,可以借助File Station。Docker的默认数据目录通常在 /volume1/docker (取决于你安装Docker时选择的存储卷)。在这个目录下,找到 containers 文件夹,里面会有一串由容器ID命名的子文件夹。每个容器的日志文件通常位于 容器ID-json.log 。你可以直接在File Station中右键查看这些文件的属性,但这种方法比较繁琐,且对于大量容器不友好。

2.2 使用SSH终端进行精确诊断

这才是真正的高手操作方式。通过SSH连接到你的群晖NAS(需在DSM的“控制面板”->“终端机和SNMP”中启用SSH服务),使用 admin 或你有权限的账户登录。

首先,我们进入Docker的数据目录。路径可能因安装而异,最常见的是:

cd /volume1/@docker/containers

或者

cd /var/lib/docker/containers

在群晖上, /volume1/@docker 是更常见的路径。进入后,你会看到很多64位容器ID命名的文件夹。

接下来,使用一个强大的组合命令,一次性找出所有容器日志文件的大小,并按从大到小排序:

du -sh * | grep -E '[0-9]+[MG]' | sort -hr

这个命令会列出每个文件夹的总大小。但日志文件通常就在这些文件夹内,名为 容器ID-json.log 。我们可以用更精确的命令来查找所有日志文件并计算大小:

find /volume1/@docker/containers -name "*-json.log" -type f -exec du -ch {} + | grep total

这个命令会计算所有 -json.log 文件的总大小。如果想看每个日志文件的具体大小:

find /volume1/@docker/containers -name "*-json.log" -type f -exec ls -lh {} \;

然而,最直观的方法是将容器名称与其日志大小关联起来。因为容器ID难以记忆,我们需要一点技巧。下面这个命令脚本非常实用:

#!/bin/bash
for container_id in $(docker ps -aq); do
  container_name=$(docker inspect --format='{{.Name}}' $container_id | sed 's/^\///')
  log_path=$(docker inspect --format='{{.LogPath}}' $container_id 2>/dev/null)
  if [ -f "$log_path" ]; then
    log_size=$(du -h "$log_path" | cut -f1)
    echo "容器名: $container_name, 日志文件: $log_path, 大小: $log_size"
  else
    echo "容器名: $container_name, 未找到标准JSON日志文件(可能配置了其他日志驱动)。"
  fi
done

你可以将上述内容保存为一个脚本文件(如 check_log_size.sh ),赋予执行权限( chmod +x check_log_size.sh )后运行。它会列出每个容器的名称、日志路径和当前大小,一目了然。

注意: docker inspect 命令输出的 LogPath 字段仅在日志驱动为 json-file journald 等本地文件驱动时有效。如果容器配置了其他驱动(如 syslog , none ),这里可能显示为空或非文件路径。

3. 清理行动:安全删除日志的多种方法

找到占用空间大的日志文件后,就可以开始清理了。根据你的需求和对服务的干扰程度,可以选择不同的方法。

3.1 方法一:手动清理单个日志文件(最直接)

如果你已经通过上面的方法定位到了某个巨大的日志文件(比如 /volume1/@docker/containers/abcd1234.../abcd1234...-json.log ),并且确认其历史日志已无保留价值,可以直接删除。

重要警告:直接删除正在被Docker守护进程写入的日志文件,可能会导致程序报错或行为异常。更安全的方法是清空文件内容,而不是删除文件本身。

安全清空日志文件命令:

# 将日志文件大小截断为0字节,但保留文件句柄
truncate -s 0 /path/to/your/container-id-json.log

或者使用 cat 命令:

cat /dev/null > /path/to/your/container-id-json.log

为什么是 truncate cat /dev/null ,而不是 rm 因为 rm 会删除文件本身,而Docker守护进程可能还持有这个文件的写入句柄。直接删除会导致Docker向一个不存在的文件描述符写日志,可能引发“No such file or directory”错误,直到容器重启或Docker守护进程重新打开日志文件。而 truncate 或清空操作是在原文件句柄上将其内容置零,不会影响Docker的写入操作,更为安全。

3.2 方法二:使用Docker内置命令清理(针对已停止容器)

对于已经停止运行的容器,其日志文件不会再增长,但依然占用磁盘。Docker提供了 docker logs 命令来查看日志,但没有直接的 docker clean logs 命令。不过,我们可以通过组合命令来管理。

首先,列出所有已停止的容器:

docker ps -a -f status=exited

如果你想清理所有已停止容器的日志(请谨慎,确认日志无用),可以写一个循环:

for container_id in $(docker ps -aq -f status=exited); do
  log_path=$(docker inspect --format='{{.LogPath}}' $container_id 2>/dev/null)
  if [ -f "$log_path" ]; then
    echo "清空 $container_id 的日志..."
    truncate -s 0 "$log_path"
  fi
done

3.3 方法三:一键清理所有容器的日志(批量操作)

如果你确定所有容器的历史日志都可以清理,可以使用一个更激进但高效的一键脚本。这个脚本会清空所有正在运行和已停止容器的JSON日志文件。

#!/bin/bash
echo "正在查找所有Docker容器的JSON日志文件..."
find /var/lib/docker/containers -name "*-json.log" -type f | while read log_file; do
  if [ -f "$log_file" ]; then
    size_before=$(du -h "$log_file" | cut -f1)
    truncate -s 0 "$log_file"
    size_after=$(du -h "$log_file" | cut -f1)
    echo "已清空: $log_file (原大小: $size_before, 现大小: $size_after)"
  fi
done
echo "所有Docker容器日志已清空。"

再次强调 :执行此操作前,请务必确认没有需要保留的调试或审计日志。对于生产环境的关键服务,建议先对重要容器的日志进行备份。

3.4 方法四:配置日志轮转(治本之策)

手动清理是“治标”,配置日志轮转(Log Rotation)才是“治本”。Docker的 json-file 日志驱动本身支持日志轮转参数,可以在创建或运行容器时通过 --log-opt 指定。

关键的日志轮转选项有:

  • max-size :单个日志文件的最大大小,达到此值就会轮转。例如 10m (10MB)、 100m 1g
  • max-file :保留的日志文件数量。例如 3 表示保留最近3个轮转日志(如 container-id-json.log , container-id-json.log.1 , container-id-json.log.2 ),更旧的会自动删除。
  • compress :轮转的日志文件是否启用gzip压缩,可选 true false

如何在群晖上配置?

  1. 对于新建容器 :在DSM的Docker图形界面中,创建容器时,在“高级设置”->“执行命令”页面,你无法直接添加 --log-opt 参数。图形界面功能有限。更推荐的方式是使用命令行创建,或者先创建,再修改配置。

  2. 对于已运行的容器(推荐方法) :Docker容器一旦创建,其日志驱动参数不能直接修改。标准的做法是: a. 停止并删除旧容器 :在DSM Docker界面中停止容器,并删除它(注意:删除容器不会删除你映射出来的数据卷,但会丢失容器内的临时文件。确保你的数据都通过“卷”映射到了宿主机)。 b. 重新创建容器并指定日志参数 :使用Docker命令重新运行。例如,重新运行一个Nginx容器并配置日志轮转:

    docker run -d \
      --name my-nginx \
      --log-driver json-file \
      --log-opt max-size=10m \
      --log-opt max-file=3 \
      --log-opt compress=true \
      -p 8080:80 \
      nginx:latest
    

    这样,这个Nginx容器的日志文件最大为10MB,最多保留3个(压缩的)文件,总日志占用不会超过约30MB(压缩后更小)。

  3. 全局配置(谨慎操作) :你可以修改Docker守护进程的配置文件 /etc/docker/daemon.json (在群晖上路径可能是 /var/packages/Docker/etc/dockerd.json )来设置所有容器的默认日志驱动和选项。

    {
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "10m",
        "max-file": "3",
        "compress": "true"
      }
    }
    

    重要警告 :修改全局配置会影响之后创建的所有容器,且需要重启Docker服务才能生效。在群晖上,重启Docker服务可能会导致所有容器短暂停止。对于生产环境,建议逐个容器配置,而不是修改全局设置。

4. 进阶策略与深度管理

除了基本的查看和清理,还有一些进阶策略可以帮助你更好地管理Docker日志,适应更复杂的需求。

4.1 使用第三方日志驱动

如果你的群晖NAS性能足够,并且你有中央日志管理的需求,可以考虑使用其他Docker日志驱动,将日志直接发送到外部系统,避免在本地堆积。

  • syslog 驱动 :将日志发送到本机或远程的syslog服务器(如rsyslog)。这需要你在群晖上配置并运行syslog服务。
    docker run --log-driver=syslog --log-opt syslog-address=udp://localhost:514 your-image
    
  • journald 驱动 :如果群晖系统使用了systemd和journald(通常不是默认),可以使用此驱动将日志交给journald管理。
  • none 驱动 :直接禁用容器日志。 极度不推荐 ,除非你非常确定不需要任何日志,否则出问题时将无从排查。
    docker run --log-driver=none your-image
    
  • fluentd loki 等第三方驱动 :这些是更专业的日志收集方案,需要部署额外的服务(如Fluentd、Grafana Loki),适合大型或分布式环境。在资源有限的群晖上部署可能稍显复杂。

对于大多数家庭或小工作室用户,配置好 json-file 驱动的轮转参数已经足够。

4.2 编写定期清理脚本与计划任务

手动清理毕竟麻烦,我们可以让群晖自动定期执行。思路是创建一个清理脚本,然后通过DSM的“计划任务”功能定时运行。

  1. 创建清理脚本 :用SSH登录群晖,在某个目录(如 /volume1/scripts )下创建文件 clean_docker_logs.sh

    #!/bin/bash
    # clean_docker_logs.sh
    LOG_DIR="/volume1/@docker/containers"
    MAX_LOG_AGE_DAYS=7 # 保留最近7天的日志
    
    echo "$(date): 开始Docker日志清理任务..."
    
    # 方法A:清空所有日志文件(激进)
    # find "$LOG_DIR" -name "*-json.log" -type f -exec truncate -s 0 {} \;
    
    # 方法B:删除超过指定天数的日志文件(推荐)
    find "$LOG_DIR" -name "*-json.log" -type f -mtime +$MAX_LOG_AGE_DAYS -delete
    
    # 也可以结合轮转日志文件(*-json.log.1, *.gz等)
    find "$LOG_DIR" -name "*-json.log.*" -type f -mtime +$MAX_LOG_AGE_DAYS -delete
    
    echo "$(date): Docker日志清理任务完成。"
    

    给脚本添加执行权限: chmod +x /volume1/scripts/clean_docker_logs.sh

  2. 配置计划任务

    • 打开DSM的“控制面板”。
    • 进入“任务计划器”。
    • 点击“新增”->“计划的任务”->“用户定义的脚本”。
    • 在“常规”选项卡,给任务起个名字,比如“每周清理Docker日志”。
    • 在“计划”选项卡,设置运行频率,例如“每周”运行一次,选择非业务高峰时间(如周日凌晨3点)。
    • 在“任务设置”选项卡,在“用户定义的脚本”框中,填写脚本路径:
      /volume1/scripts/clean_docker_logs.sh
      
    • 点击“确定”。你可以立即运行一次进行测试。

提示:使用 -mtime +7 参数是基于文件的修改时间。确保你的Docker日志轮转配置( max-file )和这个脚本的清理策略协调好,避免脚本把还需要的轮转日志也删除了。更稳妥的做法是脚本只清理那些已经被Docker轮转出来的、带数字后缀的旧日志文件(如 *.log.2 , *.log.2.gz ),而主日志文件( *.log )由Docker自身的 max-size max-file 参数管理。

4.3 监控与告警集成

对于真正重要的NAS,我们不应该等到空间满了才发现问题。可以建立简单的监控。

  • 使用群晖内置的存储空间警告 :在DSM的“控制面板”->“通知设置”中,确保存储空间警告是开启的,并设置一个合理的阈值(例如85%)。这是最基本也是最重要的防线。
  • 自定义监控脚本 :编写一个脚本,定期检查Docker目录的大小,如果超过某个阈值就发送通知(通过邮件、Telegram Bot等)。这需要一些额外的配置,但提供了更细粒度的控制。
    #!/bin/bash
    DOCKER_DIR="/volume1/@docker"
    THRESHOLD_GB=50 # 阈值设为50GB
    CURRENT_SIZE_GB=$(du -s $DOCKER_DIR | awk '{print $1/1024/1024}') # 转换为GB
    
    if (( $(echo "$CURRENT_SIZE_GB > $THRESHOLD_GB" | bc -l) )); then
        # 这里可以加入发送告警的逻辑,例如调用邮件接口或curl一个Webhook
        echo "警告:Docker目录大小 ${CURRENT_SIZE_GB}GB 已超过阈值 ${THRESHOLD_GB}GB!" | mail -s "群晖Docker存储告警" your-email@example.com
    fi
    
    同样,可以将此脚本加入计划任务,每天运行一次。

5. 实战排坑:你可能遇到的典型问题与解决方案

在实际操作中,你可能会遇到一些意料之外的情况。这里分享几个我踩过的坑和解决办法。

5.1 清空日志后,容器日志无新输出或报错

现象 :使用 truncate cat /dev/null 清空日志文件后,通过 docker logs 命令看不到该容器新的日志输出,或者容器应用本身报错无法写入日志。

根因分析 :这种情况比较少见,但可能发生在一些特定场景下:

  1. Docker守护进程或容器应用对日志文件持有特殊的锁或缓存,清空操作可能没有正确同步。
  2. 极少数情况下,文件系统的inode或文件描述符出现异常。

解决方案

  1. 重启容器 :这是最简单有效的方法。重启容器会迫使Docker重新打开日志文件。
    docker restart <容器名或ID>
    
  2. 发送信号重新打开日志 :对于支持 SIGUSR1 信号的应用(如Nginx),可以向容器内进程发送信号,让其重新打开日志文件。但这需要应用本身支持,且操作复杂。
  3. 检查日志驱动配置 :确认容器没有使用 none journald 等驱动,这些驱动的日志不存储在 *-json.log 文件里,清空文件自然无效。

5.2 日志文件删除后,磁盘空间未释放

现象 :你删除了一个巨大的日志文件,但在DSM存储管理器中看到可用空间并没有增加,或者增加得很少。

根因分析 :这是因为文件可能还被某个进程打开着。在Linux系统中,当一个文件被进程打开时,你删除的只是文件系统目录项中的一个链接(文件名),而文件的实际数据块(inode)只有在所有指向它的链接都被删除,且没有进程再打开它时,才会被真正释放。如果你用 rm 删除了一个正在被Docker写入的日志文件,Docker进程仍然持有该文件的句柄,磁盘空间就不会立即释放。

解决方案

  1. 使用 lsof 命令查找并关闭句柄 :首先安装 lsof (群晖可能需要通过ipkg或Entware安装)。
    # 查找哪些进程正在使用已删除的文件
    lsof | grep deleted | grep json.log
    
    输出会显示进程ID(PID)和文件描述符(FD)。最根本的解决方法是重启持有该文件句柄的Docker容器或Docker守护进程。
    docker restart <容器ID>
    
    或者,在极端情况下,重启Docker服务(在群晖上可通过“套件中心”停用再启用Docker套件,但这会重启所有容器)。
  2. 预防优于治疗 :这就是为什么我们强烈推荐使用 truncate -s 0 而不是 rm 来清理日志。 truncate 操作不会删除文件链接,因此不存在空间不释放的问题。

5.3 配置日志轮转后,日志文件仍然很大

现象 :你已经为容器配置了 --log-opt max-size=10m --log-opt max-file=3 ,但发现主日志文件( container-id-json.log )还是超过了10MB。

根因分析 :Docker的日志轮转是异步触发的。它不会在日志文件达到 max-size 的瞬间立即切割,而是会在下一次写入日志时检查文件大小并触发轮转。因此,在两次检查之间,如果应用瞬间写入了大量日志(例如,一个异常堆栈跟踪),文件大小可能会暂时超过设定值。此外,请确认配置是否真的生效了。你可以使用 docker inspect <容器名> 命令查看容器的 HostConfig.LogConfig 部分。

解决方案

  1. 验证配置
    docker inspect --format='{{.HostConfig.LogConfig}}' <容器名>
    
    确认输出中 max-size max-file 的值是否正确。
  2. 手动触发轮转(如果驱动支持) :对于 json-file 驱动,没有直接的手动轮转命令。通常重启容器会生成新的日志文件。
  3. 调整应用日志级别 :如果某个容器日志增长异常快,考虑调整该容器内应用的日志级别,减少不必要的调试(DEBUG)或信息(INFO)日志的输出。这需要在运行容器的应用本身配置,而不是Docker层面。

5.4 群晖Docker图形界面中看不到日志选项

现象 :在DSM的Docker套件创建或编辑容器时,找不到设置日志驱动和轮转参数的地方。

分析与解决 :这是DSM Docker套件图形界面的一个功能限制。群晖的Docker套件基于Docker Engine,但其Web管理界面只暴露了最常用的功能,很多高级参数(包括日志驱动选项)需要通过命令行(CLI)来设置。这也是为什么我强烈建议重要的、长期运行的容器,使用 docker run 命令配合 --log-opt 参数来创建,以便获得更精细的控制。你可以先用图形界面配置好卷、端口映射等,记下参数,然后用等效的 docker run 命令来创建容器。

经过这一整套从诊断、清理、配置到监控和排坑的操作,你的群晖Docker日志管理应该已经上了正轨。记住核心原则: 预防优于清理,配置好日志轮转是根本 。定期检查一下存储空间,结合计划任务进行维护,就能让NAS在默默为你服务的同时,保持磁盘空间的清爽。

更多推荐