一个看似简单的需求------"让宿主机也有 dev 用户,让 xshell 和 Trae 都能连容器"------前后我大概踩了 6 个坑。每个坑单独看都是 Linux 基础,串在一起才发现:凡是跨了环境边界(宿主机 vs 容器、本地 vs 远程),就不能想当然。这篇把所有坑、所有原理、所有架构图一次性写全。
一、场景铺垫
1.1 我搭的是什么
一套 Docker Compose 开发环境,核心是一个容器化的 Ubuntu,预置了 dev 用户(UID 1000)、GCC/GDB/VS Code Server,端口映射 2222→22 给 SSH 用。
bash
/home/ubuntu/dev-environment/
├── docker-compose.yml
├── dev-environment/
│ └── workspace/ // 容器 volume 挂载源目录
│ └── readme
├── elasticsearch/
├── mysql/
├── redis/
└── rabbitmq/
1.2 原始 compose 配置
bash
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 连接架构
cpp
你的 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 一看:
bash
宿主机 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:
bash
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 神器一眼看穿
bash
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 权限,连"路过"都不行。
临时修复
bash
sudo chmod o+x /home/ubuntu # 只加 x 不加 r,能过但不能看
最终放弃 symlink
symlink 虽然能跑,但 dev 用户的 workspace 本质上还是"寄人篱下"------数据在 ubuntu 家目录里。这根刺扎得我心慌,决定彻底解决。
坑 3:Docker volume 相对路径 vs 绝对路径
原来的写法
bash
- ./dev-environment/workspace:/home/dev/workspace
./dev-environment/workspace 是相对路径,Docker Compose 会相对 docker-compose.yml 所在目录解析,得到:
bash
/home/ubuntu/dev-environment/dev-environment/workspace
也就是 ubuntu 家目录下的路径------这就是权限墙的根源。
改成绝对路径
bash
- /home/dev/workspace:/home/dev/workspace
直接指向 dev 自己家目录,跟 ubuntu 没关系。
宿主机侧准备
bash
# 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,输出:
bash
Container dev-env-service Recreate ← 看到这个就对了
Container dev-env-service Recreated
Container dev-env-service Starting
Container dev-env-service Started
如果配置没变(或者你忘了改对),会输出:
bash
dev-environment is up-to-date ← 什么也没做!
Recreate vs Restart:到底怎么回事
Docker Compose 对容器生命周期的管理逻辑:
bash
docker compose up -d
│
├─ 容器不存在 → 创建 + 启动
│
├─ 容器存在,但配置变了(volume/ports/environment/image)
│ → 销毁旧容器 + 创建新容器 + 启动 ← Recreate
│
└─ 容器存在,配置没变 → 啥也不做 ← up-to-date(幂等性)
| 操作 | 做了什么 | volume/ports/environment 生效? |
|---|---|---|
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 进程 | 独立的 | 独立的 |
修复:进容器设密码
cpp
sudo docker exec dev-env-service bash -c 'echo "dev:XXX(密码)" | chpasswd'
同时确认容器 SSH 密码登录开启:
bash
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 验证:
bash
echo "XXX" | su - dev -c 'echo ok' # 验证密码是否真的生效
教训 :Docker 容器是完全隔离的文件系统。宿主机的 useradd、chpasswd、chmod 都改不到容器里。凡是涉及容器内部系统配置的操作,必须 docker exec 进去搞。
坑 6:Trae Remote-SSH 配置写在哪?(本地 vs 远程)
Trae 已经在宿主机上了,我想让它连容器 dev 用户。第一反应在宿主机 的 ~/.ssh/config 加配置:
bash
# 宿主机 ~/.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 连接架构:
bash
[你的 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
bash
# 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,列表里就出现了。
四、完整操作时间线
cpp
[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
bash
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 命令
bash
sudo docker exec dev-env-service mount | grep workspace
# /dev/vda2 on /home/dev/workspace type ext4 (rw,relatime)
有挂载点说明不是容器原生目录。
方法 3:跨端写文件
bash
# 宿主机 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 反向查的。
bash
容器写了个文件,owner UID=1000
│
▼
宿主机 ls -l 查 UID 1000 → /etc/passwd 里 ubuntu 是 1000 → 显示 ubuntu 所有
│
但容器里写文件的是 dev 用户!
只是数字碰巧对上了而已!
反过来:
bash
宿主机 dev(UID 1005) 写了个文件,owner UID=1005
│
▼
容器 ls -l 查 UID 1005 → 容器的 /etc/passwd 里没人是 1005 → 显示数字 1005
想让两边显示一致,要么改宿主机 UID 跟容器对齐(但宿主机 1000 被占了),要么在 compose 里加 user: "1005:1006" 让容器进程以宿主机的 UID 跑。
6.2 symlink 权限判断的完整规则
bash
访问 /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 的核心设计是幂等------重复执行同样的命令,结果一样。
bash
# 第一次 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、每一次"改了配置没生效",背后都是一个值得想通的原理。