容器自启、更新与资源限制

重启策略:不仅仅是勾个选项

容器能不能在 NAS 重启后自己回来,取决于重启策略。向导里是一个「自动重启」勾选项,compose 里写成 restart 字段。没设这个值的容器,NAS 一重启全部趴窝,人不在家的时候就只能干瞪眼。

四种策略的差别值得分清楚。no 是默认值,容器停止后谁也不拉它;always 是只要容器不在运行就拉起来,包括你手动停止之后重启 NAS 的情况;unless-stopped 尊重手动停止,只在异常退出或系统重启时拉起;on-failure 只在进程异常退出时重启。自用场景里 unless-stopped 覆盖 90% 的需求。

策略 手动停止后 异常退出 NAS 重启后
no 不动 不动 不启动
always 保持停止 重启 启动
unless-stopped 保持停止 重启 启动
on-failure 保持停止 重启 不启动

注意 always 和 unless-stopped 的唯一区别在「手动停止后,系统重启时」这一个时刻。选 always 的人常遇到这种事:临时停掉的容器某天 DSM 更新重启后又自己跑了,还以为是灵异事件。

策略之外还有一个容易漏的设置:容器的启动顺序。多容器项目里用 depends_on 声明依赖,数据库先起、应用后起。但 depends_on 只保证启动顺序,不保证数据库初始化完成,应用可能连上还没 ready 的数据库而崩溃。稳健的做法是应用自身配置重试,或者 compose 里加 healthcheck 配合 depends_on 的 condition: service_healthy,Container Manager 的项目编辑器支持这种写法。

还有一个现实问题:DSM 更新重启或者断电之后,Docker 服务起得比某些依赖慢,偶尔会出现容器比存储就绪更早启动的情况。对顺序敏感的应用,可以在计划任务里加一条延时启动脚本,开机后等一分钟再逐个拉起关键容器。这种兜底手段比改配置温和,适合不想深究的玩家。

重启策略也影响排障节奏。出问题先手动停掉容器再看日志,always 策略下它会立刻被拉回来,日志滚得飞快;这时先临时改策略为 no 再排查,完事再改回来。

镜像更新:手动流程与回滚

Container Manager 没有内置自动更新,这是刻意的,容器自动更新出过不少事故。标准的手动更新流程只有四步:

  1. 在「镜像」标签页搜索并下载新版本的镜像。
  2. 停止旧容器,进入「编辑」。
  3. 把镜像版本切换到新下载的 tag,保存。
  4. 启动容器,验证功能。

这套流程的关键在于:映射出去的配置和数据都在原处,重建容器不丢任何东西。万一新版本出 bug,反向走一遍流程切回旧 tag 就完成回滚——前提是你没删掉旧镜像。所以更新前旧镜像先留着,确认稳定再清理。

习惯命令行的可以走 SSH,两条命令搞定更新,完整流程见 命令行速查:

docker compose -f /volume1/docker/app/docker-compose.yml pull
docker compose -f /volume1/docker/app/docker-compose.yml up -d

pull 拉取新镜像,up -d 用新镜像重建容器,配置沿用 YAML 里的定义。

要不要自动更新

社区里常见的自动更新方案是跑一个 Watchtower 容器,它定期检查镜像更新并自动重建容器。听起来很美,代价是所有更新都不经你手。镜像作者推个有问题的 tag,你睡着了,服务挂到天亮。

折中方案是只给低风险容器开自动更新,核心服务保持手动。Watchtower 自己也支持排除清单和更新窗口,配置时把数据库、反代这类关键容器加进排除列表。更高阶的玩法是给关键容器配 Webhook 通知,镜像有更新时只提醒不动手,更新时机完全由你掌握。通知渠道用 Uptime Kuma 也能顺带解决,监控和更新提醒用一套基础设施。

更新方式 及时性 风险 适合服务
手动 低 最低 数据库、反代
手动加通知提醒 中 低 大多数自用
Watchtower 排除关键 高 中 监控、工具类
全自动 最高 高 不推荐

