首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WSL2 + Docker 跑本地大模型:12 个官方文档不会告诉你的坑

WSL2 + Docker 跑本地大模型:12 个官方文档不会告诉你的坑

原创
作者头像
用户12706783
发布2026-08-21 16:46:06
发布2026-08-21 16:46:06
1190
举报

目标:在自己的机器上跑一个断网的本地模型,用来处理西门子博图(TIA Portal)的工程问题。 要求不只是"能跑",而是要能向别人证明数据没出去过。 环境:Windows 11 + RTX 4060 Laptop 8GB + WSL2 + Docker。

先说结论:能做成,但过程中 90% 的时间不是在写代码,是在跟环境搏斗。

下面这些坑,每一个都真实卡住过我至少半小时,有几个卡了三轮。

写出来是因为它们在网上基本搜不到——报错信息很具体,但没人写过原因。


一、最坑的:模型加载成功了,一提问就炸

现象非常有迷惑性:

代码语言:txt
复制
[06:34:41] transformers 后端已加载: Qwen3-4B-Base(设备 cuda:0)
[06:34:41] 培养皿就绪,后端=transformers

显存占用 4390 MiB,nvidia-smi 能看到,日志一片正常。然后第一次提问:

代码语言:python
复制
File "triton/runtime/jit.py", line 713, in run
    device = driver.active.get_current_device()
RuntimeError: Failed to find C compiler. Please specify via CC environment variable

原因bitsandbytes 的 4bit 反量化走 Triton,而 Triton 是在运行时把 kernel 编译成机器码的。容器里没装 gcc,加载阶段不需要编译所以一切正常,一推理才炸

解法:镜像里装 gcc g++ build-essential,并给 Triton 一个固定可写的缓存目录:

代码语言:dockerfile
复制
RUN apt-get install -y gcc g++ build-essential
ENV TRITON_CACHE_DIR=/tmp/triton-cache

教训:加载成功给的"没问题"错觉非常强。验证要验到链路末端,不能看到日志正常就宣布完工。


二、wsl --install 在脚本里静默失败

非交互环境(脚本、CI、被程序调用)下执行:

代码语言:bash
复制
wsl --install -d Ubuntu-24.04 --no-launch

退出码 0,没有任何输出,什么也没装。 wsl -l -v 里根本没有这个发行版。

--web-download 换 CDN 也一样。

解法:手动下 rootfs 再导入。

代码语言:bash
复制
curl -L -o ubuntu.tar.gz \
  https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz
wsl --import Ubuntu-24.04 D:\WSL\Ubuntu-24.04 ubuntu.tar.gz

注意cdimages.ubuntu.com/ubuntu-wsl/noble/daily-live/ 下的 daily 构建版导入会失败

代码语言:txt
复制
var/spool/mail: Failed to create dir 'var': No such file or directory
bsdtar: Error exit delayed from previous errors.

daily 包的 tar 里缺 var/ 这类目录条目本身、只有目录下的文件条目,WSL 用的 bsdtar 不会自动补父目录。换 releases/ 下的正式版就好了。

更优解:直接用 ubuntu-base(28 MB),比 WSL 专用包小十倍,自己装 docker 反而更快。


三、gpg --dearmor 在没有 tty 的环境里挂起

装 NVIDIA Container Toolkit 的官方文档第一步:

代码语言:bash
复制
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

在脚本里跑,它会挂起,没有任何报错。key 明明下载成功了 3195 字节,下一行就再无输出。

我为此白跑了三次,每次都以为是网络问题。

原因gpg 初始化钥匙环时需要 tty,非交互环境下直接卡住。

解法:现代 apt 支持 ASCII armored 的 .asc,压根不需要转换:

代码语言:bash
复制
curl -sSL https://nvidia.github.io/libnvidia-container/gpgkey \
  -o /etc/apt/keyrings/nvidia-container-toolkit.asc
echo "deb [signed-by=/etc/apt/keyrings/nvidia-container-toolkit.asc] \
  https://nvidia.github.io/libnvidia-container/stable/deb/amd64 /" \
  > /etc/apt/sources.list.d/nvidia-container-toolkit.list

顺带一条:清华镜像没有这个仓库(试过 404)。而 WSL 里访问 nvidia.github.io 实测 200 ——

