在 NAS 上玩 Docker,真正让人头疼的从来不是安装,而是存储。很多人都有过类似经历:容器跑了几个月一切正常,某天手痒更新了镜像,重建之后配置全没了;或者新部署的下载器日志里反复刷 Permission Denied,明明文件夹就在那儿,容器就是说没权限写。这两类问题的根子都在同一个地方——存储卷映射没搞明白。这篇不重复 Docker 怎么装,只聚焦一件事:卷映射怎么做才不翻车。
先分清两种卷:Bind Mount 和 Named Volume
Docker 持久化数据有两条路,NAS 用户经常混着用却说不清区别。
Bind Mount 是把宿主机上的一个具体路径直接映射进容器,比如把群晖的 /volume1/docker/sonarr 映射到容器的 /config。数据实打实落在 NAS 的文件系统里,你可以在 File Station 或 SMB 共享里直接看到、直接改、直接备份。
Named Volume 则由 Docker 自己管理,数据存放在 Docker 数据目录深处,平时不会在文件管理器里出现。它最大的好处是权限省心:Docker 在创建卷时会按镜像要求初始化属主,基本不会碰到 Permission Denied。
| 对比维度 | Bind Mount | Named Volume |
|---|---|---|
| 数据位置 | 宿主机指定路径,看得见摸得着 | Docker 内部管理的路径 |
| 文件管理 | 可直接在 NAS 文件管理器中操作 | 需要进 Docker 目录或借助命令 |
| 权限问题 | 需自己保证属主正确,容易踩坑 | Docker 初始化,较少权限纠纷 |
| 备份迁移 | 直接复制文件夹即可 | 需额外导出步骤 |
| 典型用途 | 配置、媒体库、下载目录 | 数据库等对权限敏感的目录 |
实践经验是:配置文件、媒体库、下载目录用 Bind Mount,因为你需要随时查看和备份;而应用内置数据库的目录(比如某些相册应用的 pgsql 目录)可以考虑单独挂 Named Volume,绕开宿主机权限的麻烦。
Permission Denied 的真相:容器里的用户和宿主机不是一回事
权限报错的本质很简单:Linux 只认 UID/GID 数字,不认用户名。容器里的进程以自己的 UID 往挂载目录写文件,如果这个数字和宿主机上目录属主的 UID 对不上,内核就直接拒绝。容器里显示的用户名叫什么并不重要,数字对不上就是没权限。
解决思路是让容器进程"降权"成你 NAS 上拥有目标目录的那个用户,这就是 PUID/PGID 环境变量的作用——支持这两个变量的镜像(linuxserver 系、大多数面向 NAS 的镜像都支持)会先以 root 完成初始化,再切换到你指定的 UID/GID 运行。
获取自己账号的 PUID 和 PGID
通过 SSH 登录 NAS,执行 id 用户名 即可拿到。比如绿联 NAS 上执行 id ugreen,就能看到该账号的 uid 和 gid。不同品牌默认值不一样:绿联默认用户 UID/GID 多为 1000,群晖常见的是 1026/100 这类组合,飞牛 OS 的 Docker 默认以 root 运行、通常不用额外设置。不要照抄别人的数字,以自己机器上 id 命令的输出为准。
把 PUID/PGID 传给容器
docker run 方式:
docker run -d
--name=container_name
-e PUID=1026
-e PGID=100
-v /volume1/docker/app/config:/config
-v /volume1/data:/data
image_nameCompose 方式:
environment:
- PUID=1026 # 替换为宿主机路径属主的 UID
- PGID=100 # 替换为宿主机路径属主的 GID
- TZ=Asia/Shanghai有一个硬性限制:PUID 和 PGID 不能为 0,也就是不能让容器以 root 身份跑应用。
如果镜像不支持这两个变量,可以用 Docker 原生的 user: "1000:1000" 强制指定运行用户。但这个方式有代价:镜像 entrypoint 里需要 root 权限的初始化步骤会全部失效,容器内还会出现 whoami: cannot find name for user ID 1000 这类提示(功能正常,只是名字查不到)。除非镜像文档明确没提 PUID/PGID,否则优先用环境变量方案。
一个容易被忽略的坑:初始化脚本只管 /config
很多人配好了 PUID/PGID,容器启动横幅里也显示了正确的 UID,结果第一次导入媒体库还是失败。原因在于:linuxserver 系镜像的初始化脚本启动时只对 /app、/config 和 /defaults 三个路径执行 chown,你挂载的 /data、/downloads、/tv 这类媒体路径会原样交给应用处理。这是刻意设计——每次启动都对十几 TB 的媒体库递归 chown,谁也受不了。
所以媒体目录的属主要自己在宿主机上修。先停止容器,再执行:
sudo chown -R 1026:100 /volume1/media一定要在容器停止后操作。应用运行期间递归 chown,可能赶上正在写入的文件,导致目录树只改了一半,引发第二轮更难排查的错误。
一套可以直接套用的路径映射方案
映射规划的核心原则是:按数据类型分目录,同类数据集中放,容器内路径保持语义清晰。
| 数据类型 | 宿主机路径示例 | 容器内路径 | 建议 |
|---|---|---|---|
| 应用配置 | /volume1/docker/sonarr/config | /config | Bind Mount,重点备份对象 |
| 媒体库 | /volume1/media/movies | /data/movies | Bind Mount,自行确保属主正确 |
| 下载目录 | /volume1/downloads | /downloads | 下载器与媒体工具挂同一宿主路径 |
| 应用数据库 | 不直接映射宿主机路径 | /config/pgsql | 单独用 Named Volume,避开权限坑 |
| 缓存临时文件 | 可不映射 | /cache | 无需持久化,随容器重建清空 |
其中下载目录和媒体库值得多说一句:下载器、Sonarr、Plex 这一串工具如果都要读写同一份文件,它们看到的挂载视角必须一致——宿主机路径一致,容器内路径也尽量一致。否则硬链接、移动文件这类操作会退化成跨目录复制,既慢又占双倍空间。
更新镜像不丢数据的正确流程
容器的设计哲学就是"可丢弃":写进容器层的数据,重建即消失。更新镜像之所以丢数据,几乎都是因为该映射出来的路径没映射,或者映射写错了一级目录,应用把数据悄悄写进了容器层,你用了很久都没察觉,一更新就现形。
稳妥的更新流程是这样:
- 先核对现有容器的卷映射和环境变量,确认所有需要保留的数据都落在宿主机路径或 Named Volume 里
- 停止容器,把 config 目录复制一份做备份
- 拉取新镜像,用完全相同的映射路径、PUID/PGID 和环境变量重建容器
- 启动后看日志,确认应用正常读写挂载目录,再删掉旧备份
用 Compose 管理的话翻车概率低得多:映射关系写在 yaml 文件里,docker compose pull 加 docker compose up -d 两步完成更新,参数不会凭记忆敲错。这也是为什么进阶玩家基本都从图形界面迁移到 Compose——配置即文件,重建可追溯。
最后给一个快速自查思路:每次部署新容器前,先想清楚这个应用会产生哪些需要保留的数据,分别是配置、数据库还是媒体文件,然后对照上面的映射表逐条落实。凡是没映射的路径,默认它明天就会消失——带着这个假设做规划,Permission Denied 和数据丢失这两类问题基本就与你无关了。

评论(3)
更新镜像丢数据那个太真实了,之前就栽过一回
UID和用户名对不上这个解释终于看明白了
一直用Bind Mount,原来数据库目录该用Named Volume