欢迎光临
我们一直在努力

【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战

一个看似简单的需求——"让宿主机也有 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" 太麻烦,先把权限通路搞定再说。


    想让宿主机 /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 虽然能跑,但 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(幂等性)

    操作

    做了什么

    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 进程

    独立的

    独立的

    修复:进容器设密码

    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 跑。


    访问 /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、每一次"改了配置没生效",背后都是一个值得想通的原理。

    赞(0)
    未经允许不得转载:171主机测评 » 【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址