这台机器同时跑着生产站点 jinghao.site。整套 devbox 与它并存,全程守着一条底线:宁可 devbox 自己撞内存墙,也绝不拖垮 nginx。核心机制只有两块基石 —— Docker + tmux —— 外加一个把它们缝起来的薄脚本。全部可读、可改、可接管。
命令里的 myserver-sg 就是这台机器(新加坡,8.222.155.72)。
| 开工:选/建项目,落进 tmux | ssh -t myserver-sg dev |
| 起 claude(跳过权限,在 tmux 里敲) | claude-go |
| 离开但不中断 | Ctrl-b 松手 d |
| 回来接着干 | ssh -t myserver-sg dev → 选项目 |
| 新建指定语言容器 | devbox up <proj> python|java|none |
| 列出所有项目 / 语言 | ssh myserver-sg devbox ls · … devbox langs |
| 停容器(留数据) | ssh myserver-sg devbox down <proj> |
| 删容器+卷(留代码) | ssh myserver-sg devbox nuke <proj> |
离开 tmux 用 detach(Ctrl-b d),绝不用 exit —— 会杀掉会话 / 打断 claude。
从冷笔记本到「容器里正在跑的 claude」,一条命令:
ssh -t myserver-sg dev # 弹菜单:选语言 + 项目名 → 落进 tmux → 敲 claude-go
派完活按 Ctrl-b d 离开(或直接合盖),claude 在容器里继续跑。回来再 ssh -t myserver-sg dev 选同一项目,接回原会话。
从外到里,每下一层多一层隔离。任何时候拿不准「我在第几层」,敲 hostname && whoami。
┌─ Layer 0 你的笔记本 (Windows / PowerShell)
│ │ ssh -t myserver-sg
│ ▼
├─ Layer 1 宿主机 root@阿里云ECS ← Docker 住这层;生产站点也在这
│ │ docker exec (devbox 替你做) 这层能 docker ps
│ ▼
├─ Layer 2 容器 dev@devbox-<proj> ← 隔离沙箱:独立文件系统 / 用户 / 内存封顶
│ │ tmux attach (devbox 替你做) 这层 docker ps 会 command not found
│ ▼
└─ Layer 3 tmux 会话 "main" ← 常驻层:断开不死,claude 跑在这
| 层 | 你是谁 | 进入方式 | 退出方式 |
|---|---|---|---|
| 1 宿主机 | root | ssh myserver-sg | exit → 回笔记本 |
| 2 容器 | dev | docker exec -it -u dev devbox-<proj> bash | exit → 回宿主机 |
| 3 tmux | dev | tmux attach -t main | Ctrl-b d(detach,别 exit) |
| 层 | 是什么 | 它挡住的杀手 |
|---|---|---|
| L1 | tmux server 跑在容器内 | SSH 断开只是 detach;claude 收不到 SIGHUP,继续跑 |
| L2 | 容器 PID1 = sleep infinity | claude 崩了/退了,容器不跟着死 |
| L3 | ~/.claude 挂具名卷 | 容器重建,会话历史与登录态照样在 |
都 FROM devbox-base,所以 tmux / git / claude-code / claude-go / dev 用户全部共享继承。
| 镜像 | 语言 | 内含 |
|---|---|---|
devbox-base:latest | 无语言 / 仅 claude | node:22-slim · tmux · git · claude-code 2.1.204 · ripgrep/fd/jq · claude-go |
devbox-python:latest | Python | base + Python 3.11.2 · pip · venv · build-essential |
devbox-java:latest | Java | base + Temurin JDK 21.0.11 (LTS) · Maven 3.9.9 |
语言在创建容器那一刻定死(devbox up <proj> python);之后 up/attach 都不用再写语言。devbox langs 看有哪些语言可用。
| 东西 | 宿主机 | 容器内 | 说明 |
|---|---|---|---|
| 代码 | /srv/dev/<proj> | /workspaces/<proj> | 同一份文件(bind mount),属主 1000:1000 |
| claude 状态 | 具名卷 devbox-claude-<proj> | /home/dev/.claude | 会话历史 / 登录态(L3) |
| MiniMax 密钥 | /etc/devbox/minimax.env | 运行时注入为环境变量 | chmod 600 · root:root · 不烘焙进镜像 |
认证走 MiniMax(非 Claude 官方账号):ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic,模型 MiniMax-M3。每次 docker run --env-file 注入,容器里没有任何 Anthropic 凭证。
| 护栏 | 设置 | 为什么 |
|---|---|---|
| 内存硬顶 | --memory=1g --memory-swap=3g | 容器最多 1g 常驻 + 2g swap;撞墙也伤不到 nginx |
| OOM 优先级 | 容器 -500 · nginx -900 | 内存不够时,先杀杂项 > 再杀 agent > 最后才 nginx |
| 端口 | 绝不 publish 到 0.0.0.0 | dev server 一律走 ssh -L 隧道(见 §13) |
| 容器存活 | --restart unless-stopped | 机器重启后容器自启(已实测跨 resize 生效) |
| 用途 | 服务器上 | 本地源 |
|---|---|---|
| 生命周期脚本 | /usr/local/bin/devbox | D:\code\private_site\devbox\devbox |
| 交互式前台 | /usr/local/bin/dev | D:\code\private_site\devbox\dev |
| 一键起 claude | 镜像内 /usr/local/bin/claude-go | (写在 base Dockerfile 里) |
| base 镜像 | /srv/devbox/base/Dockerfile | D:\code\private_site\devbox\Dockerfile |
| 语言镜像 | /srv/devbox/langs/Dockerfile.{python,java} | D:\code\private_site\devbox\langs\ |
| 密钥 | /etc/devbox/minimax.env | (只在服务器,不进版本库) |
笔记本 ~/.ssh/config 里,本文所有命令的 myserver-sg = root@8.222.155.72(新加坡这台,装了 devbox)。另有 myserver-bj = 8.141.92.77,是另一台机器、没装 devbox,别搞混。密钥 id_ed25519;VS Code / 命令行 ssh 读同一份。
日常就用这一条,弹菜单:
ssh -t myserver-sg dev
════════════ devbox 交互启动 ════════════
现有项目:
1) myapp [python, running]
2) svc [java, exited]
n) 新建一个
q) 退出
选择(编号/n/q): 1
→ 接入 myapp …
[落进容器里的 tmux "main"]
没有项目时直接进新建流程(选语言 → 输项目名 → 建好并接入)。-t 不能漏 —— tmux 需要一个真终端。
知道自己要什么时,直接调,比菜单快。都能 ssh myserver-sg devbox … 远程执行(要落进 tmux 的加 -t)。
| 命令 | 做什么 |
|---|---|
devbox up <proj> [python|java|none] | 起容器 + 建 tmux main(已存在则不动)。语言只在首次创建时用 |
devbox attach <proj> | 接进 tmux;断开=detach,容器内进程继续跑(配 ssh -t) |
devbox ls | 列所有项目:语言 / 状态 / 内存 / tmux 会话 |
devbox langs | 列可用语言镜像 |
devbox shell <proj> | 开个纯 bash(绕过 tmux,调试用) |
devbox down <proj> | 停容器,保留卷(历史 / 登录态都在) |
devbox nuke <proj> | 删容器 + 删卷;代码目录 /srv/dev/<proj> 不动 |
接进 tmux 后,一句话以 skip-permission 起 claude,不用手敲长参数:
claude-go
| 你在哪跑 | 它怎么做 |
|---|---|
| 已在 tmux 里(attach 之后,最常见) | 当前窗口直接起 claude --dangerously-skip-permissions;claude 退出后回到 shell,会话不散 |
不在 tmux 里(如 devbox shell 进去) | 自动建 main + 在里面起 claude + 接入 |
万一手滑把 main 会话 exit 没了 —— 从容器里(非 tmux)敲一句 claude-go,就能重建 main 并起 claude,一步到位。
| 想干什么 | 敲什么 | 结果 |
|---|---|---|
| 进去(接上 claude) | tmux attach -t main | — |
| 走人(留着它继续跑) | Ctrl-b 松手 d | detach,claude 继续跑 ✓ |
离开 tmux 永远用 detach(Ctrl-b d),绝不用 exit。exit 会杀掉当前 shell —— 空会话会被带走,有 claude 会被打断。往回翻历史用鼠标滚轮(已开 mouse on)。
# 早上,开工
ssh -t myserver-sg dev # 菜单选 myapp(或新建)→ 落进 main
claude-go # 起 claude,派活
# … 给 claude 交代任务 …
Ctrl-b d # 合盖走人;claude 在容器里继续跑
# 中午回来,换了个地方
ssh -t myserver-sg dev # 选 myapp → 回到 main → claude 还在原地,接着看
# 晚上,收工但想让它夜里接着跑
Ctrl-b d # 只 detach,别关容器
# (若配了完成通知,跑完会推手机)
# 某天这个项目不要了
ssh myserver-sg devbox nuke myapp # 删容器+卷;/srv/dev/myapp 代码仍在
容器内 /workspaces/<proj> 就是宿主 /srv/dev/<proj>,同一份文件。直接在里面 git clone:
cd /workspaces/myapp && git clone <你的仓库> .
比如容器里跑了个服务在 :5173,想在笔记本浏览器看:先把宿主的端口转出来,再从宿主转进容器。最简单是让服务监听容器的 0.0.0.0,然后:
# 笔记本上另开一个终端
ssh -L 5173:localhost:5173 myserver-sg
# 然后在宿主再 -L 到容器,或直接用 docker 的端口——但本方案约定:
# 服务绑容器 0.0.0.0:5173,宿主用 docker exec 转发/或临时 -p 仅供本机,
# 对外一律只经 ssh 隧道,绝不 0.0.0.0 直接暴露。
容器永不 -p 到公网。要看 dev server,用 ssh -L 把流量隧道回笔记本。需要时告诉我,给你把某个项目的隧道命令固化下来。
三步,跟你说一声我就做:
langs/Dockerfile.python 的模式写一个 Dockerfile.go(FROM devbox-base + 装 Go)docker build -t devbox-go:latest -f Dockerfile.go .(一次)devbox 脚本的 image_for_lang() 里加一行 go) echo 'devbox-go:latest';;之后 devbox up proj go 就能用。dev 菜单里也加一个选项即可。
| 症状 | 怎么办 |
|---|---|
手滑 exit 把 main 弄没了 | 容器里敲 claude-go(非 tmux 分支会重建 main 并起 claude);或宿主 devbox up <proj> 重建空会话 |
| 合盖回来连不上 | ssh -t myserver-sg dev 选那个项目即可;容器 --restart 会自启 |
docker ps 看不到容器 | 你多半在容器终端里敲的 —— 容器内没 docker。请在宿主 root 敲 |
| 重连后窗口缩成一小格 | 正常:断连留了孤儿 client。devbox attach 已自动踢掉它,重连即好 |
| 容器停了想恢复(保数据) | devbox up <proj> 会 docker start 它,卷和会话都在 |
| 确认站点没被影响 | 宿主 curl -s -o /dev/null -w '%{http_code}' https://jinghao.site/ 应为 200 |