【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 用。

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 端口),但我发现:

  1. 宿主机上没有 dev 用户,只有容器里有

  2. 想让 Trae 连容器 dev 用户,没配 SSH config

  3. 想让 xshell 连容器 dev 用户,密码没设

  4. 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" 太麻烦,先把权限通路搞定再说。


想让宿主机 /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 虽然能跑,但 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 容器是完全隔离的文件系统。宿主机的 useraddchpasswdchmod 都改不到容器里。凡是涉及容器内部系统配置的操作,必须 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 文件权限体系只存数字 UIDls -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 跑。


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

相关推荐
程序员-Benothing1 小时前
Linux用户与组管理命令大全:useradd、usermod、groupadd、passwd
linux·运维·服务器
Gyr01 小时前
浅刷了一遍51CTO软考题库客观题及其解析(5)
学习·软考·知识点·信息安全工程师
唠玖馆1 小时前
C++11
c++
bksczm1 小时前
MySQL基础篇之事务
linux·数据库·sql·mysql
bksczm1 小时前
MySQL基础篇之视图与用户管理
linux·数据库·sql·mysql
Ahtacca1 小时前
Linux 基础实验:从终端操作到 C 语言编译
linux·运维·运维开发·虚拟机
邪修king1 小时前
Re:Linux 系统篇(二十九):动静态库Chapter2:动态库深度辨析 —— 核心本质、制作流程、双阶段查找模型与排错指南
android·java·linux·开发语言
云泽8084 小时前
从零搭建 WSL2 + Ubuntu C/C++ 开发环境:原理、实践与底层架构解析
linux·c++·windows·ubuntu·wsl2
初願致夕霞10 小时前
C++11:类型推导与完美转发复盘笔记
c++