连不上的是 Windows 侧的 Git Bash,不是 WSL。别想当然地把所有源都换成国内镜像。


四、--internal 网络下 docker -p 完全不生效

我要让容器彻底断网,但又要能从宿主访问里面的服务(noVNC 桌面)。

第一反应是 --network internal-p 端口映射,理由听起来很对:

-p 是入站规则,不改出站路由

实测:完全不生效。

代码语言:bash
复制
$ docker port dish
(空)
$ docker ps
dish   ...   6080/tcp        ← 只有 EXPOSE,没有 -> 映射

原因--internal 网络禁用了 NAT,而 -p 依赖 NAT。

解法:宿主侧自己转一道。

代码语言:bash
复制
socat TCP-LISTEN:6080,fork,reuseaddr,bind=0.0.0.0 TCP:172.18.0.2:6080

但这里还有一个坑nohup socat ... &wsl.exe 执行完命令后会被一起带走——

wsl 会话结束会清理整个进程组。要用 setsid 让它脱离会话:

代码语言:bash
复制
setsid nohup socat TCP-LISTEN:6080,fork,reuseaddr TCP:$IP:6080 </dev/null &>/log &

顺便说,--internal 的隔离强度比防火墙规则高:

代码语言:python
复制
socket.create_connection(('223.5.5.5', 53), 6)
# OSError: [Errno 101] Network is unreachable

unreachable 而不是超时 —— 内核层面压根没有到外网的路由,比 DROP 更硬。

这一条我每次启动容器都会自动重测一遍,因为"隔离"这种事不能靠假设。


五、硬链接快照会被原地写入污染

为了让实验可回滚,我用硬链接做目录快照——秒级完成、几乎不占空间,看起来很聪明。

然后发现:改完配置,快照里的那一份跟着变了,回滚等于没回滚。

原因:硬链接指向同一个 inode。只有当写入方"写临时文件再改名"(产生新 inode)时快照才安全;

谁要是原地改写(配置文件、日志、某些框架存 checkpoint),链过去的快照会被一起改掉。

解法:小文件一律实拷,只有大文件才硬链接;自己的写入统一走原子替换。

代码语言:python
复制
tmp = path.with_suffix(".tmp")
tmp.write_text(data, encoding="utf-8")
os.replace(tmp, path)          # 原子替换,产生新 inode

六、几个 Windows 侧的小刀子

.wslconfig 的默认值能把你坑死。我的机器上不知何时被写成了

defaultVhdSize=512MBmemory=1GB —— 结果在 WSL 里装两个包就

No space left on device。查了半天磁盘,其实是虚拟盘上限太小。

Git Bash 会转换路径df -h / 会变成 df -h C:/Users/Git/

docker exec dish ls /app 里的 /app 也会被改写。加 MSYS_NO_PATHCONV=1

多层引号会被打散wsl -- bash -c '...' 里带变量和引号的复杂命令,

经过 Git Bash → wsl.exe → bash 三层会面目全非。复杂命令一律写成 .sh 文件再执行。

NAME 这个变量名在 WSL 里已被占用(是主机名)。我写 ${NAME:-dish} 想给容器起名,

结果容器被命名成了 DESKTOP-BUQ9C4C。变量名要够独特。

curl -C - 续传会给你一个"看起来完整"的坏文件:大小对得上、退出码 0,

gzip -t 一测却是损坏的。校验必须做内容级,不能只看大小和退出码。

.bat 文件不能存成 UTF-8。cmd.exe 按系统 ANSI(GBK)解析,

UTF-8 的中文注释会把整行吃掉——连 echoset 这种内置命令都会报

"不是内部或外部命令"。这跟写别的文件的规矩正好相反。


七、容器里的中文桌面:locale 被排除了

想在容器里跑一个中文的 XFCE 桌面(用 noVNC 从浏览器进)。

装了 locales、生成了 zh_CN.UTF-8、环境变量也设了,界面依然全是英文

而且所有常规检查都显示"没问题"。

原因:官方 slim 镜像在 /etc/dpkg/dpkg.cfg.d/ 里写了

