惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
J
Java Code Geeks
MongoDB | Blog
MongoDB | Blog
Martin Fowler
Martin Fowler
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
B
Blog
U
Unit 42
B
Blog RSS Feed
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
腾讯CDC
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
博客园 - 聂微东
MyScale Blog
MyScale Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta

阿尔的代码屋 | 全栈技术笔记

VoxCPM2 多语言语音合成与声音克隆本地部署 | 阿尔的代码屋 MiniMax-H3 NF4 视音频联合生成模型本地部署与调试 | 阿尔的代码屋 ShareX 联动 Antigravity 自动化记录与跨环境管道构建 | 阿尔的代码屋 国内搜索引擎收录实战:百度与头条搜索接入、无备案验证绕行与自动化推送 - 独立博客 SEO 与 GEO 03 | 阿尔的代码屋 技术博客工程化治理与 WebP 自动化质量门禁 - Hexo 博客建站与优化实战 05 | 阿尔的代码屋 Hexo NexT 静态资源本地自托管、KaTeX 公式渲染与移动端适配 - Hexo 博客建站与优化实战 04 | 排坑笔记 | 阿尔的代码屋 把 VS Code 打造成 Git 终极编辑器、Diff 与 Merge 利器 - Git 避坑与工作流 04 | 排坑笔记 | 阿尔的代码屋 告别架构图看不清:Hexo NexT 8.x 本地化集成 Fancybox 5 高清灯箱实战 | 开发日志 | 阿尔的代码屋 Hexo new 日期无法自动生成且出现 object Object 报错根治 | 排坑笔记 | 阿尔的代码屋 IndexNow 毫秒级主动推送与全站语义拓扑网格 - 独立博客 SEO 与 GEO 02 | 架构实战 | 阿尔的代码屋 从拦截 AI 爬虫到成为大模型答案源 - 独立博客 SEO 与 GEO 01 | 架构实战 | 阿尔的代码屋 Chrome 扩展开发与上架全流程实战避坑 - 开发技巧 | 阿尔的代码屋 VS Code 终端日志被截断?两项配置彻底解锁完整输出与会话持久化 | 排坑笔记 | 阿尔的代码屋 在 WSL2 环境下部署 Pixal3D 的从零实战与全流程排雷日志 | 阿尔的代码屋 在 Android Termux 环境下安装 Hermes Agent 的踩坑与完美解决实践 开发日志| 阿尔的代码屋 VS Code 连接 WSL 精确每 10 分钟掉线 排坑笔记 | 阿尔的代码屋 Android 模拟器代理联网与 No Internet WiFi 锁死排坑笔记 | 阿尔的代码屋 [object Object] Flutter 本地通知实现排坑实录 - Android inexactAllowWhileIdle 调度策略与测试方案全解析 | 阿尔的代码屋 typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋 Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋
基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 ...
Algieba · 2026-06-02 · via 阿尔的代码屋 | 全栈技术笔记

1. 业务场景与核心需求

运行环境:

  • 操作系统:Ubuntu 22.04
  • 开发工具:VS Code Remote-SSH
  • 容器环境:Rootless Docker (非 root 用户权限运行)
  • 业务场景:GPU 服务部署与模型调试

核心需求:

  1. 自动释放:开发者下班合上电脑(物理断网)后,经过一段缓冲时间(例如 10 分钟),系统需自动停止该用户运行的 Docker 容器。
  2. 状态保持:在缓冲期内若用户重新连接网络,容器资源需保持运行,重置倒计时。
  3. 多开支持:支持用户同时开启多个 VS Code 窗口或 SSH 终端,只有当所有连接均断开时才触发倒计时。
  4. 常驻白名单:提供快捷指令,允许用户在特定情况下(如长时间训练任务)关闭自动清理功能。

2. 初步方案与局限性分析

初步思路:依赖 .bash_logout
最初的设计是在用户的 ~/.bash_logout 文件中添加清理逻辑。即在脚本中设定 sleep 600 (10分钟),随后执行 docker stop

遇到的问题:物理断网不触发注销流程
在实际测试中发现,当在终端手动输入 exit 时,容器能按预期被清理。但当直接合上笔记本电脑物理断网时,脚本完全未执行。