资源限制:给容器画条线

DSM 的资源本来就不富裕,J 系列和 4 GB 内存的机型尤其紧张。给容器封顶资源,防的是某个应用内存泄漏把整机拖垮,DSM 卡到连管理界面都进不去。向导的高级设置里有内存上限,compose 里用 mem_limit:

services:
  app:
    image: jellyfin/jellyfin:latest
    mem_limit: 2048m
    memswap_limit: 2048m
    cpu_shares: 1024

mem_limit 是内存上限,memswap_limit 连交换空间一起封死,cpu_shares 控制抢 CPU 时的优先级,默认 1024,给重要服务调高、后台服务调低。注意内存上限别给得太小,应用一撞墙就会被 OOM 杀掉,日志里表现为进程突然消失。观察一周实际占用再定数值最稳妥:先不加限制跑几天,看「概览」里的内存曲线峰值,峰值乘 1.5 作为上限,既留出突发余量,又守住了天花板。Jellyfin 这类带转码的应用按峰值给到 2 GB,Vaultwarden 这类几百 MB 足够。

容器更新四步

什么时候该更新镜像

新手常见两个极端:一种是永远不更新,镜像里的安全补丁永远停在出厂那年;一种是天天追最新 tag,一周不更新就难受。成熟的做法是按风险分级更新:数据库、反代、暴露在公网的服务,每月固定一个时间集中更新,更新前看一眼项目的 release notes,有重大变更才动手;纯内网工具类容器,半年更新一次也没人催你。

安全补丁要单独对待。镜像仓库或者 CVE 新闻里点到你在用的镜像,别等排期,当天更新。反过来,如果某个版本你用得很稳,而新版本改了你不关心的功能,留在旧版本是完全合理的选择——这不是偷懒,是稳定优先的运维逻辑。更新记录建议维护一份清单,哪天更新了什么版本,出问题时回滚有依据。

监控与容量规划

资源限制是防守,监控才是规划。容器列表里每个容器都有实时 CPU、内存曲线,「概览」标签页给全局视图。发现哪个容器常驻吃掉 1 GB 内存时,就该想想它值不值这个资源,或者加内存。群晖加内存是所有玩法里回报最高的一步——DS920 之后的 Plus 机型基本都留了一条 SO-DIMM 槽。

一台4GB机型典型内存分配

长期建议:容器数量按内存算,每个轻量容器预留 200~400 MB,影音和数据库类按 1 GB 起。超了就加内存或者把不常用的迁移走,别让 NAS 长期交换换页,硬盘寿命和日常体验都受不了。容器跑得稳不稳,70% 取决于内存给得够不够。

另一个常被忽略的资源是存储 I/O。数据库类容器和影音转码的缓存目录都是读写大户,放在同一块机械硬盘上会互相拖累。有条件的话,docker 目录和缓存目录放 SSD,机械硬盘只放媒体库,整机响应会拉开明显差距。监控容器列表里的 IO 曲线能看出哪个应用在频繁读写。

退出码:日志不会说话,但会报数字

容器挂掉之后,列表里会留下一个三位数退出码,这是排查问题的第一线索。常用的几个要记住:0 是正常退出,127 是命令找不到(镜像或配置有问题),137 是被 OOM 杀死(内存上限给小了),255 通常是应用自己崩了。配合容器详情里的资源曲线看,137 的前一天多半有一条顶到天花板的内存线。

退出码 含义 第一步动作
0 正常退出进程 检查是否被手动停止
127 命令或脚本不存在 查镜像 tag 是否正确
137 被 OOM Killer 杀死 调大 mem_limit
255 应用异常退出 看容器日志找堆栈

反复 137 的容器别一味加内存,先看应用是否有内存泄漏的老毛病,社区 issue 里一般有结论。启动顺序和依赖设置不当也会表现为反复重启后退出,这时日志里通常连应用横幅都没打出来。

延伸阅读