path-exclude=/usr/share/locale/*不删这条规则,装再多语言包翻译文件也不会落盘。

解法(必须排在装桌面之前,顺序反了就白装):

代码语言:dockerfile
复制
RUN sed -i '/path-exclude.*\/usr\/share\/locale/d' /etc/dpkg/dpkg.cfg.d/*

验证方式:别看有没有报错,直接数翻译文件:

代码语言:bash
复制
ls /usr/share/locale/zh_CN/LC_MESSAGES/ | wc -l    # 32

输入法还有个连环坑:fcitx5 装好了、能打出中文,但 Ctrl+空格 切换永远没反应。

两个原因叠在一起:

  1. 只写了 profile(选哪个输入法)不够,快捷键在 config 文件里,那个文件我压根没建
  2. fcitx5 和桌面不在同一个 dbus 会话

第 2 条绕了我三轮。原因是 dbus-launch --exit-with-session xfce4-session

把 dbus 地址关在了包装进程里,脚本自己拿不到;我试图从 /proc/<pid>/environ 反查,

pgrep -f xfce4-session 先匹配到的是 dbus-launch 那个包装进程,读到的地址是错的。

结果两次诊断的结论正好相反,一次"fcitx5 没有终端有",一次"fcitx5 有终端没有"。

正确做法:让启动脚本自己持有 dbus 会话,两个子进程都从它继承,不去猜别人的环境。

代码语言:bash
复制
eval "$(dbus-launch --sh-syntax)"
export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID
xfce4-session &
fcitx5 -d &

八、pkill dockerd 之后不能 sleep 3 就重启

代码语言:bash
复制
pkill dockerd
sleep 3
nohup dockerd &          # 起不来

dockerd 优雅关闭要先停掉所有容器,3 秒根本不够。新旧两个进程撞在一起,两边都起不来,

而日志里全是旧进程的 shutdown 记录,看着像是新进程的错误。

解法:等条件,不要等时间。

代码语言:bash
复制
pkill -TERM dockerd
for i in $(seq 1 30); do pgrep dockerd >/dev/null || break; sleep 1; done
pgrep dockerd >/dev/null && pkill -KILL dockerd
rm -f /var/run/docker.pid
nohup dockerd &

这条是通用的:凡是"等某个东西就绪",都该轮询条件而不是拍脑袋 sleep。


最后:一个可复现的小实验

环境跑通之后,我做了一件事——让模型答错的题自动进"错题本",

下次遇到相似问题时把上次的正确答法注入上下文。然后同一批题跑两遍:

代码语言:txt
复制
闭卷(不给参考)  0.225
开卷(给错题本)  0.800

10 道题里,6 道原本 0 分的全部翻到满分——正是进过错题本的那 6 道;

没进错题本的 4 道分数纹丝不动。后面这半句才是关键:它排除了随机波动

但必须说清楚:这不代表模型变强了,权重一个字节都没动。

把错题本清空,它立刻回到 0.225。这个数字衡量的是"检索注入"这条通路有没有生效,

不是模型能力。

真要让模型内化这些知识得靠微调改权重。而错题本攒下来的,

正好就是微调需要的问答数据集——用着用着,训练集就攒出来了。


写在最后

整套东西的目的不是"跑个本地模型"——那用 Ollama 五分钟就能搞定,效果还更好。

目的是能证明:数据没出去过(每次启动实测出网被挡)、

模型见过什么(每份投喂都有来源和哈希)、答错了怎么处理(错题本 + 奖惩留痕)。

Ollama 和各种一键工具解决的是"能用",不解决"敢交付"。

如果你也在琢磨把 AI 装进工厂、装进客户内网,大概会撞上同样这堆问题。

有一起在做这个方向的,欢迎交流。


完整代码与复现脚本:https://github.com/feiyuez638-cloud/plc-dish

同类隔离部署踩过别的坑的,欢迎评论区补充。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、最坑的:模型加载成功了,一提问就炸
  • 二、wsl --install 在脚本里静默失败
  • 三、gpg --dearmor 在没有 tty 的环境里挂起
  • 四、--internal 网络下 docker -p 完全不生效
  • 五、硬链接快照会被原地写入污染
  • 六、几个 Windows 侧的小刀子
  • 七、容器里的中文桌面:locale 被排除了
  • 八、pkill dockerd 之后不能 sleep 3 就重启
  • 最后:一个可复现的小实验
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档