
1. 为什么要把 ttyd 塞进 Docker 再用 Nginx 兜一层如果你只是在自己电脑上跑ttyd -p 8080 bash浏览器打开就能用确实没必要折腾容器和反向代理。但一旦这个 Web 终端要给别人用、要长期挂着、要放在有公网入口的机器上裸跑 ttyd 的问题就全冒出来了进程挂了没人拉起来、端口直接暴露没有认证、多个 AI 工具各自配一套 Key 和 Base URL 改到崩溃。我这次的目标很具体在一台云主机上用 Docker 跑 ttyd前面用 Nginx 做反向代理和 Basic Auth终端里跑的是 Claude Code、Codex 这类编码 Agent而这些工具统一走 TaoToken 的 API 通道Key 只维护一份。这样带来的直接好处是终端服务本身稳定可重启AI 工具的接入配置也不再散落在每台机器的每个配置文件里。ttyd 是什么一句话说清它把设备上的终端会话通过 WebSocket 同步到浏览器你打开网页就等于坐在那台机器的 shell 前面。适合谁适合需要给非开发同学一个打开网页就能敲命令入口的人也适合自己想把家里那台常开机器变成随身终端的人。Docker 负责把 ttyd 和 Nginx 的依赖封死Nginx 负责认证和 WebSocket 转发TaoToken 负责把多个 AI 工具的 Key 收敛成一套。下面按先跑起来、再加固、再接 AI 通道、最后验证排障的顺序走每一步都给可复制的命令和配置。2. TaoToken 前置先把统一 Key 和通道准备好在动 ttyd 之前先把 AI 工具要用的那套凭证准备好否则终端跑起来还得回头改配置。TaoToken 在这里扮演的角色是统一入口你拿到一个 Key配一个 Base URLClaude Code、Codex、以及各种兼容 OpenAI 协议的工具都能指向它不用每个工具去申请各自的额度。第一步注册并登录后到控制台创建 API Key。地址是 https://taotoken.net/api-keys 创建完把 Key 复制出来形如sk-xxxx只显示一次丢了就重建。第二步确认你要用的接入地址。TaoToken 的 API 根地址是 https://taotoken.net/api 注意这个地址后面不带任何查询参数。不同工具对路径的拼法不一样Claude Code 走 Anthropic 兼容通道OpenAI 系工具走/v1兼容通道具体以接入文档为准 https://taotoken.net/doc 。第三步想先验证模型通不通不用装任何工具直接在网页里对话测试即可 https://taotoken.net/model-chat 。这一步能快速排除Key 是不是有效模型名对不对这类问题省得在终端里反复试。如果你打算长期跑编码 Agent、需要更稳定的额度和并发可以了解 Coding Plan https://taotoken.net/coding-plan 。日常零散测试用按量 Key 就够了。注意Key 属于敏感凭证不要写进会提交到 Git 的配置文件里。下面所有配置我都用环境变量或单独的.env文件承载容器启动时注入。3. 可复制配置Docker ttyd Nginx 全套骨架这一节是主体配置能直接抄。整体结构是ttyd 监听一个 Unix socket不占 TCP 端口Nginx 容器通过挂载同一个 socket 文件反向代理对外只暴露 Nginx 的端口并加 Basic Auth。3.1 目录结构和 docker-compose.yml先建目录把配置和凭证分开放mkdir -p /opt/webterm/{nginx,secrets} cd /opt/webtermdocker-compose.yml内容如下两个服务ttyd 和 nginx。ttyd 用官方镜像挂载 socket 目录nginx 挂载配置、密码文件和同一个 socket 目录。version: 3.8 services: ttyd: image: tsl0922/ttyd:latest container_name: webterm-ttyd restart: always command: ttyd -i /sock/ttyd.sock -H X-WEBAUTH-USER -t fontSize14 -t theme{background:#1e1e1e} bash -l volumes: - ./sock:/sock - /root:/root environment: - TZAsia/Shanghai nginx: image: nginx:1.25-alpine container_name: webterm-nginx restart: always depends_on: - ttyd ports: - 0.0.0.0:8080:80 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./secrets/.htpasswd:/etc/nginx/.htpasswd:ro - ./sock:/sock environment: - TZAsia/Shanghai几个参数说明一下。-i /sock/ttyd.sock让 ttyd 监听 Unix socket 而不是 TCP 端口这样它就不直接对外了。-H X-WEBAUTH-USER是让 ttyd 信任反向代理传来的认证头认证交给 Nginx 做。-t fontSize14是给前端传客户端选项调字号和主题。bash -l是登录 shell保证环境变量加载完整。3.2 生成 Basic Auth 密码文件用 httpd 镜像里的 htpasswd 生成账号假设叫admindocker run --rm httpd:alpine htpasswd -nb admin 你的强密码 /opt/webterm/secrets/.htpasswd cat /opt/webterm/secrets/.htpasswd输出形如admin:$apr1$xxxx$yyyy确认文件非空即可。3.3 Nginx 反向代理配置nginx/nginx.conf的关键是 WebSocket 升级头和 socket 转发user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; map $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 80; server_name _; location / { auth_basic Web Terminal; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://unix:/sock/ttyd.sock:/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-WEBAUTH-USER $remote_user; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } }map $http_upgrade $connection_upgrade这段是 WebSocket 能不能连上的关键缺了它终端会一直转圈。proxy_read_timeout 3600s是防止长时间不操作被断开。X-WEBAUTH-USER $remote_user把 Nginx 认证出来的用户名透传给 ttyd。3.4 启动并确认容器状态cd /opt/webterm docker compose up -d docker compose ps两个容器都应该是Up状态。如果 ttyd 反复重启先看日志docker compose logs --tail50 ttyd4. 验证请求从 curl 到浏览器再到 AI 工具接入服务起来后分三层验证HTTP 认证层、WebSocket 层、AI 工具接入层。4.1 curl 验证认证和响应先不带凭证应该返回 401curl -i http://127.0.0.1:8080/预期看到HTTP/1.1 401 Unauthorized和WWW-Authenticate: Basic realmWeb Terminal。再带上凭证curl -i -u admin:你的强密码 http://127.0.0.1:8080/预期返回200 OK和一段 HTMLttyd 的前端页面。这一步过了说明 Nginx 认证和 socket 转发都通了。4.2 浏览器验证 WebSocket浏览器打开http://你的服务器IP:8080弹出账号密码框输入后应该直接进入终端。如果页面出来了但终端黑屏或一直Connecting八成是 WebSocket 头没配对回到 3.3 检查Upgrade和Connection两行。4.3 在终端里接入 TaoToken 统一 Key现在终端能用了接下来让里面的 AI 工具走 TaoToken。以 Claude Code 为例它的配置在~/.claude/settings.json骨架如下{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的TaoTokenKey } }如果你用的是走 OpenAI 兼容协议的工具配置通常长这样以config.toml为例model gpt-4o-mini base_url https://taotoken.net/api/v1 api_key sk-你的TaoTokenKey把 Key 换成你在 2 节里创建的那把。改完在终端里直接跑一次工具比如claude或对应的 CLI能正常对话就说明通道打通了。想先确认模型可用性也可以回到 https://taotoken.net/model-chat 对照测试。提示如果你在多个工具里都要用把ANTHROPIC_BASE_URL和 Key 抽到 shell 的~/.bashrc里 export比每个工具单独配更省事。但注意别把带 Key 的文件提交到仓库。5. 本篇常见错排查配置抄完跑不起来基本集中在这几类按顺序排查效率最高。502 Bad GatewayNginx 连不上 ttyd 的 socket。先确认两个容器挂载的是同一个宿主目录./sock再确认 ttyd 真的在监听docker compose exec ttyd ls -l /sock/。如果 socket 文件不存在看 ttyd 日志里有没有权限报错。另外 Nginx 容器里的 nginx 用户要能读写这个 socket必要时把 ttyd 的-u/-g调成和 Nginx 一致。401 一直弹窗密码文件路径或格式不对。确认secrets/.htpasswd挂载到了/etc/nginx/.htpasswd且文件里是用户名:哈希格式。用docker compose exec nginx cat /etc/nginx/.htpasswd看一眼。终端连上但输入没反应WebSocket 建立了但数据没转发。检查proxy_http_version 1.1和Connection $connection_upgrade是否都在map块是否写在http块里而不是server里。AI 工具报 401 或 model not foundKey 错了或模型名不对。先用curl直接打 TaoToken 的接口确认 Key 有效再检查工具配置里的base_url有没有多写或少写/v1。Claude Code 走 Anthropic 通道时不要加/v1OpenAI 兼容工具才需要。容器重启后 socket 文件残留导致启动失败ttyd 启动时如果发现旧 socket 文件会报错。可以在 compose 里加个启动前清理或者直接docker compose down docker compose up -d重建。日志在哪看Nginx 的访问和错误日志在容器内/var/log/nginx/用docker compose logs nginx看实时输出ttyd 的日志同样用docker compose logs ttyd。排查 WebSocket 问题时Nginx 的error.log里会有upstream相关记录很有用。6. 把 Key 收敛成一套之后整套跑通后你得到的是这样一个东西一个带认证的浏览器终端进程由 Docker 托管自动重启里面的 AI 工具全部指向 TaoToken 的统一通道Key 只维护一份。后面要加新工具改的是工具自己的配置文件不用再动 ttyd 和 Nginx。如果你主要是在终端里长期跑编码 Agent建议把 Key 和 Base URL 的管理再规范一点可以参考 Coding Plan 的额度组织方式 https://taotoken.net/coding-plan 。接入过程中遇到路径拼法、认证头这类细节接入文档里有分工具的说明 https://taotoken.net/doc 。需要新建或轮换 Key 时到控制台操作 https://taotoken.net/api-keys 。想快速验证某个模型是否可用直接用模型对话页面测一把最省事 https://taotoken.net/model-chat 。最后留一个我踩过的坑Nginx 的proxy_read_timeout默认 60 秒终端挂机一会儿就被断改成 3600s 之后长时间不操作也不会掉线。这个参数在调试阶段很容易被忽略但它是稳定两个字里很实在的一部分。