原因剖析:
通过检查服务器的 /etc/ssh/sshd_config,发现未配置 SSH 心跳检测 (#ClientAliveInterval 0)。物理断网属于异常掉线,操作系统完全依赖底层的 TCP KeepAlive 机制来判定连接断开。这一过程通常耗时 15 到 30 分钟,超时后系统会直接强制销毁 sshd 进程。这种非“优雅退出”的机制导致 .bash_logout 中的逻辑被彻底跳过。


3. 方案演进:引入后台守护进程 (Daemon)

既然无法在“退出时”触发,方案需转向“持续监控”模式。我们决定在用户登录时,静默启动一个轻量级的 Bash 守护进程,通过轮询统计用户的 sshd 会话数量来判断在线状态。

遇到的问题:Rootless Docker 守护进程失联
编写好监控脚本并通过 nohup 投入后台运行后,发现虽然触发了超时清理逻辑,但 docker ps -qdocker stop 命令执行失败,系统日志提示无法连接到 Docker 守护进程。

原因剖析:环境变量丢失
在 Rootless Docker 模式下,普通用户终端能够正常使用 docker 命令,是因为用户通过 SSH 登录时,系统自动为其分配了如 XDG_RUNTIME_DIR 等关键环境变量,指向了 /run/user/<uid>/docker.sock
而通过 nohup 和脱机运行的后台守护进程,脱离了用户的 Login Shell 上下文,导致这些特定环境变量丢失。Docker 客户端退化到去寻找默认的系统级 /var/run/docker.sock,从而引发权限或找不到文件的错误。


4. 最终实现方案

明确了所有限制条件后,最终落地的方案由两部分组成:注入了环境变量的独立守护进程脚本,以及 ~/.bashrc 中的控制逻辑。

步骤一:创建守护进程脚本

在用户根目录下创建 ~/.gpu_cleanup_daemon.sh 脚本。
注意:脚本开头的环境变量可通过在正常终端中运行 env | grep -E "DOCKER|XDG" 获取,需将 1002 替换为实际的用户 UID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/bin/bash





export XDG_RUNTIME_DIR=/run/user/1002
export DOCKER_HOST=unix:///run/user/1002/docker.sock


RAW_TIME=${GPU_CLEANUP_GRACE_PERIOD:-10m}
case $RAW_TIME in
*m) GRACE_SEC=$((${RAW_TIME%m} * 60)) ;;
*h) GRACE_SEC=$((${RAW_TIME%h} * 3600)) ;;
*s) GRACE_SEC=${RAW_TIME%s} ;;
*) GRACE_SEC=$RAW_TIME ;;
esac

ELAPSED=0
CHECK_INTERVAL=10

while true; do

if [ -f "$HOME/.skip_gpu_cleanup" ]; then
ELAPSED=0
sleep $CHECK_INTERVAL
continue
fi


SESSION_COUNT=$(pgrep -u "${USER}" -x sshd | wc -l)

if [ "${SESSION_COUNT}" -gt 0 ]; then

ELAPSED=0
else

ELAPSED=$((ELAPSED + CHECK_INTERVAL))


if [ "$ELAPSED" -ge "$GRACE_SEC" ]; then
CONTAINER_IDS=$(docker ps -q)
if [ -n "${CONTAINER_IDS}" ]; then
docker stop ${CONTAINER_IDS}
fi


exit 0
fi
fi
sleep $CHECK_INTERVAL
done

赋予脚本执行权限:chmod +x ~/.gpu_cleanup_daemon.sh

步骤二:配置终端环境与快捷指令

~/.bashrc 尾部添加以下配置。除了静默启动守护进程外,还封装了开关指令供日常调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17





export GPU_CLEANUP_GRACE_PERIOD="10m"


alias gpu-keep-on="touch ~/.skip_gpu_cleanup && echo -e '\033[32m[Long-run Mode Enabled]\033[0m Your Docker containers will NOT be stopped upon disconnection.'"
alias gpu-keep-off="rm -f ~/.skip_gpu_cleanup && echo -e '\033[33m[Auto-Cleanup Mode Restored]\033[0m Your Docker containers will be automatically stopped after disconnection.'"
alias gpu-status="[ -f ~/.skip_gpu_cleanup ] && echo 'Current Mode: Long-running task (No cleanup)' || echo 'Current Mode: Auto-cleanup'"


if ! pgrep -u "${USER}" -f "gpu_cleanup_daemon.sh" > /dev/null; then
nohup bash ~/.gpu_cleanup_daemon.sh > /dev/null 2>&1 &
disown
fi

执行 source ~/.bashrc 使配置生效。


5. 总结与实践建议

  1. 实际资源释放延迟:由于 Ubuntu 默认依赖底层的 TCP 机制判断异常断线,实际的容器停止时间等于 TCP 释放延迟(约 15~30 分钟)+ 脚本缓冲时间(10 分钟)。这种物理延迟恰好提供了业务上的双重缓冲冗余。
  2. 数据安全性:采用 Rootless Docker 环境时,执行 docker ps -q 只能获取当前用户空间下的容器,因此直接进行 docker stop 操作是安全的,不存在误操作其他用户资源的风险。
  3. 资源占用极低:Bash 守护进程配合 10 秒级别的 sleep 轮询,对系统资源的消耗几乎为零。且完成清理任务后进程会自我销毁,避免了僵尸进程的累积。