一个看似简单的需求——"让宿主机也有 dev 用户,让 xshell 和 Trae 都能连容器"——前后我大概踩了 6 个坑。每个坑单独看都是 Linux 基础,串在一起才发现:凡是跨了环境边界(宿主机 vs 容器、本地 vs 远程),就不能想当然。这篇把所有坑、所有原理、所有架构图一次性写全。
一、场景铺垫
1.1 我搭的是什么
一套 Docker Compose 开发环境,核心是一个容器化的 Ubuntu,预置了 dev 用户(UID 1000)、GCC/GDB/VS Code Server,端口映射 2222→22 给 SSH 用。
/home/ubuntu/dev-environment/
├── docker-compose.yml
├── dev-environment/
│ └── workspace/ // 容器 volume 挂载源目录
│ └── readme
├── elasticsearch/
├── mysql/
├── redis/
└── rabbitmq/
1.2 原始 compose 配置
networks:
dev-network:
driver: bridge
services:
dev-environment:
image: …/dev-environment:1.0
container_name: dev-env-service
volumes:
– /etc/localtime:/etc/localtime:ro
– ./dev-environment/workspace:/home/dev/workspace // ← 相对路径
ports:
– "2222:22"
– "9000-9004:9000-9004"
restart: always
privileged: true
networks:
– dev-network
// … mysql / redis / rabbitmq / elasticsearch / kibana / etcd / fastdfs 省略 …
1.3 连接架构
你的 Windows 电脑
├─ xshell ──SSH──▶ dev@服务器IP:2222 ──▶ 容器内 dev 用户
└─ Trae ──SSH──▶ ubuntu@服务器IP:22 ──▶ 宿主机 ubuntu 用户(当前)
二、问题来了
Trae 已经连了宿主机(ubuntu 用户,22 端口),但我发现:
宿主机上没有 dev 用户,只有容器里有
想让 Trae 连容器 dev 用户,没配 SSH config
想让 xshell 连容器 dev 用户,密码没设
volume 挂载在 ubuntu 家目录下,dev 用户访问撞权限墙
看上去 4 个小问题,逐个解决。
三、踩坑全记录
坑 1:宿主机建 dev 用户,UID 对不上
useradd -m -s /bin/bash dev 一行搞定,但 id dev 一看:
宿主机 dev: uid=1005, gid=1006
容器内 dev: uid=1000, gid=1000
宿主机 UID 1000 已被 ubuntu 占了(ubuntu:x:1000:1001),没法再建。
为什么 UID 对齐重要? Docker volume 是按数字 UID 映射文件所有者的:
-
容器里 dev(UID 1000)写的文件 –> 宿主机显示 UID 1000 = ubuntu 所有
-
宿主机 dev(UID 1005)改的文件 –> 容器里显示数字 1005,找不到用户
暂时搁置——改镜像加 user: "1005:1006" 太麻烦,先把权限通路搞定再说。
坑 2:symlink 权限墙(Permission denied)
想让宿主机 /home/dev/workspace 直接指向容器挂载源,搞了个 symlink:
sudo rm -rf /home/dev/workspace
sudo ln -s /home/ubuntu/dev-environment/dev-environment/workspace /home/dev/workspace
sudo chown -h dev:dev /home/dev/workspace
结果 dev 用户 cat /home/dev/workspace/readme 报 Permission denied。
根因分析:用 namei 神器一眼看穿
namei -l /home/dev/workspace/readme
f: /home/dev/workspace/readm
drwxr-xr-x root root / ← 755
drwxr-xr-x root root home ← 755
drwxr-xr-x dev dev dev ← 755
lrwxrwxrwx dev dev workspace -> … ← symlink 权限永远 777,不影响
drwxr-x— ubuntu ubuntu ubuntu ← 750 卡这了!
drwxrwxr-x ubuntu docker dev-environment
drwxrwxr-x ubuntu docker dev-environment
drwxrwxr-x dev dev workspace
-rw-rw-r– dev dev readme
symlink 权限判断规则:symlink 自身的 lrwxrwxrwx 永远是 777,系统判断的是目标文件的权限——但前提是能走到目标文件,即路径上每一层目录都必须有 x(遍历)权限,不管你是不是 owner/group。
/home/ubuntu 是 750(drwxr-x—),dev 用户既不是 owner 也不是 ubuntu 组的,没有 x 权限,连"路过"都不行。
临时修复
sudo chmod o+x /home/ubuntu # 只加 x 不加 r,能过但不能看
最终放弃 symlink
symlink 虽然能跑,但 dev 用户的 workspace 本质上还是"寄人篱下"——数据在 ubuntu 家目录里。这根刺扎得我心慌,决定彻底解决。
坑 3:Docker volume 相对路径 vs 绝对路径
原来的写法
– ./dev-environment/workspace:/home/dev/workspace
./dev-environment/workspace 是相对路径,Docker Compose 会相对 docker-compose.yml 所在目录解析,得到:
/home/ubuntu/dev-environment/dev-environment/workspace
也就是 ubuntu 家目录下的路径——这就是权限墙的根源。
改成绝对路径
– /home/dev/workspace:/home/dev/workspace
直接指向 dev 自己家目录,跟 ubuntu 没关系。
宿主机侧准备
# 1. 删掉 symlink
sudo rm /home/dev/workspace
# 2. 把 ubuntu 家目录下的 workspace 内容搬过来
sudo cp -a /home/ubuntu/dev-environment/dev-environment/workspace /home/dev/workspace
# 3. 改所有权为 dev
sudo chown -R dev:dev /home/dev/workspace
关键知识点
|
相对路径 ./xxx |
相对 compose 文件所在目录 |
通常落到某用户家目录下,跨用户访问撞权限墙 |
|
绝对路径 /home/dev/xxx |
直接用 |
如果目录不存在,Docker 自动创建但所有权是 root,目标用户写不了 |
教训:用绝对路径时,先手动创建目录 + chown,再让 Docker 挂。别让 Docker 帮你建——它建出来归 root。
坑 4:改了 compose 文件,忘了重建容器(最容易忘的一步)
改完 volume 配置,直接 docker compose up -d dev-environment,输出:
Container dev-env-service Recreate ← 看到这个就对了
Container dev-env-service Recreated
Container dev-env-service Starting
Container dev-env-service Started
如果配置没变(或者你忘了改对),会输出:
dev-environment is up-to-date ← 什么也没做!
Recreate vs Restart:到底怎么回事
Docker Compose 对容器生命周期的管理逻辑:
docker compose up -d
│
├─ 容器不存在 → 创建 + 启动
│
├─ 容器存在,但配置变了(volume/ports/environment/image)
│ → 销毁旧容器 + 创建新容器 + 启动 ← Recreate
│
└─ 容器存在,配置没变 → 啥也不做 ← up-to-date(幂等性)
|
docker compose restart |
只重启进程,不销毁不重建 |
不变 |
|
docker compose up -d(配置变了) |
销毁旧容器 → 创建新容器 |
生效 |
|
docker compose up -d(配置没变) |
啥也不做 |
不变 |
关键区分:volume、ports、environment 这些a是容器创建时就定死的配置,改了以后必须销毁旧容器、用新配置创建新容器——光 restart 没用。
Recreate 会清空容器内非持久化数据(不在 volume 里的文件),所以重要数据一定要挂 volume。
坑 5:密码设错了地方(最痛的教训)
xshell 连 dev@服务器IP:2222,密码输 XXX,SSH 拒绝密码。
我第一反应:密码设了啊!echo "dev:XXX" | sudo chpasswd 还跑成功了!
但是——我设的是宿主机 dev 用户的密码,而 xshell 连的是容器里的 SSH!
这是两套完全独立的系统:
|
SSH 端口 |
22 |
2222 |
|
dev 用户 |
新建的 UID 1005 |
镜像预置的 UID 1000 |
|
密码文件 |
/etc/shadow |
容器内的 /etc/shadow |
|
sshd 进程 |
独立的 |
独立的 |
修复:进容器设密码
sudo docker exec dev-env-service bash -c 'echo "dev:XXX(密码)" | chpasswd'
同时确认容器 SSH 密码登录开启:
sudo docker exec dev-env-service bash -c '
grep -E "^PasswordAuthentication|^PermitRootLogin" /etc/ssh/sshd_config
service ssh status
'
# PermitRootLogin yes
# PasswordAuthentication yes
# * sshd is running
额外坑:单字符密码 chpasswd 静默失败
如果 echo "dev:XXX" | chpasswd 没报错但密码实际上没改成功(PAM 策略可能拦了但没输出),用交互式 passwd 验证:
echo "XXX" | su – dev -c 'echo ok' # 验证密码是否真的生效
教训:Docker 容器是完全隔离的文件系统。宿主机的 useradd、chpasswd、chmod 都改不到容器里。凡是涉及容器内部系统配置的操作,必须 docker exec 进去搞。
坑 6:Trae Remote-SSH 配置写在哪?(本地 vs 远程)
Trae 已经在宿主机上了,我想让它连容器 dev 用户。第一反应在宿主机的 ~/.ssh/config 加配置:
# 宿主机 ~/.ssh/config
Host dev-container
HostName 127.0.0.1
Port 2222
User dev
然后 Trae 里 Ctrl+Shift+P → Connect to Host……列表里啥也没有。
为什么?
因为 Trae 的 Remote-SSH 插件跑在你本地 Windows 电脑上,它读的是本地的 SSH 配置,不是远程宿主机的。
Trae 连接架构:
[你的 Windows 电脑]
│
├─ Trae IDE(VS Code 前端,跑在本地)
│
├─ Remote-SSH 插件(跑在本地)
│ 读取:C:\\Users\\XXX(用户名)\\.ssh\\config ← 关键!本地配置!
│
├─ SSH 连接 ubuntu@XXXXXXXXXX
│
└─ 在远程宿主机上安装 VS Code Server
│
(这时候 Trae 是宿主机上的一个客户端)
│
想连容器 dev 用户?
│
├─ 再开一个 Trae 窗口
│
└─ Remote-SSH 再连 dev@XXXXXXXXXX
(这个连接也是从 Windows 发起的!)
修复:在本地 Windows 写 config
# C:\\Users\\XXX(账户名字)\\.ssh\\config
Host dev-container
HostName XXXXXXXX
Port 2222
User dev
Trae 里 Reload Window(Ctrl+Shift+P → Reload Window),再 Connect to Host,列表里就出现了。
四、完整操作时间线
[1. 宿主机建 dev 用户]
sudo useradd -m -s /bin/bash dev
echo "dev ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/dev
echo "XXX" | passwd dev(交互式验证)
[2. symlink 方案(失败)]
sudo rm -rf /home/dev/workspace
sudo ln -s /home/ubuntu/dev-environment/dev-environment/workspace /home/dev/workspace
→ Permission denied(namei 定位到 /home/ubuntu 750 权限墙)
→ 临时 sudo chmod o+x /home/ubuntu
[3. docker-compose volume 改绝对路径]
改 ./dev-environment/workspace → /home/dev/workspace
删 symlink,cp -a + chown 搬真实目录
[4. 重建容器]
sudo docker compose up -d dev-environment
→ Recreate 正确
[5. 容器内设密码
sudo docker exec dev-env-service bash -c 'echo "dev:XXX" | chpasswd'
确认 SSH 密码登录开启
[6. 本地 Trae SSH config]
Windows: C:\\Users\\XXX(账户名字)\\.ssh\\config 加 Host dev-container
Trae Reload Window
[7. 验证]
xshell 连 dev@IP:2222 → 正确
Trae 连 dev-container → 正确
宿主机写文件 → 容器里看 → 正确
五、验证挂载正确的 3 种方法
方法 1:docker inspect 看 Mounts
sudo docker inspect dev-env-service –format '
{{range .Mounts}} {{.Source}} -> {{.Destination}}{{"\\n"}}{{end}}'
# /etc/localtime -> /etc/localtime
# /home/dev/workspace -> /home/dev/workspace ← 源路径已在 dev 家目录下
方法 2:容器内 mount 命令
sudo docker exec dev-env-service mount | grep workspace
# /dev/vda2 on /home/dev/workspace type ext4 (rw,relatime)
有挂载点说明不是容器原生目录。
方法 3:跨端写文件
# 宿主机 dev 用户写
sudo su – dev -c 'touch ~/workspace/from-host'
# 容器里看
sudo docker exec dev-env-service ls /home/dev/workspace/
# 应该有 from-host
反过来也成立——同一个物理路径,两边实时同步。
六、底层原理深挖
6.1 UID/GID 与 Docker volume 文件所有者的关系
Linux 文件权限体系只存数字 UID,ls -l 显示的用户名是 /etc/passwd 反向查的。
容器写了个文件,owner UID=1000
│
▼
宿主机 ls -l 查 UID 1000 → /etc/passwd 里 ubuntu 是 1000 → 显示 ubuntu 所有
│
但容器里写文件的是 dev 用户!
只是数字碰巧对上了而已!
反过来:
宿主机 dev(UID 1005) 写了个文件,owner UID=1005
│
▼
容器 ls -l 查 UID 1005 → 容器的 /etc/passwd 里没人是 1005 → 显示数字 1005
想让两边显示一致,要么改宿主机 UID 跟容器对齐(但宿主机 1000 被占了),要么在 compose 里加 user: "1005:1006" 让容器进程以宿主机的 UID 跑。
6.2 symlink 权限判断的完整规则
访问 /home/dev/workspace/readme
│
├─ Step 1: 路径分解
│ / (root) → home → dev → workspace → readme
│
├─ Step 2: 逐层检查 x 权限(遍历权)
│ / → 755 正确 任何用户都有 x
│ /home → 755 正确
│ /home/dev → 755 正确
│ /home/dev/workspace → 这是 symlink!先跳过去
│ /home/ubuntu → 750 错误 dev 没有 x,卡住!
│
└─ 结论:symlink 自身权限不影响,卡的是目标路径上的目录权限
实用工具:namei -l <路径> 一眼看出哪层卡了,比 ls -la 直观 10 倍。
6.3 容器 vs 宿主机:哪些东西是独立的?
|
文件系统 |
/, /home/… |
容器自己的根文件系统 |
完全独立,volume 是唯一通道 |
|
/etc/passwd |
宿主机用户表 |
容器镜像里的用户表 |
独立,UID 可以不同 |
|
/etc/shadow |
宿主机密码 |
容器密码 |
独立,改一个不影响另一个 |
|
sshd |
端口 22,独立进程 |
端口 2222(映射),独立进程 |
独立配置 |
|
/dev |
宿主机设备节点 |
容器看到的设备节点(privileged 时共享) |
privileged 时部分共享 |
|
进程 |
宿主机进程 |
容器进程(有自己的 PID namespace) |
独立 PID 空间 |
一句话:容器除了 CPU/内存/内核共享宿主机,其他基本都是独立的。docker exec 是你进入容器改配置的唯一通道。
6.4 Docker Volume 三种类型
|
Named Volume |
mydata:/data |
/var/lib/docker/volumes/mydata/_data |
Docker |
不关心具体路径,只要持久化 |
|
Bind Mount(相对) |
./workspace:/data |
相对 compose 文件目录 |
手动或 Docker |
跟项目走,方便 |
|
Bind Mount(绝对) |
/home/dev/ws:/data |
指定的绝对路径 |
必须手动! |
跨用户、权限明确 |
|
tmpfs |
tmpfs:/data |
内存 |
— |
临时数据 |
本次用的是绝对路径 Bind Mount,因为需要明确指定 dev 用户家目录,避免相对路径解析到别人地盘。
6.5 Docker Compose 的幂等性与 Recreate 机制
Docker Compose 的核心设计是幂等——重复执行同样的命令,结果一样。
# 第一次 up -d
容器不存在 → 创建 + 启动 → state: running
# 第二次 up -d(配置没变)
容器存在 + 配置没变 → 啥也不做 → state: up-to-date
# 第三次 up -d(volume 改了)
容器存在 + volume 变了 → Recreate:
1. 停止旧容器(stop)
2. 删除旧容器(rm,但 volume 数据保留!)
3. 创建新容器(用新 volume 配置)
4. 启动新容器
Recreate ≠ Restart:restart 只在原有容器上重启进程,volume 挂载、端口映射这些创建时定死的配置完全不变。
6.6 Trae Remote-SSH 架构
关键点:Remote-SSH 插件跑在本地,SSH config 也读本地的。远程机器上只有一个 VS Code Server(Node.js 程序),负责把前端的操作翻译成对远程文件系统/进程的调用。
七、最终架构图
八、避坑清单(全集)
|
1 |
宿主机建用户 |
UID 跟容器对不上 |
宿主机 UID 1000 被占 |
要么改容器 user: 对齐,要么接受文件所有者显示数字 |
|
2 |
symlink 访问 |
Permission denied |
路径中间某层目录没 x 权限 |
namei -l 定位,chmod o+x 补权限,或干脆不用 symlink |
|
3 |
Docker volume 相对路径 |
跨用户访问撞权限墙 |
相对路径解析到别人的家目录下 |
改用绝对路径,直接指定目标用户家目录 |
|
4 |
Docker volume 绝对路径 |
文件所有者是 root |
Docker 自动创建的目录归 root |
手动创建 + chown,再让 Docker 挂 |
|
5 |
改了 compose |
容器没变化 |
restart 不够,volume 是创建时配置 |
docker compose up -d 触发 Recreate |
|
6 |
容器密码 |
SSH 拒绝密码 |
改了宿主机 shadow,容器是另一个 shadow |
docker exec 进容器设密码 |
|
7 |
容器密码 |
chpasswd 静默失败 |
PAM 策略可能拦了没报错 |
交互式 passwd + su – dev -c 'echo ok' 验证 |
|
8 |
Trae Remote-SSH |
列表里没有新主机 |
SSH config 写到了远程,插件读的是本地 |
写 Windows 本地 C:\\Users\\你\\.ssh\\config |
|
9 |
Recreate 丢数据 |
容器内文件没了 |
非 volume 路径的数据不持久化 |
重要数据全挂 volume,规划好挂载路径 |
|
10 |
忘了容器隔离 |
改了宿主机以为容器也变 |
容器是独立文件系统 |
volume 是唯一通道,docker exec 是唯一入口 |
九、一句话总结
凡是跨了环境边界(宿主机 vs 容器、本地 vs 远程),就不能想当然地觉得"改一个另一个也会变"。 宿主机和容器各是各的文件系统和密码库,本地和远程各是各的 SSH 配置——你要做的是先搞清楚两边各自管什么,再用 volume、SSH、bind mount 这些"桥"把它们连起来。
Linux 不怕你踩坑,怕的是踩了坑不知道为什么踩的。每一个 Permission denied、每一个数字 UID、每一次"改了配置没生效",背后都是一个值得想通的原理。






