v1.1.0: Alpine 轻量级 Docker 开发环境 + 敏感凭证清理

This commit is contained in:
lxy
2026-07-27 13:59:18 +08:00
parent feb76081a0
commit ccca0fa62b
51 changed files with 6125 additions and 993 deletions
+49
View File
@@ -0,0 +1,49 @@
# ==================== 版本控制 ====================
.git
.gitignore
# ==================== Claude 配置 ====================
.claude/
# ==================== 文档 ====================
docs/
ISSUES.md
README.md
PACKAGES.md
# ==================== 归档文件(已不需要) ====================
_archive/
# ==================== 数据/运行时 ====================
data/
# ==================== 部署辅助文件(不在镜像中) ====================
auth-proxy.js
nginx-map-patch.sh
wk.1216.conf
# ==================== 备用编排文件 ====================
docker-compose-alpine.yml
entrypoint-test.sh
# ==================== 下载脚本 ====================
download-packages.sh
download-packages.ps1
# ==================== 导出镜像(~100MB ====================
*.tar.gz
# ==================== Windows 伪文件 ====================
;C
.DS_Store
Thumbs.db
# ==================== packages 精确包含 ====================
# 仅保留 Dockerfile COPY packages/ 需要的文件:
# node-v24.14.1-linux-x64-musl.tar.gz
# *claude-code*.tgz
# *coding-helper*.tgz
# 排除未使用的大包:
packages/go1.26.1.linux-amd64.tar.gz
packages/rust-1.94.1-x86_64-unknown-linux-musl.tar.xz
packages/openclaw-*.tgz
+32 -6
View File
@@ -1,17 +1,43 @@
# 软件包(太大,不纳入版本控制) # 运行时数据
data/
.env
.env.*
# 归档文件(历史版本)
_archive/
# 构建产物
*.tar.gz
# 离线包(121MB,不入版本库)
packages/ packages/
# Docker 数据 # Claude 本地配置(含 session token
docker-data/ .claude/
# 敏感信息(密钥/密码/服务器IP/内部拓扑)
instances/flux/.env
instances/registry.yaml
dev-envs/flux/databases.yaml
dev-envs/flux/servers.yaml
# 代理工具运行时配置(含真实密码/IP,被 compose 挂载使用,不入库;模板见 *.toml.example
dev-envs/flux/mysql-proxy.toml
dev-envs/flux/redis-proxy.toml
dev-envs/flux/ssh-proxy.toml
# IDE # IDE
.idea/ .idea/
.vscode/ *.iml
*.swp
# 系统文件 # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
*.swp
*.swo
# 临时文件
tmp/
temp-images/
# 日志 # 日志
*.log *.log
+41 -90
View File
@@ -1,107 +1,58 @@
# WorkPod - 全栈开发环境容器 (本地包构建版) # WorkPod Alpine - 轻量级开发环境(多阶段构建)
# 包含:Ubuntu + Python + Go + Rust + Node + MySQL + Redis + Claude Code 2.1.79 + OpenClaw
FROM ubuntu:22.04 # ============ 阶段1: 构建阶段 ============
FROM alpine:3.23 AS builder
ENV DEBIAN_FRONTEND=noninteractive RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
ENV TZ=Asia/Shanghai apk update && apk add --no-cache xz libstdc++
# ============ 配置阿里云镜像源 ============
RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list
# ============ 基础工具 (合并安装减少层数) ============
RUN apt-get update && apt-get install -y --no-install-recommends \
# 基础工具
curl wget git vim nano less tree jq \
# 系统工具
procps htop net-tools iputils-ping lsof \
# 压缩工具
zip unzip xz-utils \
# 构建工具
build-essential pkg-config \
# SSL/加密
ca-certificates gnupg \
# SSH + PTY 支持
openssh-server locales \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \
&& mkdir -p /var/run/sshd \
&& echo 'root:workpod123' | chpasswd \
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# 中文和 UTF-8 支持
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
# ============ Python ============
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \
&& ln -sf /usr/bin/python3 /usr/bin/python
# ============ 复制本地包到镜像 ============
COPY packages/ /tmp/packages/ COPY packages/ /tmp/packages/
# ============ Node.js v24.14.0 (Krypton LTS) ============ # 安装 Node.js + npm 全局包
RUN cd /tmp/packages \ RUN mkdir -p /opt/node \
&& tar -xf node-v24.14.0-linux-x64.tar.xz -C /usr/local --strip-components=1 \
&& npm install -g npm@latest pnpm yarn \
&& npm cache clean --force
# ============ Go 1.26.1 (最新版) ============
ENV GOPATH=/root/go
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
RUN cd /tmp/packages \
&& tar -xzf go1.26.1.linux-amd64.tar.gz -C /usr/local
# ============ Rust 1.94.0 ============
ENV CARGO_HOME=/root/.cargo RUSTUP_HOME=/root/.rustup
ENV PATH=$CARGO_HOME/bin:$PATH
RUN mkdir -p /opt/rust $CARGO_HOME/bin \
&& cd /tmp/packages \ && cd /tmp/packages \
&& tar -xf rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz -C /opt/rust \ && tar -xzf node-v24.14.1-linux-x64-musl.tar.gz -C /opt/node --strip-components=1 \
&& ln -sf /opt/rust/rust-1.94.0-x86_64-unknown-linux-gnu/bin/* $CARGO_HOME/bin/ && export PATH="/opt/node/bin:$PATH" \
&& npm install -g npm@10 \
# ============ Claude Code + OpenClaw (离线安装) ============ && npm config set registry https://registry.npmmirror.com \
RUN cd /tmp/packages \ && npm install -g "@anthropic-ai/claude-code@latest" \
&& npm install -g claude-code-2.1.79.tgz \ && npm install -g "@z_ai/coding-helper@latest" \
&& npm install -g openclaw-2026.3.13.tgz \
&& npm cache clean --force && npm cache clean --force
# ============ MySQL 8.0 (手动启动) ============ # ============ 阶段2: 运行阶段 ============
RUN apt-get update && apt-get install -y --no-install-recommends \ FROM alpine:3.23
mysql-server \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* \
&& mkdir -p /var/run/mysqld \
&& chown mysql:mysql /var/run/mysqld
# ============ Redis (手动启动) ============ ENV TZ=Asia/Shanghai LANG=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
redis-server \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# ============ 更多常用工具 ============ # 基础工具 + SSH + ttyd
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
# 网络工具 apk update && apk add --no-cache \
netcat-openbsd \ curl git ca-certificates openssh-server openrc \
# 文本处理 bash ttyd tmux libstdc++ \
ripgrep fd-find \ && mkdir -p /run/openrc && touch /run/openrc/softlevel \
# 进程管理 && ssh-keygen -A \
tmux \ && echo "root:${ROOT_PASSWORD:-workpod123}" | chpasswd \
# 其他 && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
bash-completion \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# ============ 清理临时包 ============ # 从构建阶段复制 Node.js 和全局包(仅产物,无缓存)
RUN rm -rf /tmp/packages COPY --from=builder /opt/node /usr/local
# 固化 npm 镜像源(运行时也需要,builder 的 npmrc 未被 COPY 包含)
RUN npm config set registry https://registry.npmmirror.com
# ============ 工作目录 ============
WORKDIR /workspace WORKDIR /workspace
# ============ 启动脚本 ============ COPY --chmod=755 entrypoint.sh /entrypoint.sh
COPY entrypoint.sh /entrypoint.sh COPY --chmod=755 ttyd-session.sh /opt/ttyd-session.sh
RUN chmod +x /entrypoint.sh
EXPOSE 22 80 443 3306 6379 LABEL maintainer="workpod" \
org.opencontainers.image.title="WorkPod Alpine" \
org.opencontainers.image.description="Lightweight development environment (Alpine)" \
org.opencontainers.image.version="1.1.0"
EXPOSE 22 7681
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\b' || exit 1
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
+13
View File
@@ -0,0 +1,13 @@
# ws-flux-dev - Flux 金融线索管理 (Java + Node 全栈)
FROM workpod-alpine:latest
LABEL org.opencontainers.image.title="ws-flux-dev" \
org.opencontainers.image.description="Flux 全栈开发环境 (Java 17 + Node 24)" \
org.opencontainers.image.version="1.0.0"
# JDK 17 通过 docker-compose volume 从宿主机挂载 (BellSoft Liberica musl)
# JAVA_HOME 由 docker-compose environment 设置
EXPOSE 22 7681
ENTRYPOINT ["/entrypoint.sh"]
-125
View File
@@ -1,125 +0,0 @@
# Dev Box 软件包清单
> 最后更新:2026-03-19
---
## 软件包列表
| 软件 | 版本 | 文件名 | 大小 | 国内镜像 |
|------|------|--------|------|---------|
| Node.js | v24.14.0 | node-v24.14.0-linux-x64.tar.xz | ~30 MB | npmmirror |
| Go | 1.26.1 | go1.26.1.linux-amd64.tar.gz | ~64 MB | golang.google.cn |
| Rust | 1.94.0 | rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz | ~183 MB | USTC |
| Claude Code | 2.1.79 | claude-code-2.1.79.tgz | ~20 MB | npmmirror |
| OpenClaw | 2026.3.13 | openclaw-2026.3.13.tgz | ~28 MB | npmmirror |
**总大小**:约 325 MB
---
## 国内镜像源
### Node.js (淘宝镜像)
```bash
# 下载地址
https://npmmirror.com/mirrors/node/v24.14.0/node-v24.14.0-linux-x64.tar.xz
# 备用:清华镜像
https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/v24.14.0/node-v24.14.0-linux-x64.tar.xz
```
### Go (官方中国镜像)
```bash
# 下载地址
https://golang.google.cn/dl/go1.26.1.linux-amd64.tar.gz
# 备用:阿里云镜像
https://mirrors.aliyun.com/golang/go1.26.1.linux-amd64.tar.gz
```
### Rust (中科大镜像)
```bash
# 下载地址
https://mirrors.ustc.edu.cn/rust-static/dist/rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz
# 备用:清华镜像
https://mirrors.tuna.tsinghua.edu.cn/rustup/dist/rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz
```
### npm 包 (淘宝镜像)
```bash
# Claude Code
npm pack @anthropic-ai/claude-code@2.1.78 --registry=https://registry.npmmirror.com
# OpenClaw
npm pack openclaw@2026.3.13 --registry=https://registry.npmmirror.com
```
---
## 下载方式
### Windows (PowerShell)
```powershell
cd D:\dev-box
.\download-packages.ps1
```
### Linux/macOS (Bash)
```bash
cd /path/to/dev-box
chmod +x download-packages.sh
./download-packages.sh
```
### 手动下载
```bash
# Node.js
curl -LO https://npmmirror.com/mirrors/node/v24.14.0/node-v24.14.0-linux-x64.tar.xz
# Go
curl -LO https://golang.google.cn/dl/go1.26.1.linux-amd64.tar.gz
# Rust
curl -LO https://mirrors.ustc.edu.cn/rust-static/dist/rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz
```
---
## 版本更新
如需更新软件版本,修改以下位置:
1. **下载脚本** - `download-packages.ps1` / `download-packages.sh` 中的版本号
2. **Dockerfile** - 对应的文件名和注释
### 查看最新版本
| 软件 | 查询地址 |
|------|---------|
| Node.js | https://npmmirror.com/mirrors/node/ |
| Go | https://golang.google.cn/dl/ |
| Rust | https://mirrors.ustc.edu.cn/rust-static/dist/ |
| Claude Code | `npm view @anthropic-ai/claude-code versions` |
| OpenClaw | `npm view openclaw versions` |
---
## 目录结构
```
packages/
├── node-v24.14.0-linux-x64.tar.xz # Node.js
├── go1.26.1.linux-amd64.tar.gz # Go
├── rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz # Rust
├── claude-code-2.1.78.tgz # Claude Code
└── openclaw-2026.3.13.tgz # OpenClaw
```
+47 -118
View File
@@ -1,138 +1,67 @@
# WorkPod 开发环境容器 # WorkPod Alpine
全栈开发环境,包含 Ubuntu + Python + Go + Rust + Node + MySQL + Redis + Claude Code + OpenClaw 轻量级 Docker 开发环境容器 — 基于 Alpine Linux 3.23,镜像仅 ~436MB。
## 核心特性
| 特性 | 说明 |
|------|------|
| **镜像大小** | ~436MBUbuntu 版的 1/6 |
| **基础镜像** | alpine:3.23 (~7MB) |
| **运行时内存** | 80-120MB 基线 + 应用 |
| **启动时间** | ~2.5 秒 |
| **Web 终端** | ttyd + tmux 会话管理 |
| **AI 工具** | Claude Code + coding-helper |
## 快速开始 ## 快速开始
```bash ```bash
# 构建镜像 # 构建基础镜像
cd E:/wk-lab/workpod docker build -t workpod-alpine:latest .
docker build -t workpod:latest .
# 启动容器 # 启动基础实例
docker-compose up -d docker compose up -d
# 查看日志 # 访问
docker logs -f workpod # Web: http://localhost:7681 (用户 jc / 密码 1234567)
# SSH: ssh root@localhost -p 2222 (密码 workpod123)
# 进入容器
docker exec -it workpod bash
``` ```
## SSH 连接 ## 项目实例部署
```bash ```bash
ssh root@localhost -p 2222 # Flux 项目(含 JDK/Maven 挂载)
# 密码: workpod123 docker compose -f docker-compose.flux.yml up -d
# 备用编排
docker compose -f docker-compose-alpine.yml up -d
``` ```
## 端口映射 ## 端口规划
| 服务 | 宿主机端口 | 容器端口 | | 实例 | SSH | Web 终端 | 用途 |
|------|-----------|---------| |------|-----|---------|------|
| SSH | 2222 | 22 | | workpod-alpine (基础) | 2222 | 7681 | 通用开发 |
| HTTP | 8080 | 80 | | ws-flux-dev | 2201 | 7701 | Flux 项目 |
| HTTPS | 8443 | 443 |
| MySQL | 13306 | 3306 |
| Redis | 16379 | 6379 |
## 启动数据库服务 ## 目录结构
```bash
# MySQL
docker exec workpod bash -c "mysqld --user=mysql --datadir=/var/lib/mysql &"
# Redis
docker exec workpod redis-server --daemonize yes
```
## 已安装工具
| 工具 | 版本 | 说明 |
|------|------|------|
| Python | 3.10 | pip, venv |
| Node.js | 24.14.0 | npm, pnpm, yarn |
| Go | 1.26.1 | |
| Rust | 1.94.0 | cargo |
| MySQL | 8.0 | 手动启动 |
| Redis | 6.0 | 手动启动 |
| Claude Code | 2.1.79 | AI 编程工具 |
| OpenClaw | 2026.3.13 | AI 网关 |
## 镜像导出/导入
### 导出镜像
```bash
# 导出为 tar 文件
docker save workpod:latest | gzip > workpod.tar.gz
# 或不压缩
docker save workpod:latest -o workpod.tar
```
### 导入镜像
```bash
# 从 tar.gz 导入
docker load < workpod.tar.gz
# 或从 tar 导入
docker load -i workpod.tar
```
### 迁移到服务器
```bash
# 1. 本机导出
docker save workpod:latest | gzip > workpod.tar.gz
# 2. 传输到服务器
scp workpod.tar.gz user@server:/path/
# 3. 服务器导入
ssh user@server
docker load < /path/workpod.tar.gz
# 4. 复制 docker-compose.yml 到服务器并启动
docker-compose up -d
```
## 数据持久化
数据存储在 `E:/docker-data/workpod/` 目录:
- `mysql/` - MySQL 数据
- `redis/` - Redis 数据
- `workspace/` - 工作空间
## 配置文件
``` ```
E:/wk-lab/workpod/ workpod/
├── Dockerfile # 镜像构建配置 ├── Dockerfile # 基础镜像(多阶段构建)
├── docker-compose.yml # 容器编排配置 ├── Dockerfile.flux # Flux 扩展镜像
├── entrypoint.sh # 入口脚本 ├── entrypoint.sh # 容器入口(PATH 自检 + 服务启动)
├── download-packages.ps1 # 软件包下载脚本 ├── ttyd-session.sh # tmux 会话管理
├── PACKAGES.md # 软件包清单 ├── auth-proxy.js # 认证代理(可选)
├── SPECS.md # 技术规格说明书 ├── download-packages.sh # 离线包下载
── README.md # 说明文档 ── docker-compose*.yml # 编排文件
├── dev-envs/ # 开发环境配置模板
├── instances/ # 实例编排(模板化部署)
├── config/ # 配置文件
├── docs/ # 文档
└── SPECS.md # 技术规格(完整版)
``` ```
## 常用命令 ## 技术规格
```bash 详见 [SPECS.md](./SPECS.md)
# 停止容器
docker-compose down
# 重启容器
docker-compose restart
# 查看容器状态
docker ps --filter name=workpod
# 进入容器执行命令
docker exec -it workpod bash
# 查看容器资源使用
docker stats workpod
```
-131
View File
@@ -1,131 +0,0 @@
# Dev Box 代码审查与优化报告
> 审查日期:2026-03-19
> 审查范围:D:/dev-box 项目
---
## 1. 审查概述
对 Dev Box 全栈开发环境容器进行代码审查,发现并修复了多个问题。
### 审查文件
| 文件 | 行数 | 状态 |
|------|------|------|
| Dockerfile | 110 → 107 | 已优化 |
| docker-compose.yml | 56 | 无问题 |
| entrypoint.sh | 31 → 30 | 已优化 |
---
## 2. 问题修复清单
### 2.1 必须修复 (3 项) ✅
| # | 位置 | 问题描述 | 修复方案 |
|---|------|---------|---------|
| 1 | Dockerfile:53 | 删除不存在的文件 `go1.24.3.linux-amd64.tar.gz` | 移除无效命令 |
| 2 | Dockerfile:59 | `/opt/rust` 目录未创建 | 添加 `mkdir -p /opt/rust $CARGO_HOME/bin` |
| 3 | Dockerfile:61 | `|| echo "Rust 解压完成"` 错误处理无效 | 移除,让构建失败时可见真实错误 |
### 2.2 建议改进 (3 项) ✅
| # | 位置 | 问题描述 | 修复方案 |
|---|------|---------|---------|
| 1 | Dockerfile:84 | `curl wget` 重复安装(已在第 16 行安装) | 移除重复项 |
| 2 | Dockerfile:105 | PATH 环境变量重复定义(第 50 行已定义) | 删除重复定义 |
| 3 | Dockerfile:98-101 | SSH 配置单独一层,可合并 | 合并到基础工具安装层 |
### 2.3 可选优化 (1 项) ✅
| # | 位置 | 问题描述 | 修复方案 |
|---|------|---------|---------|
| 1 | entrypoint.sh:30 | `tail -f /dev/null` 语义不够清晰 | 改为 `sleep infinity` |
---
## 3. 优化效果
### 3.1 镜像层数减少
```
优化前:110 行,SSH 配置单独一层
优化后:107 行,合并到基础工具层
```
**减少 1 个 RUN 层**
### 3.2 代码质量提升
- 消除无效命令
- 移除重复定义
- 提高构建可靠性
- 改善代码可读性
---
## 4. 变更对比
### Dockerfile 关键变更
```diff
- && rm -f go1.24.3.linux-amd64.tar.gz
# 移除无效命令
+ RUN mkdir -p /opt/rust $CARGO_HOME/bin \
# 添加目录创建
- curl wget netcat-openbsd \
+ netcat-openbsd \
# 移除重复安装
- ENV PATH="/usr/local/go/bin:/root/go/bin:/root/.cargo/bin:$PATH"
# 移除重复环境变量
- # ============ SSH 配置 ============
- RUN mkdir -p /var/run/sshd \
- && echo 'root:devbox123' | chpasswd \
- && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# 合并到基础工具层
```
### entrypoint.sh 变更
```diff
- exec tail -f /dev/null
+ exec sleep infinity
```
---
## 5. 审查结论
| 指标 | 结果 |
|------|------|
| 发现问题 | 7 项 |
| 已修复 | 7 项 |
| 修复率 | 100% |
| 遗留问题 | 0 项 |
**结论**:所有问题已修复,代码质量符合规范,可以进行镜像构建。
---
## 6. 后续建议
1. **构建测试** - 执行 `docker build -t dev-box:latest .` 验证修复效果
2. **功能验证** - 启动容器并测试各服务(SSH、MySQL、Redis
3. **版本更新** - 更新 SPECS.md 版本历史
---
## 附录:审查标准
本次审查基于以下标准:
1. DRY 检查 - 是否有重复实现
2. 简洁易读 - 变量/方法名是否清晰
3. 现有实现 - 核对是否已有类似功能
4. 防御性编程 - 避免过度防御
5. 逻辑嵌套 - 减少嵌套层级
+508 -248
View File
@@ -1,317 +1,577 @@
# WorkPod 技术规格说明书 # WorkPod Alpine 技术规格
> 文档版本:1.0 > 版本:1.1.0 | 更新日期:2026-04-07 | 镜像版本:workpod-alpine:latest (1.1.0)
> 最后更新:2026-03-18
> 项目路径:`E:/workpod`
--- ---
## 1. 概述 ## 1. 项目概述
### 1.1 项目定位 WorkPod Alpine 是一个**轻量级 Docker 开发环境容器**,基于 Alpine Linux 3.23 构建,提供 Web 终端(ttyd)和 SSH 双入口,支持多实例部署、开发工具自动检测、tmux 多会话管理。
WorkPod 是一个基于 Docker 的**全栈开发环境容器**,旨在提供统一、可移植、可离线部署的开发环境。 ### 核心特性
### 1.2 设计目标 | 特性 | 说明 |
| 目标 | 说明 |
|------|------| |------|------|
| **环境统一** | 消除"在我机器上能跑"的问题 | | **镜像大小** | ~436MB(多阶段构建,Ubuntu 版的 1/6) |
| **离线可用** | 所有依赖包本地化,无需网络 | | **基础镜像** | alpine:3.23 (~7MB) |
| **快速启动** | 一键启动完整开发环境 | | **运行时内存** | 80-120MB 基线 + 应用 |
| **可迁移性** | 支持导出/导入到服务器 | | **启动时间** | ~2.5 秒 |
| **数据持久化** | 容器可重建,数据不丢失 | | **Web 终端** | ttyd + tmux 会话管理 |
| **AI 工具集成** | Claude Code + coding-helper + 智谱 GLM |
### 1.3 适用场景 ### 适用场景
- 多工作空间统一开发环境(wk-flux, wk-lab, wk-suke, wk-oth 等 - 远程开发环境(浏览器直接访问,无需本地配置
- 新成员快速上手 - 团队成员统一开发环境
- 服务器环境部署 - 多项目隔离部署(每个项目独立容器实例)
- 离线/内网开发环境
--- ---
## 2. 技术架构 ## 2. 架构设计
### 2.1 基础架构 ```mermaid
graph TB
subgraph "Docker Host"
DC["docker-compose.yml<br/>基础实例"]
DCF["docker-compose.flux.yml<br/>Flux 项目"]
DCA["docker-compose-alpine.yml<br/>备用/测试"]
end
``` subgraph "Images"
┌─────────────────────────────────────────────────────────┐ BASE["workpod-alpine:latest<br/>Alpine 3.23 + Node 24<br/>~436MB"]
│ Docker Desktop │ FLUX["ws-flux-dev:latest<br/>BASE + JDK17/Maven 挂载"]
│ ┌───────────────────────────────────────────────────┐ │ end
│ │ workpod 容器 (Ubuntu 22.04) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ subgraph "Containers"
│ │ │Python│ │ Node │ │ Go │ │ Rust │ │MySQL │ │ │ W1["workpod-alpine<br/>SSH :2222 / Web :7681<br/>network: workpod-alpine-network"]
│ │ │ 3.10 │ │ 24.x │ │1.26 │ │1.94 │ │ 8.0 │ │ │ W2["ws-flux-dev<br/>SSH :2201 / Web :7701<br/>network: workpod-flux-network"]
│ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │ end
│ │ ┌──────┐ ┌──────┐ ┌──────────────────────────┐ │ │
│ │ │Redis │ │ SSH │ │ Claude Code + OpenClaw │ │ │ subgraph "Host Mounts"
│ │ │ 6.0 │ │ 2222 │ │ (AI 编程工具) │ │ │ WS["E:/wk-flux → /workspace"]
│ │ └──────┘ └──────┘ └──────────────────────────┘ │ │ JDK["D:/Java/jdk-musl-17 → /opt/jdk-musl-17 :ro"]
│ └───────────────────────────────────────────────────┘ │ MVN["D:/Java/maven-mvnd → /opt/maven-mvnd :ro"]
│ ↕ 数据卷挂载 (/d/docker-data) │ end
│ ┌───────────────────────────────────────────────────┐ │
│ │ MySQL │ Redis │ Workspace │ │ subgraph "Optional"
│ └───────────────────────────────────────────────────┘ │ AP["auth-proxy.js<br/>认证代理 :8080<br/>Basic Auth + Token"]
└─────────────────────────────────────────────────────────┘ NX["nginx (wk.1216.top)<br/>SSL 终止 + 反向代理"]
end
DC -->|"build"| BASE
DCF -->|"build"| FLUX
DCA -->|"image"| W1
BASE --> W1
FLUX --> W2
W2 --> WS
W2 --> JDK
W2 --> MVN
AP -.->|"proxy"| W1
AP -.->|"proxy"| W2
NX -.->|"ssl termination"| AP
``` ```
### 2.2 技术栈明细 ### 组件职责
| 组件 | 版本 | 安装方式 | 启动方式 | | 组件 | 职责 | 状态 |
|------|------|---------|---------| |------|------|------|
| Ubuntu | 22.04 | 基础镜像 | - | | `Dockerfile` | 基础镜像构建(Node + Claude Code + coding-helper | 活跃 |
| Python | 3.10 | apt | 常驻 | | `Dockerfile.flux` | Flux 项目扩展(基于基础镜像,JDK 外挂) | 活跃 |
| Node.js | 24.14.0 | 离线包 | 常驻 | | `entrypoint.sh` | 容器入口:信号处理、PATH 检测、服务启停 | 活跃 |
| Go | 1.26.1 | 离线包 | 常驻 | | `ttyd-session.sh` | tmux 会话管理(URL 参数 / 交互菜单) | 活跃 |
| Rust | 1.94.0 | 离线包 | 常驻 | | `docker-compose.yml` | 基础实例编排 | 活跃 |
| MySQL | 8.0 | apt | 手动 | | `docker-compose.flux.yml` | Flux 项目实例编排 | 活跃 |
| Redis | 6.0 | apt | 手动 | | `docker-compose-alpine.yml` | 备用编排(多端口 /root 挂载) | 归档备用 |
| Claude Code | 2.1.78 | 离线包 | 按需 | | `auth-proxy.js` | 认证代理(Basic Auth + Token + WS 代理) | 可选组件 |
| OpenClaw | 2026.3.13 | 离线包 | 按需 | | `download-packages.sh` | 离线包下载脚本 | 归档备用 |
| `entrypoint-test.sh` | Test 入口(developer 用户 + 全权限 Claude | 已归档 |
### 2.3 离线包清单
```
packages/
├── node-v24.14.0-linux-x64.tar.xz # Node.js 预编译包
├── go1.26.1.linux-amd64.tar.gz # Go SDK
├── rust-1.94.0-x86_64-unknown-linux-gnu.tar.xz # Rust 工具链
├── claude-code-2.1.78.tgz # Claude Code npm 包
├── openclaw-2026.3.13.tgz # OpenClaw npm 包
└── rustup-init.sh # Rust 安装脚本 (备用)
```
--- ---
## 3. 网络配置 ## 3. 镜像构建
### 3.1 端口映射 ### 3.1 基础镜像 (Dockerfile)
| 服务 | 宿主机 | 容器 | 协议 | 说明 | ```dockerfile
|------|--------|------|------|------| # 多阶段构建
| SSH | 2222 | 22 | TCP | 远程登录 | FROM alpine:3.23 AS builder # 阶段1: 构建
| HTTP | 8080 | 80 | TCP | Web 服务 | # → 安装 xz, libstdc++
| HTTPS | 8443 | 443 | TCP | 加密 Web 服务 | # → 解压 Node.js v24.14.1 (musl)
| MySQL | 13306 | 3306 | TCP | 数据库 | # → npm install -g claude-code@latest, coding-helper@latest
| Redis | 16379 | 6379 | TCP | 缓存 | # → npm cache clean
### 3.2 连接方式 FROM alpine:3.23 # 阶段2: 运行时
# → apk add: curl git openssh-server bash ttyd tmux libstdc++
# → ssh-keygen + chpasswd + PermitRootLogin yes
# COPY --from=builder /opt/node /usr/local
# → npm config set registry npmmirror.com
# COPY --chmod=755 entrypoint.sh /entrypoint.sh
# COPY --chmod=755 ttyd-session.sh /opt/ttyd-session.sh
```
**构建产物**
| 层 | 内容 | 大小 |
|----|------|------|
| alpine:3.23 | 基础系统 | ~7MB |
| runtime tools | curl/git/ssh/ttyd/tmux/bash | ~80MB |
| /usr/local | Node.js + npm 全局包 | ~200MB |
| scripts | entrypoint + ttyd-session | <10KB |
| **总计** | | **~436MB (压缩后)** |
### 3.2 Flux 扩展镜像 (Dockerfile.flux)
```dockerfile
FROM workpod-alpine:latest
# 无额外 RUN 指令
# JDK 通过 docker-compose volume 从宿主机挂载
# JAVA_HOME 由 environment 设置
EXPOSE 22 7681
ENTRYPOINT ["/entrypoint.sh"]
```
Flux 镜像与基础镜像几乎等大(仅增加 LABEL 元数据),JDK/Maven 不占用镜像空间。
### 3.3 构建命令
```bash ```bash
# SSH 连接 # 基础镜像
ssh root@localhost -p 2222 docker build -t workpod-alpine:latest .
# 密码:workpod123
# Docker exec 进入 # Flux 镜像(依赖基础镜像先构建好)
docker exec -it workpod bash docker build -t ws-flux-dev:latest -f Dockerfile.flux .
# 数据库连接
mysql -h 127.0.0.1 -P 13306 -u root
redis-cli -h 127.0.0.1 -p 16379
``` ```
--- ---
## 4. 存储配置 ## 4. 容器编排
### 4.1 数据卷映射 ### 4.1 三份 Compose 文件对比
| 宿主机路径 | 容器路径 | 用途 | | 配置项 | docker-compose.yml (基础) | docker-compose.flux.yml (Flux) | docker-compose-alpine.yml (备用) |
|-----------|---------|------| |--------|--------------------------|-------------------------------|--------------------------------|
| `/d/docker-data/mysql` | `/var/lib/mysql` | MySQL 数据 | | **Service 名** | workpod-alpine | ws-flux-dev | workpod-alpine |
| `/d/docker-data/redis` | `/var/lib/redis` | Redis 数据 | | **Container 名** | workpod-alpine | ws-flux-dev | workpod-alpine |
| `/d/docker-data/workspace` | `/workspace` | 工作目录 | | **SSH 端口** | 2222 → 22 | 2201 → 22 | 2223 → 22 |
| `./config/supervisor` | `/etc/supervisor/conf.d` | 进程配置 | | **Web 端口** | 7681 → 7681 | 7701 → 7681 | 7683→7681, 7684→7682 |
| **工作空间挂载** | ./data/workspace → /workspace | E:/wk-flux → /workspace | data/workpod-alpine/workspace → /workspace |
| **JDK 挂载** | 无 | D:/Java/jdk-musl-17:ro → /opt/jdk-musl-17 | 无 |
| **Maven 挂载** | 无 | D:/Java/maven-mvnd:ro → /opt/maven-mvnd | 无 |
| **智谱 GLM env** | 无 | ANTHROPIC_* 5 项传递 | 无 |
| **privileged** | true | false | true |
| **内存限制** | 4G limit / 1G reserve | 4G limit / 1G reserve | 无 |
| **健康检查** | 有 | 有 | 无 |
| **日志轮转** | 10m × 3 | 10m × 3 | 无 |
| **网络** | workpod-alpine-network | workpod-flux-network | workpod-network |
### 4.2 存储要求 ### 4.2 启动命令
| 项目 | 最小空间 | 建议空间 | ```bash
# 基础实例
docker compose up -d
# Flux 项目实例
docker compose -f docker-compose.flux.yml up -d
# 备用实例
docker compose -f docker-compose-alpine.yml up -d
```
---
## 5. 运行时
### 5.1 启动流程 (entrypoint.sh)
```
trap SIGTERM/SIGINT/SIGQUIT → cleanup()
┌─ PATH 自动检测 ─────────────────────────┐
│ JAVA_HOME → ${JAVA_HOME}/bin │
│ MAVEN_HOME → ${MAVEN_HOME}/bin │
│ .cargo/bin → Rust (cargo/rustup) │
│ gvm/gos → Go (gvm 版本管理) │
│ .pyenv → Python (pyenv) │
│ go/bin → Go (自定义安装) │
│ .local/bin → 本地工具 │
│ rust-* → Rust 独立安装 │
└──────────────────────────────────────────┘
┌─ 写入 /etc/profile.d/dev-tools.sh ─────┐│ (新 shell 会话也生效)
│ (同样逻辑,确保持久化) │
└──────────────────────────────────────────┘
┌─ 启动服务 ────────────────────────────────┐
│ /usr/sbin/sshd │
│ ttyd -W -c "${TTYD_CREDENTIALS}" \ │
│ -t fontSize=16 \ │
│ -t theme='{"background":"#1a1a2e"}' │
│ /opt/ttyd-session.sh & │
└──────────────────────────────────────────┘
┌─ 健康检查 ────────────────────────────────┐
│ kill -0 $TTYD_PID → ttyd 存活? │
│ pidof sshd → sshd 存活? │
│ 任一失败 → exit 1 │
└──────────────────────────────────────────┘
┌─ 显示环境信息 ──────────────────────────┐
│ Node/npm/Claude/Rust/Go/Python 版本 │
│ 连接方式 (Web URL + SSH 命令) │
└──────────────────────────────────────────┘
exec sleep infinity (PID 1)
```
### 5.2 信号处理
```bash
cleanup() {
kill $TTYD_PID # ttyd 后台进程
kill $SSHD_PID # sshd (注意: 当前 PID 未追踪,实际用 pkill)
wait # 等待子进程退出
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
```
### 5.3 ttyd 会话管理 (ttyd-session.sh)
```
用户访问 http://host:7681
┌─ URL 带 ?session=xxx ? ──────┐
│ 是 → 解析 session 名称 │
│ ↓ │
│ 过滤非法字符 (仅 a-zA-Z0-9_-) │
│ ↓ │
│ tmux attach (存在) 或 new (不存在)│
│ │
│ 否 → 显示交互式菜单 │
│ ┌─────────────────────────┐ │
│ │ [1] session_a │ │
│ │ [2] session_b │ │
│ │ 输入编号或新名称 │ │
│ └─────────────────────────┘ │
│ ↓ │
│ 数字 → 映射到已有 session │
│ 字符串 → 新建 session │
│ ↓ │
│ exec tmux attach/new │
└─────────────────────────────────────┘
```
**安全设计**
- Session 名过滤:`tr -cd 'a-zA-Z0-9_-'` 防止注入
- 共享模式:不带 `-d` 参数,多人可同时查看同一 session
- 默认值:空输入默认为 `default`
---
## 6. 服务端口与连接方式
### 6.1 端口分配表
| 实例 | SSH | Web 终端 (ttyd) | 网络 |
|------|-----|---------------|------|
| **workpod-alpine** (基础) | 2222 | 7681 | workpod-alpine-network |
| **ws-flux-dev** (Flux) | 2201 | 7701 | workpod-flux-network |
| **workpod-test** (旧测试) | 2223 | 7682 | — |
### 6.2 连接速查
```bash
# 基础实例
Web: http://localhost:7681 # 用户 jc / 密码 1234567
SSH: ssh root@localhost -p 2222 # 密码 workpod123 (或 ROOT_PASSWORD)
# Flux 项目实例
Web: http://localhost:7701 # 用户 jc / 密码 1234567
SSH: ssh root@localhost -p 2201 # 密码 workpod123 (或 ROOT_PASSWORD)
# 进入容器 (调试用)
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash
MSYS_NO_PATHCONV=1 docker exec -it ws-flux-dev bash
```
### 6.3 环境变量速查
| 变量 | 默认值 | 用途 |
|------|--------|------|
| `ROOT_PASSWORD` | workpod123 | SSH root 密码 |
| `TTYD_CREDENTIALS` | jc:1234567 | ttyd Web 终端认证 (格式 `用户:密码`) |
| `TZ` | Asia/Shanghai | 时区 |
| `JAVA_HOME` | (无) | JDK 路径 (Flux 实例设置) |
| `MAVEN_HOME` | (无) | Maven 路径 (Flux 实例设置) |
| `ANTHROPIC_AUTH_TOKEN` | (从宿主机继承) | 智谱 API 密钥 |
| `ANTHROPIC_BASE_URL` | https://open.bigmodel.cn/api/anthropic | 智谱 API 地址 |
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | glm-5v-turbo | Sonnet 模型 |
| `ANTHROPIC_DEFAULT_OPUS_MODEL` | glm-5.1 | Opus 模型 |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | glm-4.5-air | Haiku 模型 |
---
## 7. 开发工具外挂机制
### 7.1 支持的工具链
entrypoint.sh 启动时自动检测以下路径并加入 PATH:
| 工具 | 检测路径 | 环境变量 |
|------|---------|---------| |------|---------|---------|
| 镜像构建 | 10 GB | 20 GB | | Java (JDK) | `$JAVA_HOME/bin` | JAVA_HOME |
| 容器运行 | 5 GB | 10 GB | | Maven | `$MAVEN_HOME/bin` | MAVEN_HOME |
| MySQL 数据 | 1 GB | 按需 | | Rust (rustup) | `/root/.cargo/bin` | RUSTUP_HOME, CARGO_HOME |
| 工作空间 | 1 GB | 按需 | | Rust (独立) | `/root/rust-*/bin` | |
| Go (gvm) | `/root/gvm/gos/current/bin` | — |
| Python (pyenv) | `/root/.pyenv/shims`, `.pyenv/bin` | — |
| Go (自定义) | `/root/go/bin` | GOROOT |
| 本地工具 | `/root/.local/bin` | — |
### 7.2 PATH 生效范围
- **当前 entrypoint 进程**:立即 export
- **后续 shell 登录**:通过 `/etc/profile.d/dev-tools.sh` 自动加载
- **docker exec**:需要 `bash -lc 'command'` 或手动 source profile
### 7.3 使用方式
开发者只需将工具安装到对应路径(通过挂载 /root 或在容器内安装),重启或重新登录即可生效。无需修改 Dockerfile 或 entrypoint。
--- ---
## 5. 构建规范 ## 8. 多实例部署策略
### 5.1 构建前检查 ### 8.1 推荐方案
```bash ```
# 检查 Docker 状态 项目 A → docker-compose.A.yml (端口 220x / 770x)
docker info 项目 B → docker-compose.B.yml (端口 221x / 771x)
docker ps 通用 → docker-compose.yml (端口 2222 / 7681)
# 检查离线包完整性
cd E:/workpod/packages
ls -lh *.tar.* *.tgz
``` ```
### 5.2 构建命令 每个项目一份 compose 文件,基于同一个基础镜像 `workpod-alpine:latest`,通过 Dockerfile 扩展或 volume 挂载添加项目特定依赖。
```bash ### 8.2 扩展新实例步骤
# 构建镜像
cd E:/workpod
docker build -t workpod:latest .
# 验证镜像 1. 创建 `Dockerfile.xxx`(如需额外包)或直接用基础镜像
docker images workpod 2. 创建 `docker-compose.xxx.yml`(配置端口、挂载、环境变量)
3. `docker compose -f docker-compose.xxx.yml up -d`
4. 确认端口不冲突
# 启动容器 ### 8.3 网络隔离
docker-compose up -d
# 验证容器 每个 compose 文件创建独立的 bridge network,容器间默认不可互通。如需互通可在同一 compose 中定义多 service。
docker ps --filter name=workpod
---
## 9. 安全模型
### 9.1 当前状态
| 安全项 | 配置 | 风险等级 | 备注 |
|--------|------|---------|------|
| **特权模式** | 基础版 `privileged: true` | **高** | Flux 版已移除 |
| **运行用户** | 全部 root | 中 | 开发环境可接受 |
| **SSH 登录** | PermitRootLogin yes + 密码 | 中 | 建议生产环境禁用 |
| **ttyd 认证** | Basic Auth (jc:1234567) | 低-中 | 可配置强密码 |
| **密码管理** | 环境变量注入 | 低 | 已修复硬编码 |
| **API Key** | docker-compose env 传递 | 低 | 不写入文件系统 |
| **镜像源** | alpine:3.23 + npmmirror | 低 | 第三方信任链 |
| **npm 包** | @latest (不确定版本) | 低 | 用户决策,接受风险 |
### 9.2 已修复的问题
- ~~密码硬编码在源码中~~ → ROOT_PASSWORD / TTYD_CREDENTIALS 环境变量
- ~~API Key 写入 .bashrc~~ → docker-compose environment 直接传递
- ~~healthcheck CMD 数组管道错误~~ → CMD-SHELL + netstat
- ~~entrypoint 外挂 bind mount~~ → COPY --chmod 内置镜像
- ~~构建上下文 ~520MB~~ → .dockerignore 优化到 275 bytes
### 9.3 待改进项
| 优先级 | 改进项 | 建议 |
|--------|--------|------|
| P0 | 移除 privileged: true | 基础版改用具体 capability 或确认是否真需要 |
| P1 | SSH 禁用密码登录 | 改为密钥认证,或限制来源 IP |
| P2 | ttyd 加密传输 | 前置 nginx/caddy 做 HTTPS 终止 |
| P3 | npm 包固定版本 | 将 @latest 改为具体版本号,构建可重现 |
---
## 10. 认证代理 (auth-proxy.js)
> 状态:可选组件,已补充到主目录但未默认启用
### 10.1 架构
```
浏览器 → :8080 auth-proxy.js
├── GET /login → 登录页面 (static/login.html)
├── POST /auth/check → Basic Auth → Token (1h 过期)
├── GET /api/workspaces → 工作空间列表
├── GET/WS /* → Token 验证 → 代理到 ttyd :7681
└── WebSocket upgrade → 双向管道 (终端 I/O)
``` ```
### 5.3 镜像导出/导入 ### 10.2 配置
| 配置项 | 值 |
|--------|-----|
| 监听端口 | 8080 |
| 认证方式 | Basic Auth (`wk:1234567`, `admin:admin123`) |
| Token 格式 | `wk_{用户名}_{时间戳}` |
| Token 过期 | 1 小时 (内存存储) |
| 工作区路由 | 默认(/) → :7681, hszd → :7682 |
### 10.3 生产部署
```bash ```bash
# 导出(压缩 # 1. 修改密码(环境变量或配置文件
docker save workpod:latest | gzip > workpod.tar.gz # 2. 前置 Nginx 反向代理 (wk.1216.conf)
# 3. SSL 证书 (wk.1216.top)
# 导入 # 4. 启动
docker load < workpod.tar.gz node auth-proxy.js
``` ```
--- ---
## 6. 运维规范 ## 11. 离线包管理
### 6.1 日常操作 > 状态:归档备用,Dockerfile 直接从 registry 安装
### 11.1 包清单
| 包名 | 版本 | 用途 | 来源 |
|------|------|------|------|
| node-v24.14.1-linux-x64-musl.tar.gz | v24.14.1 | Node.js 运行时 | unofficial-builds.nodejs.org |
| claude-code-*.tgz | @latest | AI 编程助手 | registry.npmmirror.com |
| z_ai-coding-helper-*.tgz | @latest | AI 编程助手 | registry.npmmirror.com |
| openclaw-*.tgz | 2026.3.28 | (未使用) | registry.npmmirror.com |
| go1.26.1.linux-amd64.tar.gz | 1.26.1 | (外挂预留) | Go 官方 |
| rust-1.94.1-x86_64-unknown-linux-musl.tar.xz | 1.94.1 | (外挂预留) | Rust 官方 |
### 11.2 下载脚本
```bash ```bash
# 启动容器 ./download-packages.sh
docker-compose up -d # 自动下载到 packages/ 目录
# 支持断点续传(文件存在则跳过)
# 使用国内镜像加速
```
# 停止容器 ---
docker-compose down
# 重启容器 ## 12. 决策记录
docker-compose restart
以下是在项目演进过程中做出的关键架构决策及其原因:
### DEC-01: Alpine vs Ubuntu 作为基础镜像
- **决策**: 选择 Alpine 3.23
- **原因**: 镜像小 6 倍(436MB vs 2.6GB)、内存基线低 5 倍、启动快 6 倍
- **代价**: musl libc 兼容性需注意(JDK 必须用 musl 版本)
### DEC-02: JDK/Maven 宿主机挂载 vs 镜像内安装
- **决策**: 选择宿主机目录 ro 挂载(`D:/Java/jdk-musl-17`, `D:/Java/maven-mvnd`
- **原因**: 升级 JDK/Maven 无需 rebuild 镜像;本地多项目可共享同一套工具链
- **代价**: 依赖宿主机路径存在;容器不可脱离该主机单独分发
### DEC-03: BellSoft Liberica JDK vs 其他 musl JDK
- **决策**: BellSoft Liberica JDK 17.0.16+12 (musl)
- **原因**: Adoptium "musl" 标签实际仍链接 glibcAlpine openjdk 无法外部挂载;gcompat 缺少符号
- **参考**: 经 ldd 验证解释器为 `/lib/ld-musl-x86_64.so.1`
### DEC-04: npm 包 @latest vs 固定版本
- **决策**: 保持 @latestclaude-code 和 coding-helper
- **原因**: 用户选择每次构建自动获取最新版本,接受不确定性
- **风险**: 构建结果可能不一致;回归问题排查困难
### DEC-05: auto-upgrade 功能废弃
- **决策**: 不实现容器启动时自动检测升级
- **原因**: 维护负担 > 收益;增加启动复杂度和网络依赖;set -e 下失败处理困难
- **替代**: 手动 `docker build` 时自然获取最新
### DEC-06: workpod-alpine 目录合并到 workpod
- **决策**: 删除 `E:/wk-lab/workpod-alpine/`,全部内容归入 `E:/wk-lab/workpod/`
- **原因**: 单一目录管理;避免两份代码漂移;旧版完整归档于 `_archive/`
### DEC-07: entrypoint/ttyd-session 内置镜像 vs bind mount
- **决策**: COPY --chmod=755 内置到镜像
- **原因**: 避免 execvp failedbind mount 源目录删除后容器失效);符合 immutable 镜像最佳实践
- **触发事件**: 2026-04-04 因删除 workpod-alpine 目录导致 workpod 容器 ttyd 子进程全部 crash (exit code 254)
---
## 附录 A: 常用运维命令
```bash
# 构建
docker build -t workpod-alpine:latest .
docker build -t ws-flux-dev:latest -f Dockerfile.flux .
# 启动/停止
docker compose up -d
docker compose down
docker compose -f docker-compose.flux.yml up -d
# 查看日志 # 查看日志
docker logs -f workpod docker logs -f workpod-alpine
docker logs --tail 50 ws-flux-dev
# 查看资源使用
docker stats workpod
# 进入容器 # 进入容器
docker exec -it workpod bash MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash
# 验证工具链 (需 login shell)
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash -lc '
echo "Java: $(java -version 2>&1 | head -1)"
echo "Maven: $(mvn -version 2>&1 | head -1)"
echo "Node: $(node -v)"
echo "Claude: $(claude --version)"
'
# 清理
docker system prune -f # 清理悬停资源
docker image prune -f # 清理未使用镜像
``` ```
### 6.2 数据库管理 ## 附录 B: 文件总览
```bash
# 启动 MySQL
docker exec workpod bash -c "mysqld --user=mysql --datadir=/var/lib/mysql &"
# 启动 Redis
docker exec workpod redis-server --daemonize yes
# 停止 MySQL
docker exec workpod bash -c "mysqladmin -u root shutdown"
# 停止 Redis
docker exec workpod redis-cli shutdown
```
### 6.3 健康检查
```bash
# 检查容器状态
docker inspect workpod --format='{{.State.Health.Status}}'
# 检查 SSH 服务
curl -k https://localhost:2222
# 检查 MySQL
docker exec workpod mysql -e "SELECT VERSION()"
# 检查 Redis
docker exec workpod redis-cli ping
```
---
## 7. 安全规范
### 7.1 访问控制
| 项目 | 当前配置 | 建议 |
|------|---------|------|
| SSH 密码 | workpod123 | 生产环境修改 |
| MySQL root | 无密码 | 生产环境设置密码 |
| Redis | 无密码 | 生产环境设置密码 |
### 7.2 安全加固建议
1. **生产环境**必须修改默认密码
2. 限制端口暴露范围
3. 使用 Docker 网络隔离
4. 定期更新基础镜像
---
## 8. 故障排查
### 8.1 常见问题
| 问题 | 可能原因 | 解决方案 |
|------|---------|---------|
| 容器启动失败 | 端口被占用 | `netstat -ano \| findstr :2222` |
| MySQL 无法启动 | 数据目录权限 | `chown mysql:mysql /var/lib/mysql` |
| SSH 连接失败 | SSH 服务未启动 | `docker exec workpod service ssh start` |
| 构建失败 | 离线包缺失 | 检查 packages/ 目录 |
### 8.2 日志位置
| 日志 | 命令 |
|------|------|
| 容器日志 | `docker logs workpod` |
| SSH 日志 | `docker exec workpod cat /var/log/auth.log` |
| MySQL 日志 | `docker exec workpod cat /var/log/mysql/error.log` |
---
## 9. 版本历史
| 版本 | 日期 | 变更说明 |
|------|------|---------|
| 1.0 | 2026-03-18 | 初始版本 |
---
## 附录 A:快速参考
### A.1 环境信息
``` ```
基础镜像:ubuntu:22.04 workpod/
时区:Asia/Shanghai ├── Dockerfile # 基础镜像 (多阶段构建)
语言:en_US.UTF-8 ├── Dockerfile.flux # Flux 扩展镜像
工作目录:/workspace ├── entrypoint.sh # 容器入口 (信号/PATH/服务/信息)
``` ├── ttyd-session.sh # tmux 会话管理
├── docker-compose.yml # 基础实例 (:2222/:7681)
### A.2 默认账号 ├── docker-compose.flux.yml # Flux 实例 (:2201/:7701)
├── docker-compose-alpine.yml # 备用编排
| 服务 | 用户名 | 密码 | ├── auth-proxy.js # 认证代理 (可选)
|------|-------|------| ├── download-packages.sh # 离线包下载 (归档)
| SSH | root | workpod123 | ├── entrypoint-test.sh # Test 入口 (归档)
| MySQL | root | - | ├── wk.1216.conf # Nginx 配置
| Redis | - | - | ├── nginx-map-patch.sh # Nginx WS 补丁
├── connection_upgrade.map # Nginx map 片段
### A.3 文件位置 ├── .dockerignore # 构建忽略
├── ISSUES.md # 问题记录
``` ├── SPECS.md # 本文档 ← 你在这里
E:/workpod/ ├── docs/
├── Dockerfile # 镜像构建配置 │ └── 05-问题处理/ # 5 份审核报告
├── docker-compose.yml # 容器编排配置 ├── config/ # 配置模板
├── entrypoint.sh # 启动脚本 ├── static/ # auth-proxy 登录页
├── SPECS.md # 本文档 ├── data/ # 运行时数据
── README.md # 使用说明 ── _archive/ # 历史版本归档
├── packages/ # 离线安装包
└── config/supervisor/ # Supervisor 配置
``` ```
+173
View File
@@ -0,0 +1,173 @@
const http = require('http');
const net = require('net');
const fs = require('fs');
// 配置
const PORT = 8080;
const TOKEN_PREFIX = 'wk_';
// 用户口令从环境变量读取(JSON),未设置时用弱默认——生产环境务必通过 WORKPOD_AUTH_USERS 注入强口令
const VALID_PASSWORDS = JSON.parse(process.env.WORKPOD_AUTH_USERS || '{"wk":"1234567","admin":"admin123"}');
const WORKSPACES = [
{ path: '/', name: '默认工作区', desc: '/workspace', port: 7681 },
{ path: '/hszd', name: '华商智地', desc: '/workspace/wk-hszd', port: 7682 },
];
// Token 存储 (内存)
const tokens = new Set();
// 生成 token
function createToken(user) {
const t = TOKEN_PREFIX + user + '_' + Date.now();
tokens.add(t);
// 1小时过期清理
setTimeout(() => tokens.delete(t), 3600000);
return t;
}
// 验证 token
function validateToken(token) {
return token && tokens.has(token);
}
// 解析 URL 路径,匹配工作区
function matchWorkspace(pathname) {
for (const ws of WORKSPACES) {
if (pathname === ws.path || pathname.startsWith(ws.path + '/')) {
const proxyPath = ws.path === '/' ? pathname : pathname.slice(ws.path.length) || '/';
return { ws, proxyPath };
}
}
return null;
}
// 代理请求到 ttyd
function proxyToTtyd(req, res, port, path) {
const options = {
hostname: '127.0.0.1',
port: port,
path: path,
method: req.method,
headers: { ...req.headers, host: '127.0.0.1:' + port, 'X-WorkPod-Auth': '1' }
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', () => {
if (!res.headersSent) res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
}
// 登录页 HTML
const LOGIN_HTML = fs.readFileSync('/var/www/workpod/login.html', 'utf-8');
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://' + req.headers.host);
const token = url.searchParams.get('token') || '';
// 登录页
if (url.pathname === '/login') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(LOGIN_HTML);
}
// 认证 API
if (url.pathname === '/auth/check') {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Basic ')) {
res.writeHead(401);
return res.end('Unauthorized');
}
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
const [user, pass] = decoded.split(':');
if (!VALID_PASSWORDS[user] || VALID_PASSWORDS[user] !== pass) {
res.writeHead(401);
return res.end('Unauthorized');
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
return res.end(createToken(user));
}
// 工作空间列表
if (url.pathname === '/api/workspaces') {
if (!validateToken(token)) {
res.writeHead(401);
return res.end('Unauthorized');
}
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(WORKSPACES));
}
// ttyd 内部端点 (/token, /ws 等) — 直接代理到默认工作区
if (url.pathname === '/token' || url.pathname.startsWith('/ttyd/')) {
proxyToTtyd(req, res, WORKSPACES[0].port, url.pathname + url.search);
return;
}
// Token 验证
if (!validateToken(token)) {
res.writeHead(302, { 'Location': '/login' });
return res.end();
}
// 路由到对应工作区 (普通 HTTP)
const matched = matchWorkspace(url.pathname);
if (matched) {
const { ws, proxyPath } = matched;
proxyToTtyd(req, res, ws.port, proxyPath + url.search);
return;
}
// 未匹配路径 -> 登录页
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(LOGIN_HTML);
});
// WebSocket 代理 (upgrade 事件)
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'http://' + req.headers.host);
const token = url.searchParams.get('token') || '';
// Token 验证
if (!validateToken(token)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
// 匹配工作区
const matched = matchWorkspace(url.pathname);
if (!matched) {
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
return;
}
const { ws, proxyPath } = matched;
const proxyUrl = new URL(proxyPath + url.search, 'http://127.0.0.1:' + ws.port);
// 建立到 ttyd 的 TCP 连接
const target = net.connect(ws.port, '127.0.0.1', () => {
// 构造 upgrade 请求头
const headers = { ...req.headers, host: '127.0.0.1:' + ws.port, 'X-WorkPod-Auth': '1' };
const requestLine = 'GET ' + proxyUrl.pathname + proxyUrl.search + ' HTTP/1.1\r\n';
const headerLines = Object.entries(headers).map(([k, v]) => k + ': ' + v).join('\r\n');
target.write(requestLine + headerLines + '\r\n\r\n');
// 如果有缓冲数据(head),转发给 ttyd
if (head.length > 0) target.write(head);
// 双向管道: 浏览器 <-> ttyd
target.pipe(socket);
socket.pipe(target);
});
target.on('error', () => socket.destroy());
socket.on('error', () => target.destroy());
});
server.listen(PORT, () => {
console.log('[auth-proxy] 认证代理已启动,端口: ' + PORT);
});
+1
View File
@@ -0,0 +1 @@
{"permissions": {"allow": ["*"], "deny": []}}
+4
View File
@@ -0,0 +1,4 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
+117
View File
@@ -0,0 +1,117 @@
# Flux 开发环境
> 当前在 ws-flux-dev 容器内
> 工作目录: /workspace (= E:/wk-flux)
## 项目概述
贷款撮合平台 (Flux),核心模块:
| 模块 | 技术栈 | 说明 |
|------|--------|------|
| flux-api | Java 17 + Redkale + Maven | 后端 API,核心业务 |
| flux-admin | Vue 3 + Arco Design + bun | 管理后台 |
| flux-uniapp | UniApp + Vite | H5 前端 |
| flux-mock | Go | 三方模拟服务 |
| flux-test | Go | 业务链测试 (撞库等) |
| file-server | Go | 文件上传下载 |
| flux-wechat-api | — | 微信服务端 |
## 代理工具(优先使用)
> **容器内代理服务由 .bashrc 自动启动**,无需手动启动。
> **所有数据库和远程操作必须通过代理工具执行**,禁止直接用 mysql/ssh/redis-cli。
### MySQL 查询
```bash
# 查询 (默认表格输出)
mysql-proxy cli -c flux_dev -e "SELECT * FROM users LIMIT 10"
# JSON 输出 (便于解析)
mysql-proxy cli -c flux_dev -e "SHOW TABLES" -F json
# 执行 DML (加 -x)
mysql-proxy cli -c flux_dev -e "UPDATE table SET status=1 WHERE id=1" -x
```
### SSH 远程执行
```bash
# 执行命令
ssh-proxy exec -n flux_dev -c "docker ps"
# JSON 输出
ssh-proxy exec -n flux_dev -c "docker ps" -F json
# 生产服务器 (byr_pro = prod_baiyarong)
ssh-proxy exec -n byr_pro -c "cd /opt/flux-api && bash bin/restart.sh"
```
### Redis 操作
```bash
# 取值
redis-proxy get -c flux_dev -k "session:user:123"
# 通用命令 (最灵活)
redis-proxy run -c flux_dev -C "KEYS" -a "user:*"
# 设置值
redis-proxy set -c flux_dev -k "key" -v "value"
```
## 开发命令
```bash
# ===== flux-api (Java/Redkale/Maven) =====
cd /workspace/flux-api
mvn clean package # 构建
cmd /c bin\restart.bat # 重启 (Windows bat,容器内需调整)
# ===== flux-admin (Vue3/bun) =====
cd /workspace/flux-admin
bun install # 安装依赖
bun run dev # 开发模式
# ===== flux-uniapp (UniApp) =====
cd /workspace/flux-uniapp
npm install
npm run dev:h5 # H5 模式
# ===== Go 服务 =====
cd /workspace/flux-mock && go run *.go
cd /workspace/flux-test && go run test_quick.go qulaijie -mode=collide
cd /workspace/file-server && go run *.go
```
## 连接信息速查
详细配置见:
- 服务器: `/root/.dev-env/servers.yaml`
- 数据库: `/root/.dev-env/databases.yaml`
- 规范: `/root/.dev-env/norms.md`
**快速参考**:
| 环境 | MySQL | Redis |
|------|-------|-------|
| 测试 (flux_dev) | 见 databases.yaml(不入库) | 见 databases.yaml :6379 |
| 生产 (flux_prox) | 见 databases.yaml(不入库) | — |
## 项目知识库
Flux 专属文档位于 `/root/.dev-env/docs/`
| 文件 | 用途 |
|------|------|
| Redkale使用易错点.md | **flux-api 必读** — 框架踩坑记录 |
| MySQL慢查询分析.md | SQL 慢查询优化参考 |
## 开发规范
> **唯一事实来源**: `/root/.dev-env/norms.md`
> 本文件不重复规范内容,避免维护不一致。
执行任务前先读取 norms.md,关键规则:
- Git 提交、代码审查、命名规范、高危操作检查清单
+1
View File
@@ -0,0 +1 @@
{"permissions": {"allow": ["Bash(mysql-proxy *)", "Bash(ssh-proxy *)", "Bash(redis-proxy *)", "Bash(mongo-proxy *)", "Bash(mvn *)", "Bash(java *)", "Bash(javac *)", "Bash(go *)", "Bash(bun *)", "Bash(npm *)", "Bash(node *)", "Bash(cmd *)", "Bash(git *)", "Bash(docker *)", "Bash(curl *)", "Bash(wget *)", "Bash(cat *)", "Bash(ls *)", "Bash(grep *)", "Bash(find *)", "Read", "Write", "Edit"], "deny": []}}
+124
View File
@@ -0,0 +1,124 @@
# MySQL 慢查询分析
---
## 查看慢查询配置
```sql
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
```
---
## 查询慢日志(RDS/MySQL
```sql
-- 最近24小时慢查询
SELECT
db,
LEFT(REPLACE(REPLACE(sql_text, '\n', ' '), ' ', ' '), 200) as sql_preview,
query_time,
rows_examined,
rows_sent,
start_time
FROM mysql.slow_log
WHERE start_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY query_time DESC
LIMIT 30;
-- 查询所有慢记录
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 20;
```
---
## 查看当前运行的长查询
```sql
-- 查看所有查询
SHOW FULL PROCESSLIST;
-- 筛选长查询(超过10秒)
SELECT
id, User, Host, db, Time, State,
LEFT(Info, 150) as sql_preview
FROM information_schema.processlist
WHERE Command = 'Query' AND Time > 10
ORDER BY Time DESC;
```
---
## 终止查询
```sql
-- 单个终止
KILL <process_id>;
-- 批量生成终止语句(按来源IP
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE Host LIKE '39.99.243.191%' AND Command = 'Query';
```
---
## performance_schema 分析
```sql
-- 按总耗时排序
SELECT
SCHEMA_NAME,
LEFT(DIGEST_TEXT, 150) as sql_pattern,
COUNT_STAR as exec_count,
ROUND(SUM_TIMER_WAIT/1000000000000, 2) as total_sec,
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_ms,
ROUND(MAX_TIMER_WAIT/1000000000000, 2) as max_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
-- 按平均耗时排序
SELECT
SCHEMA_NAME,
LEFT(DIGEST_TEXT, 150) as sql_pattern,
COUNT_STAR as exec_count,
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_ms,
ROUND(MAX_TIMER_WAIT/1000000000000, 2) as max_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 15;
```
---
## 查看表大小
```sql
SELECT
table_schema,
table_name,
ROUND(data_length/1024/1024, 2) as data_MB,
ROUND(index_length/1024/1024, 2) as index_MB,
table_rows
FROM information_schema.tables
WHERE table_schema IN ('db1', 'db2')
ORDER BY data_length DESC
LIMIT 30;
```
---
## 常用连接
| 库 | 连接命令 |
|----|----------|
| suke 生产库(阿里云RDS | `mysql -hrm-8vbrd2wil14hclop3vm.mysql.zhangbei.rds.aliyuncs.com -uroot -pWFFGwffg233` |
| 二号机 | `mysql -h47.92.117.13 -uroot -px251119!` |
---
*创建时间:2026-03-04*
@@ -0,0 +1,382 @@
# Redkale FilterNode 使用易错点
> 创建时间:2026-02-03
> 基于:flux-api 项目实际代码
---
## ❌ 常见错误
### 1. LIKE 查询错误
**错误写法**
```java
// ❌ 错误:使用静态方法
node.and(PartnerFormRecord::getRealname, FilterNode.like(keyword));
```
**正确写法**
```java
// ✅ 正确:使用实例方法
node.like(PartnerFormRecord::getRealname, keyword);
```
---
### 2. 比较查询错误
**错误写法**
```java
// ❌ 错误:方法名拼写错误
node.and(PartnerFormRecord::getCreatedtime, FilterNode.greateEqual(starttime));
node.and(PartnerFormRecord::getCreatedtime, FilterNode.lessEqual(endtime));
```
**正确写法**
```java
// ✅ 正确:使用简短的方法名
node.ge(PartnerFormRecord::getCreatedtime, starttime);
node.le(PartnerFormRecord::getCreatedtime, endtime);
```
**参考方法**
- `ge()` - Greater or Equal>=
- `le()` - Less or Equal<=
- `gt()` - Greater Than>
- `lt()` - Less Than<
---
### 3. OR 条件错误
**错误写法**
```java
// ❌ 错误:在 and() 中混用 FilterNode 静态方法
node.and(
FilterNode.or(
PartnerFormRecord::getRealname, FilterNode.like(keyword),
PartnerFormRecord::getMobile, FilterNode.like(keyword)
)
);
```
**正确写法**
```java
// ✅ 正确:创建独立 OR 节点,链式调用
FilterNode orNode = new FilterNode();
orNode.or(PartnerFormRecord::getRealname, FilterNode.like(keyword))
.or(PartnerFormRecord::getMobile, FilterNode.like(keyword))
.or(PartnerFormRecord::getIdcard, FilterNode.like(keyword));
node.and(orNode);
```
---
### 4. 等值条件冗余
**错误写法**
```java
// ❌ 冗余:等值条件不需要特殊方法
node.and(PartnerFormRecord::getStatus, FilterNode.eq(1));
```
**正确写法**
```java
// ✅ 简洁:直接传值
node.and(PartnerFormRecord::getStatus, 1);
```
---
### 5. NOT 条件错误
**错误写法**
```java
// ❌ 错误:使用不存在的 notLike 方法
node.notLike(PartnerFormRecord::getStatus, 3);
```
**正确写法**
```java
// ✅ 正确:使用 notEq
node.notEq(PartnerFormRecord::getStatus, 3);
```
---
## ✅ 正确的模式
### 基础查询条件
```java
FilterNode node = new FilterNode();
// 固定条件
node.and(PartnerFormRecord::getDraftstatus, (short) 0);
// 可选条件
if (bean.getKeyword() != null && !bean.getKeyword().isEmpty()) {
node.like(PartnerFormRecord::getRealname, bean.getKeyword());
}
// 范围查询
if (bean.getStarttime() > 0) {
node.ge(PartnerFormRecord::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerFormRecord::getCreatedtime, bean.getEndtime());
}
// 不等于
node.notEq(PartnerFormRecord::getStatus, (short) 3);
```
### OR 条件组合
```java
// 关键词:姓名 OR 电话 OR 身份证
if (bean.getKeyword() != null && !bean.getKeyword().isEmpty()) {
FilterNode orNode = new FilterNode();
orNode.or(PartnerFormRecord::getRealname, FilterNode.like(bean.getKeyword()))
.or(PartnerFormRecord::getMobile, FilterNode.like(bean.getKeyword()))
.or(PartnerFormRecord::getIdcard, FilterNode.like(bean.getKeyword()));
node.and(orNode);
}
```
### IN 条件
```java
// IN 查询
FilterNode node = new FilterNode();
node.in(PartnerFormRecord::getFormdataid, formIds.toArray());
List<PartnerFormRecord> list = dataSource.queryList(PartnerFormRecord.class, node);
```
---
## 📋 FilterNode 方法速查表
| 方法 | 说明 | 示例 |
|------|------|------|
| `and()` | AND 等值条件 | `node.and(Entity::getField, value)` |
| `like()` | LIKE 模糊查询 | `node.like(Entity::getField, "%keyword%")` |
| `ge()` | >= 大于等于 | `node.ge(Entity::getTime, timestamp)` |
| `le()` | <= 小于等于 | `node.le(Entity::getTime, timestamp)` |
| `gt()` | > 大于 | `node.gt(Entity::getField, value)` |
| `lt()` | < 小于 | `node.lt(Entity::getField, value)` |
| `notEq()` | != 不等于 | `node.notEq(Entity::getStatus, 3)` |
| `in()` | IN 查询 | `node.in(Entity::getId, ids.toArray())` |
| `or()` | OR 条件 | 需创建独立 FilterNode |
---
## 🔍 完整示例参考
### PartnerFormService.java
```java
@RestMapping(name = "list", comment = "分页查询表单记录")
public RetResult<Sheet<PartnerFormRecord>> list(FormRecordFilterBean bean, Flipper flipper) {
FilterNode node = new FilterNode();
node.notEq(PartnerFormRecord::getStatus, STATUS_DELETED);
if (!Utils.isEmpty(bean.getRealname())) {
node.like(PartnerFormRecord::getRealname, bean.getRealname());
}
if (bean.getDraftstatus() >= 0) {
node.and(PartnerFormRecord::getDraftstatus, bean.getDraftstatus());
}
if (bean.getStarttime() > 0) {
node.ge(PartnerFormRecord::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerFormRecord::getCreatedtime, bean.getEndtime());
}
Sheet<PartnerFormRecord> sheet = dataSource.querySheet(PartnerFormRecord.class, flipper, node);
return render(sheet);
}
```
### PartnerCustomerService.java
```java
@RestMapping(name = "list", comment = "分页查询客户列表")
public RetResult<Sheet<PartnerCustomer>> list(PartnerCustomerFilterBean bean, Flipper flipper) {
FilterNode node = new FilterNode();
node.notEq(PartnerCustomer::getStatus, (short) 3);
if (!Utils.isEmpty(bean.getMobile())) {
node.and(PartnerCustomer::getMobile, bean.getMobile());
}
if (bean.getStatus() > 0) {
node.and(PartnerCustomer::getStatus, bean.getStatus());
}
if (bean.getStarttime() > 0) {
node.ge(PartnerCustomer::getCreatedtime, bean.getStarttime());
}
if (bean.getEndtime() > 0) {
node.le(PartnerCustomer::getCreatedtime, bean.getEndtime());
}
Sheet<PartnerCustomer> sheet = dataSource.querySheet(PartnerCustomer.class, flipper, node);
return render(sheet);
}
```
---
## ⚠️ 注意事项
1. **方法调用方式**:所有条件方法都是 `FilterNode` 实例的方法,不是静态方法
2. **方法命名**`ge`/`le` 而非 `greateEqual`/`lessEqual`
3. **OR 条件**:必须创建独立的 `FilterNode` 对象进行链式调用
4. **值比较**:等值条件直接传值,不需要 `FilterNode.eq()`
5. **LIKE 参数**:通常需要在参数中自行添加 `%` 通配符
---
## 📚 参考代码位置
- `flux-api/src/main/java/cn/casehub/partner/PartnerFormService.java`
- `flux-api/src/main/java/cn/casehub/partner/PartnerCustomerService.java`
- `flux-api/src/main/java/cn/casehub/partner/PartnerVisitService.java`
---
# Redkale JSON 使用要点
> 更新时间:2026-03-03
> 背景:速贷接口接入时错误使用 `@JsonProperty`Jackson 注解),Redkale 不支持
---
## ❌ 常见错误
### 1. 使用 Jackson 注解
**错误写法**
```java
// ❌ 错误:Redkale 不支持 Jackson 注解
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonProperty("mobile_md5")
private String mobileMd5;
```
**正确方案**:使用 Redkale 的 `@ConvertColumn(name = "xxx")` 进行字段重命名:
```java
// ✅ 正确:使用 @ConvertColumn 重命名
import org.redkale.convert.ConvertColumn;
@ConvertColumn(name = "mobile_md5")
private String mobileMd5; // Java驼峰,JSON下划线
```
---
## ✅ JsonConvert 使用方式
### 序列化(对象 → JSON
```java
// 方式1:实例方法
JsonConvert convert = JsonConvert.root();
String json = convert.convertTo(object);
// 方式2:静态调用
String json = JsonConvert.root().convertTo(object);
```
### 反序列化(JSON → 对象)
```java
// 单个对象
User user = JsonConvert.root().convertFrom(User.class, json);
// 泛型集合
List<User> users = JsonConvert.root().convertFrom(
new TypeToken<List<User>>() {}.getType(),
json
);
// Map(用于解析不确定结构的JSON)
Map<String, Object> map = JsonConvert.root().convertFrom(Map.class, json);
```
---
## 📋 字段映射规则
| Java 字段名 | JSON 字段名 | 说明 |
|------------|------------|------|
| `mobileMd5` | `mobileMd5` | 默认:驼峰 → 驼峰 |
| `mobile_md5` | `mobile_md5` | 默认:下划线 → 下划线 |
| `mobileMd5` + `@ConvertColumn(name="mobile_md5")` | `mobile_md5` | ✅ 重命名 |
| `mobileMd5` + `@ConvertColumn(ignore=true)` | - | 忽略字段 |
**结论**:使用 `@ConvertColumn(name = "xxx")` 可实现字段重命名。
---
## 🔧 跨命名风格转换方案
当第三方接口使用下划线,内部使用驼峰时:
### 方案1@ConvertColumn + Utils.copy(推荐)
```java
// 速贷请求BeanJava驼峰,JSON下划线)
public class SuDaiRequest {
@ConvertColumn(name = "mobile_md5")
private String mobileMd5;
@ConvertColumn(name = "city_id")
private String cityId;
}
// 内部Bean(驼峰)
public class CollideBean {
private String mobileMd5;
private String cityId;
}
// 转换器 - 使用 Utils.copy
public class Converter {
public static CollideBean toBean(SuDaiRequest req) {
return Utils.copy(new CollideBean(), req); // 一行搞定
}
}
```
### 方案2:手动映射(不推荐)
```java
// 速贷请求Bean(下划线)
public class SuDaiRequest {
private String mobile_md5;
private String city_id;
}
// 转换器 - 手动映射
public class Converter {
public static CollideBean toBean(SuDaiRequest req) {
CollideBean bean = new CollideBean();
bean.setMobileMd5(req.getMobile_md5()); // 手动映射
bean.setCityId(req.getCity_id());
return bean;
}
}
```
---
## ⚠️ 注意事项
1. **使用 @ConvertColumn**:Redkale 的字段重命名注解,不是 Jackson 的 @JsonProperty
2. **@ConvertColumn 用法**
- `@ConvertColumn(name = "xxx")` - 重命名JSON字段
- `@ConvertColumn(ignore = true)` - 忽略字段
3. **配合 Utils.copy**:重命名后,Java字段名一致,可用 `Utils.copy` 简化转换
+29
View File
@@ -0,0 +1,29 @@
# mysql-proxy 连接配置模板
# 用法:复制为 mysql-proxy.toml 后填入真实值(mysql-proxy.toml 已在 .gitignore,不入库)
# 真实密码/IP 请勿提交,参见 docs/03-运维/开发环境搭建手册.md
[server]
port = 3307
host = "0.0.0.0"
[pool]
default_max_connections = 5
idle_timeout_secs = 300
check_interval_secs = 60
[[connections]]
name = "flux_dev"
host = "<TEST_DB_HOST>"
port = 3306
user = "root"
password = "<TEST_DB_PASSWORD>"
database = "flux_dev"
[[connections]]
name = "flux_prox"
host = "<PROD_DB_HOST>"
port = 3306
user = "root"
password = "<PROD_DB_PASSWORD>"
database = "flux_prox"
max_connections = 10
+102
View File
@@ -0,0 +1,102 @@
# Flux 项目开发规范
> 本文件供 Claude Code / AI 助手阅读,统一开发规范认知
---
## Git 提交规范
```
<类型>: <简述>
```
**类型**: 新增 | 修复 | 优化 | 重构
**禁止**:
- 英文提交信息
- Co-Authored-By 尾巴
- 提及工具名称(如 "generated by Claude"
**示例**:
```
新增: 渠道价格匹配规则引擎
修复: 下游进件超时未回调处理
优化: SQL 慢查询添加联合索引
重构: UpstreamService 抽象三方对接接口
```
## 技术栈与启动命令
| 模块 | 技术 | 目录 | 启动命令 |
|------|------|------|---------|
| flux-api | Java 17 + Redkale + Maven | flux-api/ | `cmd /c bin\restart.bat``mvn clean package` 后运行 |
| flux-admin | Vue 3 + Arco Design Pro + Vite + bun | flux-admin/ | `bun install && bun run dev` |
| flux-uniapp | UniApp (H5) + Vite | flux-uniapp/ | `npm install && npm run dev:h5` |
| flux-mock | Go (模拟三方) | flux-mock/ | `go run *.go` |
| flux-test | Go (业务链测试) | flux-test/ | `go run test_quick.go {channel}` |
| file-server | Go (文件上传) | file-server/ | `go run *.go` |
| flux-wechat-api | 微信服务 | flux-wechat-api/ | — |
## 核心架构
```
上游渠道 (10个) → UpstreamService → 下游对接 (美信/小薇/易贷通/CryptoApi) → 三方机构
```
- **上游**: 小爱、速贷、趣来借、臻品借、微融花、腰贷钱包、演示、放心借、龙享花、可贷
- **下游**: 美信钱包、美信B、小薇钱包、易贷通、有鑫钱包、源融花、闪融花、Mock
- **加密**: AES-CBC/ECB, CBC模式, 部分Base64密钥
## 代码审查要点
### Redkale 框架 (flux-api)
- FilterNode 使用规范 → 见 `/root/.kms/Redkale使用易错点.md`
- JSON 字段映射用 `@ConvertColumn`,注意命名转换
- 数据库连接通过 `@Resource(name="lake")` 注入
- 自定义 Render 用 `cn.casehub.base.TplRender`
### 前端 (flux-admin)
- 组件库: Arco Design Pro
- 状态管理: Pinia
- 路由: Vue Router 4
### Go 服务
- 交叉编译: `$env:GOOS="linux"; $env:GOARCH="amd64"; go build`
- 配置文件: 各模块独立 config.json
## 命名规范
```
{业务域}-{类型}-{名称}
```
| 前缀 | 含义 |
|------|------|
| flux | Flux 架构核心项目 |
| 后缀 | 含义 |
|------|------|
| -api | 后端 API 服务 |
| -admin | 管理后台前端 |
| -web | 用户端 Web |
| -mp | 小程序 |
| -task | 定时任务 |
| -kit | 工具库/SDK |
## 高危操作检查清单
- [ ] 改 Docker/系统配置 → 先备份,验证语法,再重启
- [ ] 删数据库/容器 → 确认备份和影响范围
- [ ] 生产环境操作 → 必须先在测试环境验证
- [ ] 发版部署 → 通知相关人,准备回滚方案
- [ ] 数据库 DML → 先 SELECT 预览,确认 WHERE 条件
## 模型选择参考
| 场景 | 推荐模型 |
|------|---------|
| 关键决策/技术选型 | glm-5 / deepseek-r1 |
| 复杂推理 | deepseek-r1 |
| 图片理解 | kimi-k2.5 / qwen-max |
| 日常开发操作 | kimi-k2.5 / deepseek-v3 |
| 简单任务 | glm-4.7-flash / qwen-turbo |
+17
View File
@@ -0,0 +1,17 @@
# redis-proxy 连接配置模板
# 用法:复制为 redis-proxy.toml 后填入真实值(redis-proxy.toml 已在 .gitignore,不入库)
[server]
port = 3310
host = "0.0.0.0"
[pool]
idle_timeout_secs = 300
check_interval_secs = 60
[[connections]]
name = "flux_dev"
host = "<TEST_REDIS_HOST>"
port = 6379
password = "<REDIS_PASSWORD>"
db = 0
+25
View File
@@ -0,0 +1,25 @@
# ssh-proxy 连接配置模板
# 用法:复制为 ssh-proxy.toml 后填入真实值(ssh-proxy.toml 已在 .gitignore,不入库)
# 注意:server name 用短名(如 byr_pro),作为内部连接标识符
[server]
port = 3308
host = "0.0.0.0"
[pool]
idle_timeout_secs = 300
check_interval_secs = 60
[[servers]]
name = "flux_dev"
host = "<TEST_SERVER_HOST>"
port = 22
user = "root"
private_key = "/root/.ssh/id_ed25519"
[[servers]]
name = "byr_pro"
host = "<PROD_SERVER_HOST>"
port = 22
user = "root"
private_key = "/root/.ssh/id_ed25519"
+24
View File
@@ -0,0 +1,24 @@
# WorkPod Alpine 实例
services:
workpod-alpine:
image: workpod-alpine:latest
container_name: workpod-alpine
hostname: workpod-alpine
privileged: true
tty: true
stdin_open: true
restart: unless-stopped
ports:
- "7683:7681"
- "7684:7682"
- "2223:22"
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
volumes:
- ./data/workpod-alpine/workspace:/workspace
- ./data/workpod-alpine/root:/root
networks:
default:
name: workpod-network
-55
View File
@@ -1,55 +0,0 @@
services:
workpod:
build:
context: .
dockerfile: Dockerfile
image: workpod:latest
container_name: workpod
hostname: workpod
# 特权模式
privileged: true
# 端口映射
ports:
- "2222:22" # SSH
- "8080:80" # HTTP
- "8443:443" # HTTPS
- "13306:3306" # MySQL
- "16379:6379" # Redis
# 数据卷挂载
volumes:
- /e/docker-data/workpod/mysql:/var/lib/mysql
- /e/docker-data/workpod/redis:/var/lib/redis
- /e/docker-data/workpod/workspace:/workspace
- ./config/supervisor:/etc/supervisor/conf.d
# 环境变量
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
# PTY 支持
tty: true
stdin_open: true
# 资源限制
deploy:
resources:
limits:
memory: 8G
reservations:
memory: 2G
# 重启策略
restart: unless-stopped
# 工作目录
working_dir: /workspace
networks:
default:
name: workpod-network
+70
View File
@@ -0,0 +1,70 @@
# WorkPod - 容器化开发环境
> 基于 Alpine 3.23 的轻量级开发容器,支持多项目隔离部署。
## 是什么
WorkPod 是一套 **Docker 容器化的全栈开发环境**,每个项目运行在独立容器中,通过 Web 终端(ttyd)或 SSH 访问。
## 核心特性
| 特性 | 说明 |
|------|------|
| 轻量镜像 | ~436MBAlpine 多阶段构建) |
| 开箱即用 | Node.js 24 + Claude Code + ttyd + tmux |
| 工具外挂 | Rust/Go/Python 安装到 /root,自动检测 PATH |
| 项目隔离 | 每个项目独立容器、独立端口、独立网络 |
| 会话管理 | tmux session 支持 URL 参数 / 交互菜单 |
## 快速开始
```bash
# 1. 构建基础镜像
docker compose -f instances/base/docker-compose.yml build
# 2. 启动基础实例
docker compose -f instances/base/docker-compose.yml up -d
# 3. 打开 Web 终端
# http://localhost:7681 (用户: jc, 密码: 1234567)
# 或 SSH 连接
# ssh root@localhost -p 2222 (密码: workpod123)
```
## 目录结构
```
workpod/
├── Dockerfile # 基础镜像构建
├── entrypoint.sh # 统一入口脚本
├── ttyd-session.sh # tmux 会话管理
├── instances/ # ★ 实例配置(每个项目一个目录)
│ ├── registry.yaml # 全局实例注册表
│ ├── base/ # 基础实例
│ ├── flux/ # Flux 项目实例
│ └── .template/ # 新建实例模板
├── docs/ # 文档体系
└── data/ # 运行时数据
```
## 文档索引
| 文档 | 位置 | 说明 |
|------|------|------|
| 实例管理指南 | `instances/README.md` | 端口规则、新建流程 |
| 实例注册表 | `instances/registry.yaml` | 所有实例的端口/状态 |
| 端口分配规则 | `docs/00-规范/端口分配规则.md` | 详细端口规划 |
| 实例创建流程 | `docs/00-规范/实例创建流程.md` | 步骤教程 |
| 架构设计 | `docs/01-架构/` | C4 图 + 组件关系 |
| 问题记录 | `docs/02-技术文档/ISSUES.md` | 历史问题与解决方案 |
| 部署指南 | `docs/03-运维/` | 本机 + 测试服部署 |
| 测试服实例 | `docs/03-运维/测试服实例.md` | 远程实例配置 |
| 审核报告 | `docs/04-审核/` | 安全/架构/Docker/Shell 审核 |
## 相关链接
- 基础镜像:`workpod-alpine:latest`(基于 alpine:3.23
- AI 工具:Claude Code (@anthropic-ai/claude-code)
- Web 终端:ttyd (Alpine apk)
- 会话管理:tmux + ttyd-session.sh
+118
View File
@@ -0,0 +1,118 @@
# 实例创建流程
> 为新项目创建 WorkPod 开发容器的标准步骤。
---
## 前置条件
- Docker 已安装并运行
- 基础镜像已构建:`docker images \| grep workpod-alpine`
---
## 标准流程(3 步)
### Step 1: 创建实例目录
```bash
cd E:/wk-lab/workpod
mkdir -p instances/{name}
cp instances/.template/docker-compose.yml instances/{name}/
```
### Step 2: 编辑 compose 文件
打开 `instances/{name}/docker-compose.yml`,修改占位符:
| 占位符 | 改为 | 示例 |
|--------|------|------|
| `{name}` | 实例名 | `suke` |
| `{SSH_PORT}` | SSH 端口 | `2211` |
| `{WEB_PORT}` | Web 端口 | `7711` |
| `{PROJECT_PATH}` | 项目路径 | `E:/wk-suke` |
按需取消注释:
- **Java 项目** → 取消注释 JDK/Maven volume 和 JAVA_HOME/MAVEN_HOME env
- **home 持久化** → 确保 `../../data/home/{name}:/root` 已配置
### Step 3: 注册并启动
编辑 `instances/registry.yaml`,在 `local:` 下添加新条目:
```yaml
- name: ws-{name}-dev
display: {中文名}
type: project
project: {name}
project_path: E:/wk-{name}
image: ws-{name}-dev:latest
ssh_port: {SSH_PORT}
web_port: {WEB_PORT}
network: workpod-{name}-network
status: starting
compose: instances/{name}/docker-compose.yml
```
启动:
```bash
cd instances/{name} && docker compose up -d --build
```
验证:
```bash
docker ps --filter name=ws-{name}
# 应看到端口映射正确
```
更新状态:`registry.yaml``status: starting``running`
---
## 可选:自定义镜像
如果项目需要额外工具(如 JDK),创建 `instances/{name}/Dockerfile`
```dockerfile
FROM workpod-alpine:latest
LABEL org.opencontainers.image.title="ws-{name}-dev" \
org.opencontainers.image.description="{描述}"
EXPOSE 22 7681
ENTRYPOINT ["/entrypoint.sh"]
```
compose 中添加 build 配置:
```yaml
build:
context: ../../
dockerfile: instances/{name}/Dockerfile
image: ws-{name}-dev:latest
```
---
## 完整示例:创建 suke 实例
```bash
# 1. 创建目录 + 复制模板
mkdir -p instances/suke
cp instances/.template/docker-compose.yml instances/suke/
# 2. 替换占位符(手动或 sed
# {name} → suke, {SSH_PORT} → 2211, {WEB_PORT} → 7711, {PROJECT_PATH} → E:/wk-suke
# 3. 注册到 registry.yaml(添加 local 条目)
# 4. 启动
cd instances/suke && docker compose up -d --build
# 5. 验证
docker ps --filter name=ws-suke
# curl -s http://localhost:7711 | head -5 # ttyd 可访问
```
+61
View File
@@ -0,0 +1,61 @@
# 端口分配规则
> 最后更新:2026-04-07
---
## 规则总览
### 本机端口
```
SSH: 22xx (容器内统一为 22)
Web: 77xx (容器内统一为 7681)
```
| 类型 | SSH 前缀 | Web 前缀 | 范围 | 示例 |
|------|---------|---------|------|------|
| 基础/通用 | 220-221 | 768-769 | 2201-2210 / 7681-7690 | base=2222/7681 |
| **项目开发** | **221-229** | **770-779** | **2211-2290 / 7701-7790** | flux=2201/7701 |
### 测试服端口 (flux_dev @ 39.99.243.191)
复用本机规则,Web 端口做偏移避免冲突:
| 实例 | SSH | Web | 域名 |
|------|-----|-----|------|
| workpod-1 | 2221 | 7681 | wk.1216.top |
| workpod-alpine | 2222 | 7683 | wk2.1216.top |
| workpod-ada | 2224 | 7685 | ada.1216.top |
| workpod-yxl | 2226 | 7686 | yxl.1216.top |
---
## 已分配端口表
### 本机
| # | 实例 | SSH | Web | 网络 | 状态 |
|---|------|-----|-----|------|------|
| 1 | workpod-alpine (base) | 2222 | 7681 | workpod-alpine-network | running |
| 2 | workpod-test | 2223 | 7682 | workpod-test-network | running |
| 3 | ws-flux-dev | 2201 | 7701 | workpod-flux-network | running |
### 预留
| # | 项目 | SSH | Web | 技术栈 |
|---|------|-----|-----|--------|
| 4 | suke | 2211 | 7711 | Java + Flutter |
| 5 | case | 2212 | 7712 | Java + Vue |
| 6 | hszd | 2213 | 7713 | Vue |
| 7 | pm | 2214 | 7714 | 待确认 |
| 8 | me | 2215 | 7715 | 待确认 |
---
## 分配原则
1. **项目实例从 2211 开始** — 2201-2210 保留给 base/test 等通用实例
2. **SSH 和 Web 同编号** — flux 用 2201/7701suke 用 2211/7711,便于记忆
3. **测试服 Web 偏移** — 同一实例测试服 Web = 本机 Web + 偏移值(通常 +2)
4. **注册后再占用** — 新增前先查 registry.yaml 和 `netstat -ano \| grep :22xx`
+122
View File
@@ -0,0 +1,122 @@
# WorkPod Alpine 问题记录
> 最后更新:2026-04-02
---
## Alpine 无 `ss` 命令
HEALTHCHECK 使用 `ss -tlnp` 报错 `ss: not found`
**解决**:改用 busybox 内置的 `netstat -tlnp`
---
## docker-compose healthcheck 覆盖镜像 HEALTHCHECK
修复了 Dockerfile 中的 HEALTHCHECK,但容器仍报 `ss` 错误。
**原因**docker-compose.yml 中的 `healthcheck:` 字段会覆盖镜像的 HEALTHCHECK 指令。
**解决**:同步修改 docker-compose.yml 中的 healthcheck 命令。
---
## `COPY --chmod` 需要 BuildKit
`COPY --chmod=755 entrypoint.sh /entrypoint.sh` 报错 `the --chmod option requires BuildKit`
**解决**:拆为两步:
```dockerfile
COPY entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
```
---
## BuildKit 缓存不失效 HEALTHCHECK
修改 HEALTHCHECK 后重新 build,镜像中仍是旧命令。
**原因**BuildKit 缓存了 HEALTHCHECK 的 metadata 层。
**解决**:使用 `DOCKER_BUILDKIT=0` 经典构建器。
---
## ttyd execvp failed: No such file or directory
ttyd 启动命令使用了容器中不存在的程序(如 `claude``tmux`)。
**原因**:ttyd 子进程的 PATH 可能不完整,或镜像未安装对应软件。
**解决**:确保 ttyd 启动的命令在镜像中已安装,且使用完整路径或先验证可用性。
---
## ttyd 静态资源 404
浏览器访问 ttyd 时 `style.css``js` 等 404。
**说明**ttyd 1.7.4 将资源内联到 HTML 中,单独文件的 404 是预期行为,页面功能正常。
---
## 容器重建丢失数据
`docker compose down && up` 后 /root 下的 Rust 安装等文件丢失。
**原因**:只有 bind mount 的路径才会持久化,未挂载的目录随容器销毁。
**解决**:将需要持久化的目录通过 volumes 挂载。
---
## ttyd 断线后会话丢失
ttyd 默认启动 `bash`,断线后进程被 kill,会话无法恢复。
**解决**:使用 `tmux new -A -s workpod` 替代 `bash`,需要镜像安装 `tmux`
---
## Web 终端中文输入不了
ttyd + tmux 环境下无法输入中文字符。
**原因**:容器未设置 `LANG` 环境变量,终端无 UTF-8 编码支持。
**解决**:添加 `LANG=C.UTF-8` 到 Dockerfile ENV 和 docker-compose environment。
---
## tmux 多用户共享同一 session
使用 `tmux new -A -s workpod` 时所有用户进入同一个 session,互相可见。
**解决**:通过 `ttyd-session.sh` 脚本实现 session 选择——支持 URL 参数 `?session=name` 直接进入,或交互式菜单选择/创建。
注意:`tmux attach-session -d` 会踢掉旧连接,应使用 `tmux attach-session`(不加 `-d`)实现共享模式。
---
## 容器重建后手动安装的工具丢失
在容器内手动安装的工具,解压到 `/usr/local` 等非挂载目录的会在重建后丢失。
**解决**:所有工具解压到 `/root` 下,entrypoint 自动检测并设置 PATH。
### 安装方式
| 工具 | 安装命令 | 存储位置 |
|------|---------|---------|
| Rust | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh` | `/root/.cargo` `/root/.rustup` |
| Python | `curl https://pyenv.run` | `/root/.pyenv` |
| Go | `tar -xzf go1.xx.linux-amd64.tar.gz -C /root` | `/root/go` |
| pip 用户包 | `pip install --user xxx` | `/root/.local` |
### 原则
- 解压目录用 `/root` 而不是 `/usr/local`
- 不要用 `apk add` 装开发工具(重建会丢),需要的加到 Dockerfile
- entrypoint 自动检测 `/root` 下的工具目录,写入 `/etc/profile.d/dev-tools.sh`
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
# 测试服实例配置
> 从 memory/server-workpod-instances.md 迁移,保持同步。
---
## 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 | flux_dev |
| IP | 39.99.243.191 |
| 配置目录 | /opt/workpod/ |
| 共用镜像 | workpod-alpine:latest (或 workpod-alpine:3.23) |
## 实例列表
| 实例 | SSH 端口 | Web 端口 | 域名 | ttyd 账号 | 备注 |
|------|---------|---------|------|----------|------|
| workpod-1 | 2221 | 7681 | wk.1216.top | jc:1234567 | 默认 entrypoint.sh |
| workpod-alpine | 2222 | 7683 | wk2.1216.top | jc:1234567 | 默认 entrypoint.sh |
| workpod-ada | 2224 | 7685 | ada.1216.top | **ada:123** | 独立 ttyd 凭据 |
| workpod-yxl | 2226 | 7686 | yxl.1216.top | **yxl:123** | 独立 entrypoint-yxl.sh |
## 密钥配置
ada 和 yxl 的 Claude Code 密钥通过 docker-compose environment 注入:
```yaml
environment:
- ANTHROPIC_AUTH_TOKEN=xxx
- ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
- API_TIMEOUT_MS=3000000
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
```
workpod-1 和 workpod-alpine 的密钥在 /root/.claude/settings.json 中。
## Nginx 配置
- 配置目录:`/etc/nginx/conf.d/`
- SSL 证书:`/etc/nginx/sslkey/_.1216.top.pem`(通配符证书)
- WebSocket 代理必须配置:
```nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
```
## 更新容器注意事项
1. 镜像更新:`docker load < image.tar` 后逐个修改 compose 的 image tag
2. 脚本更新:entrypoint.sh 是共用的,ttyd-session.sh 是共用的
3. ttyd 账号不同:ada 用 `ada:123`yxl 用 `yxl:123`,其他用 `jc:1234567`
4. 权限:上传脚本后必须 `chmod 755`
5. Docker Compose 版本:测试服用 `docker-compose`v1),不是 `docker compose`
6. 网络:所有实例共用 `workpod-network`
## 部署步骤
```bash
# 1. 上传代码到 /opt/workpod/
scp -r ./workpod/* root@39.99.243.191:/opt/workpod/
# 2. SSH 登录
ssh root@39.99.243.191
# 3. 加载镜像(如有新镜像)
docker load < /opt/workpod/workpod-alpine-latest.tar.gz
# 4. 设置权限
chmod 755 /opt/workpod/entrypoint.sh /opt/workpod/ttyd-session.sh
# 5. 启动各实例
cd /opt/workpod && docker-compose -f docker-compose.yml up -d # workpod-1
cd /opt/workpod && docker-compose -f docker-compose-alpine.yml up -d # workpod-alpine
cd /opt/workpod && docker-compose -f docker-compose-ada.yml up -d # workpod-ada
cd /opt/workpod && docker-compose -f docker-compose-yxl.yml up -d # workpod-yxl
```
+79
View File
@@ -0,0 +1,79 @@
# 部署指南
> 本机 + 测试服部署步骤。
---
## 本机部署
### 前置条件
- Docker Desktop for Windows
- Git Bash 或 WSL2 终端
### 首次部署
```bash
cd E:/wk-lab/workpod
# 1. 构建基础镜像
docker compose -f instances/base/docker-compose.yml build
# 2. 启动基础实例
docker compose -f instances/base/docker-compose.yml up -d
# 3. 启动项目实例(按需)
docker compose -f instances/flux/docker-compose.yml up -d --build
```
### 日常使用
```bash
# 查看所有实例
docker ps --filter name=workpod --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 启动/停止单个实例
docker compose -f instances/{name}/docker-compose.yml up -d
docker compose -f instances/{name}/docker-compose.yml down
# 重建(修改 Dockerfile 后)
docker compose -f instances/{name}/docker-compose.yml up -d --build
```
### 连接方式
| 方式 | 命令/地址 |
|------|----------|
| Web 终端 | http://localhost:{WEB_PORT} (用户: jc, 密码: 1234567) |
| SSH | `ssh root@localhost -p {SSH_PORT}` (密码: workpod123) |
| Exec | `docker exec -it {container_name} bash` |
---
## 测试服部署
详见 [测试服实例.md](./测试服实例.md)
---
## 镜像构建与分发
### 构建基础镜像
```bash
cd E:/wk-lab/workpod
docker compose -f instances/base/docker-compose.yml build
```
### 导出镜像(用于测试服)
```bash
docker save workpod-alpine:latest | gzip > workpod-alpine-latest.tar.gz
```
### 测试服加载
```bash
# 上传后
docker load < workpod-alpine-latest.tar.gz
```
+428
View File
@@ -0,0 +1,428 @@
# WorkPod 部署维护手册
> 版本:1.0 | 更新日期:2026-04-07
---
## 1. 系统概览
WorkPod 是基于 Docker 的轻量级开发环境容器系统,提供 Web 终端 (ttyd) 和 SSH 双入口,支持多实例部署。
### 1.1 架构总览
```
┌─────────────────────────────────────────────────────┐
│ 宿主机 (Windows) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ workpod-alpine│ │ ws-flux-dev │ │ 基础设施 │ │
│ │ :2222 / :7681│ │ :2201 / :7701│ │ mysql/redis│ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ ↑ ↑ │
│ Alpine 3.23 + Node24 + JDK17/Maven │
│ ~438MB ~438MB │
└─────────────────────────────────────────────────────┘
```
### 1.2 实例清单
| 实例 | 类型 | SSH | Web | 镜像 | 内存限制 |
|------|------|-----|-----|------|---------|
| workpod-alpine | 基础实例 | :2222 | :7681 | workpod-alpine:latest (438MB) | 4G |
| ws-flux-dev | Flux 项目 | :2201 | :7701 | ws-flux-dev:latest (438MB) | 4G |
| workpod-test | 测试实例 | :2223 | :7682 | ae5a15cc0c94 (旧镜像) | 4G |
---
## 2. 快速操作
### 2.1 日常启停
```bash
# 启动全部实例
cd E:/wk-lab/workpod/instances/base && docker compose up -d
cd E:/wk-lab/workpod/instances/flux && docker compose up -d
# 停止单个实例
docker compose -f instances/base/docker-compose.yml down
docker compose -f instances/flux/docker-compose.yml down
# 重启(推荐:先 down 再 up)
docker compose -f instances/base/docker-compose.yml down && docker compose -f instances/base/docker-compose.yml up -d
```
### 2.2 连接方式
```bash
# ====== 基础实例 ======
Web 终端: http://localhost:7681 # 用户 jc / 密码 1234567
SSH: ssh root@localhost -p 2222 # 密码 workpod123
进入容器: MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash
# ====== Flux 实例 ======
Web 终端: http://localhost:7701 # 用户 jc / 密码 1234567
SSH: ssh root@localhost -p 2201 # 密码 workpod123
进入容器: MSYS_NO_PATHCONV=1 docker exec -it ws-flux-dev bash
```
### 2.3 一键检查状态
```bash
# 容器状态
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 资源占用
docker stats --no-stream --format "table {{.Name}}\t{{CPUPerc}}\t{{MemUsage}}\t{{NetIO}}"
# 镜像列表
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
```
---
## 3. 构建与更新
### 3.1 镜像构建
```bash
# 基础镜像(Alpine 3.23 + Node 24 + Claude Code
cd E:/wk-lab/workpod
docker build -t workpod-alpine:latest .
# Flux 扩展镜像(基础镜像 + LABELJDK 通过 volume 挂载)
docker build -t ws-flux-dev:latest -f Dockerfile.flux .
```
**构建产物** (~438MB):
- alpine:3.23 基础 (~7MB)
- 运行时工具 curl/git/ssh/ttyd/tmux/bash (~80MB)
- Node.js v24.14.1 (musl) + claude-code + coding-helper (~200MB)
- entrypoint.sh + ttyd-session.sh (<10KB)
### 3.2 更新 Claude Code / coding-helper
```bash
# 重新构建即可获取最新版(@latest 标签)
docker build --no-cache -t workpod-alpine:latest .
docker build --no-cache -t ws-flux-dev:latest -f Dockerfile.flux .
# 重启容器生效
docker compose -f instances/base/docker-compose.yml down && docker compose -f instances/base/docker-compose.yml up -d
docker compose -f instances/flux/docker-compose.yml down && docker compose -f instances/flux/docker-compose.yml up -d
```
### 3.3 滚动更新(不中断服务)
```bash
# 1. 构建新镜像
docker build -t workpod-alpine:latest .
# 2. 创建新容器(旧容器仍在运行)
docker compose -f instances/base/docker-compose.yml up -d --no-deps --build workpod-alpine
# 3. 确认健康后删除旧容器(自动完成,compose 管理)
```
---
## 4. 运维操作
### 4.1 日志查看
```bash
# 实时日志
docker logs -f workpod-alpine
docker logs -f ws-flux-dev
# 最近 50 行
docker logs --tail 50 workpod-alpine
# 按时间过滤
docker logs --since 2026-04-07T08:00:00 workpod-alpine
```
**日志策略**: json-file 驱动,单文件最大 10MB,保留 3 个文件(每实例最多 30MB)
### 4.2 进入容器调试
```bash
# 交互式 shell(推荐 login shell 以加载 PATH
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash -lc 'echo $PATH; java -version; node -v'
# 执行单条命令
MSYS_NO_PATHCONV=1 docker exec workpod-alpine bash -lc 'claude --version'
```
> **注意**: Windows Git Bash 下必须加 `MSYS_NO_PATHCONV=1`,否则路径转换会导致错误。
### 4.3 文件传输
```bash
# 宿主机 → 容器
docker cp ./local-file.txt workpod-alpine:/workspace/
# 容器 → 宿主机
docker cp workpod-alpine:/workspace/output.txt ./
```
### 4.4 tmux 会话管理
容器内 ttyd 使用 tmux 管理终端会话:
```bash
# 在 Web 终端或 SSH 中操作
tmux ls # 列出所有会话
tmux attach -t session_name # 加入会话
tmux new -s my_session # 新建会话
# URL 快速访问指定会话
http://localhost:7681/?session=my_session
```
### 4.5 数据持久化
| 实例 | 宿主机路径 | 容器路径 | 用途 |
|------|-----------|---------|------|
| base | `data/workspace` | `/workspace` | 工作目录 |
| flux | `E:/wk-flux` | `/workspace` | Flux 项目源码 |
| flux | `data/home/flux-dev` | `/root` | 用户配置 (.gitconfig 等) |
---
## 5. 故障排查
### 5.1 容器无法启动
```bash
# 查看启动日志
docker logs workpod-alpine
# 常见问题:
# exit 127 → 命令未找到(镜像损坏或 entrypoint 缺失)
# exit 1 → 健康检查失败(sshd 或 ttyd 未启动)
# exit 255 → 信号处理异常
```
**workpod-test 退出码 127 排查**:
该实例使用旧镜像 ID `ae5a15cc0c94`,可能缺少 entrypoint。修复方法:
```bash
# 方案 A:改用最新基础镜像
# 编辑 instances/test/docker-compose.yml,将 image 改为 workpod-alpine:latest
# 方案 B:重新构建并替换
cd E:/wk-lab/workpod
docker build -t workpod-test:latest .
# 然后修改 test/docker-compose.yml 的 image 字段
```
### 5.2 端口冲突
```bash
# 查看端口占用
netstat -ano | grep :7681
# 端口分配表(避免冲突)
# 2222 / 7681 — workpod-alpine (基础)
# 2201 / 7701 — ws-flux-dev (Flux)
# 2223 / 7682 — workpod-test (测试)
# 新实例建议用 22xx / 77xx / 78xx
```
### 5.3 健康检查失败
```bash
# 手动检查端口
MSYS_NO_PATHCONV=1 docker exec workpod-alpine netstat -tlnp
# 应看到:
# tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN sshd
# tcp 0 0 0.0.0.0:7681 0.0.0.0:* LISTEN ttyd
# 如果 sshd/ttyd 未运行,查看进程
MSYS_NO_PATHCONV=1 docker exec workpod-alpine ps aux
```
### 5.4 内存不足
```bash
# 查看内存使用
docker stats --no-stream
# 当前限制:每个实例 4GB 上限 / 1GB 预留
# 如需调整,编辑对应 docker-compose.yml 的 deploy.resources.limits.memory
```
### 5.5 PATH 环境变量丢失
entrypoint.sh 启动时会自动检测工具链并写入 `/etc/profile.d/dev-tools.sh`。如果 docker exec 中找不到命令:
```bash
# 使用 login shell (-lc) 加载完整环境
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash -lc 'java -version'
# 或手动 source
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash -c 'source /etc/profile && java -version'
```
---
## 6. 清理维护
### 6.1 日常清理命令
```bash
# 清理悬空镜像(已停止容器的孤立层)
docker image prune -f
# 清理未使用的镜像(除运行中容器外的所有未标记镜像)
docker image prune -a -f # ⚠️ 会删除未运行的镜像
# 清理停止的容器
docker container prune -f
# 清理未使用的网络
docker network prune -f
# 全面清理(悬停资源 + 停止容器 + 未使用网络 + 构建缓存)
docker system prune -f
# 深度清理(包含未使用的镜像)⚠️ 慎用
docker system prune -a -f
```
### 6.2 日志清理
```bash
# 手动清空某容器日志(不重启)
truncate -s 0 $(docker inspect --format='{{.LogPath}}' workpod-alpine)
# 或设置全局日志限制(需编辑 daemon.json
# /etc/docker/daemon.json:
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
```
### 6.3 废弃资源清理记录
| 时间 | 操作 | 释放空间 |
|------|------|---------|
| 2026-04-07 | `docker image prune -f` | 802.8MB (28个悬空层) |
**待清理**:
- `workpod:latest` (2.6GB) — 旧 Ubuntu 版基础镜像
- `dev-box:latest` (2.79GB) — 旧开发箱镜像
- `workpod-alpine:size-test` (436MB) — 大小测试镜像
```bash
# 清理上述废弃镜像(确认无容器引用后)
docker rmi workpod:latest dev-box:latest workpod-alpine:size-test
# 预计释放 ~5.8GB
```
---
## 7. 远程服务器部署
### 7.1 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 39.99.243.191 |
| SSH 别名 | flux_dev |
| 部署路径 | /opt/workpod/ |
### 7.2 远程实例
| 实例 | SSH 端口 | Web 端口 | 域名 |
|------|---------|---------|------|
| workpod-1 | 2221 | 7681 | wk.1216.top |
| workpod-alpine | 2222 | 7683 | wk2.1216.top |
| workpod-ada | 2224 | 7685 | ada.1216.top |
| workpod-yxl | 2226 | 7686 | yxl.1216.top |
### 7.3 远程运维命令
```bash
# 通过 ssh-proxy 操作远程服务器
ssh-proxy exec -n flux_dev -c "docker ps --format 'table {{.Names}}\t{{.Status}}'"
ssh-proxy exec -n flux_dev -c "docker logs --tail 30 workpod-1"
ssh-proxy exec -n flux_dev -c "docker system prune -f"
```
---
## 8. 安全备忘
| 项目 | 当前配置 | 风险等级 | 建议 |
|------|---------|---------|------|
| privileged 模式 | 仅基础实例开启 | 高 | 确认是否必需,否则移除 |
| root 用户运行 | 全部实例 | 中 | 开发环境可接受 |
| SSH 密码登录 | PermitRootLogin yes | 内网可接受 | 生产环境禁用 |
| ttyd 认证 | Basic Auth (jc:1234567) | 低-内网 | 可配置强密码 |
| API Key 传递 | docker-compose env | 低 | 不写入文件系统 |
| JDK/Maven 挂载 | ro 只读 | 低 | 安全做法 |
---
## 9. 目录结构速查
```
E:/wk-lab/workpod/
├── Dockerfile # 基础镜像构建 (多阶段)
├── Dockerfile.flux # Flux 扩展镜像
├── entrypoint.sh # 容器入口脚本
├── ttyd-session.sh # tmux 会话管理
├── auth-proxy.js # 认证代理 (可选)
├── wk.1216.conf # Nginx 反向代理配置
├── SPECS.md # 技术规格文档
├── instances/ # ★ 实例中心
│ ├── .template/ # 实例模板
│ ├── registry.yaml # 实例注册表
│ ├── base/docker-compose.yml # 基础实例
│ ├── flux/docker-compose.yml # Flux 实例
│ └── test/docker-compose.yml # 测试实例
├── docs/ # 文档
│ ├── 00-规范/
│ ├── 01-架构/
│ ├── 02-技术文档/
│ ├── 03-运维/ # ← 本文档
│ └── 04-审核/
├── config/ # 配置模板
├── static/ # auth-proxy 静态页
├── data/ # 运行时数据
│ ├── workspace/ # 基础实例工作区
│ └── home/ # 用户 home 目录
└── _archive/ # 历史版本归档
```
---
## 附录:常用命令速查卡
```bash
# ════════════ 构建 ════════════
docker build -t workpod-alpine:latest .
docker build -t ws-flux-dev:latest -f Dockerfile.flux .
# ════════════ 启停 ════════════
docker compose -f instances/base/docker-compose.yml up -d
docker compose -f instances/flux/docker-compose.yml up -d
docker compose -f instances/base/docker-compose.yml down
# ════════════ 查看 ════════════
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
docker stats --no-stream
docker logs -f workpod-alpine
# ════════════ 进入 ════════════
MSYS_NO_PATHCONV=1 docker exec -it workpod-alpine bash
ssh root@localhost -p 2222 # workpod123
# http://localhost:7681 # jc / 1234567
# ════════════ 清理 ════════════
docker image prune -f # 悬空镜像
docker system prune -f # 全面清理
docker rmi <image> # 删除指定镜像
# ════════════ 远程 ════════════
ssh-proxy exec -n flux_dev -c "docker ps"
```
+352
View File
@@ -0,0 +1,352 @@
# 安全性审核报告 (WorkPod Alpine)
> 审核日期:2026-04-07
> 审核范围:Dockerfile、entrypoint.sh、ttyd-session.sh、docker-compose.yml、auth-proxy.js、ISSUES.md
---
## 审核概要
| 严重等级 | 数量 |
|---------|------|
| 严重 (Critical) | 1 (S-02) |
| 高危 (High) | 3 (H-02, H-03, H-04, H-05, H-06) |
| 中危 (Medium) | 5 (S-01[降级], M-01~M-05) |
| 低危 (Low) | 5 (L-01~L-05) |
**总计:21 项安全问题(其中 S-01/S-03/S-04/H-01/M-06 已修复/改进/不适用)**
---
## 问题清单
### [中危] S-01: 硬编码密码明文存储在源码中 **[已修复]**
- **位置:**
- `Dockerfile:34``echo 'root:${ROOT_PASSWORD}' | chpasswd` (构建时仍保留默认值)
- `entrypoint.sh` — ttyd 密码通过 `${TTYD_CREDENTIALS:-jc:1234567}` 环境变量注入
- **描述:** ~~所有认证凭证均以明文硬编码在源码文件中~~**已修复**:密码现已通过环境变量 `${ROOT_PASSWORD}``${TTYD_CREDENTIALS:-jc:1234567}` 注入,`entrypoint.sh` 不再硬编码密码。但需注意 `Dockerfile:34``chpasswd` 仍保留默认值(构建时),运行时通过环境变量覆盖。
- **影响:** 攻击者获取源码不再直接获得运行时凭据。降级为 **[中危]**Dockerfile 构建时默认值仍存在,但运行时可被 env 覆盖。
- **建议修复:**
1. Dockerfile 中 chpasswd 也改为环境变量引用或移除默认值
2.`.env` 文件加入 `.gitignore`,使用 `docker-compose --env-file` 加载
3. 对已提交的密码执行 `git filter-branch` 或 BFG 清理历史
4. 若密码已泄露,立即轮换所有凭证
### [严重] S-02: privileged: true 特权模式运行
- **位置:**
- `docker-compose.yml:11``privileged: true`
- `docker-compose-alpine.yml:7``privileged: true`
- **描述:** 容器以特权模式运行,拥有宿主机全部 capabilities(包括 SYS_ADMIN、NET_ADMIN 等),可访问所有设备文件(/dev/*),可修改内核参数,可逃逸到宿主机。
- **影响:** 容器内任何代码执行均可直接控制宿主机操作系统。结合 root 用户 + 全权限 Claude 配置,AI 工具的任意命令执行等同于宿主机 root 权限。
- **建议修复:**
1. 评估是否真的需要特权模式。若仅需要 Docker-in-Docker,使用 `docker.sock` 挂载替代
2. 若必须使用特权模式,限制为最小必要 capabilities(如仅 `SYS_PTRACE`
3. 考虑使用 `--security-opt=no-new-privileges` 防止权限提升
4. 在生产环境中绝对禁止特权模式
### [严重] S-03: Claude Code settings.json 设置 allow: ["*"] — 无限制全权限 **[不适用当前版本]**
- **位置:**
- ~~`entrypoint-test.sh:41-48`~~ — 已归档到 `_archive/`
- ~~`entrypoint-test.sh:51-58`~~ — 已归档到 `_archive/`
- ~~`config/test-claude-settings.json`~~ — 外部配置文件同样 allow: ["*"]
- **描述:** ~~Claude Code 的 permissions 配置设置为 `allow: ["*"]`~~**不适用当前版本**`entrypoint-test.sh` 已归档到 `_archive/` 目录,当前主 `entrypoint.sh` 中无此 `allow: ["*"]` 配置。
- **影响:** 当前版本不受此问题影响。
- **建议修复:** 若后续重新启用类似配置,应遵循最小权限原则。
### [严重] S-04: API 密钥 (ANTHROPIC_AUTH_TOKEN) 通过环境变量暴露给所有进程 **[已修复]**
- **位置:**
- ~~`entrypoint-test.sh:67`~~ — ~~写入 .bashrc~~ → 已修复
- ~~`entrypoint-test.sh:68`~~ — ~~写入 .bashrc~~ → 已修复
- **描述:** ~~API 密钥写入 .bashrc 并全局暴露~~**已修复**:当前版本通过 `docker-compose.yml``environment` 直接传递 `ANTHROPIC_AUTH_TOKEN` 等环境变量,不再写入 `.bashrc`
- **影响:** 密钥不再持久化到 `.bashrc`,不再对所有登录用户自动暴露。但容器内子进程仍可通过 `/proc/*/environ` 读取(Docker 环境变量的固有特性)。
- **建议修复:**
1. 使用 Docker secrets 或外部密钥管理服务进一步加固
2. 限制密钥文件的权限为 `chmod 600`
---
### [高] H-01: developer 用户 NOPASSWD sudo 全权限 **[不适用当前版本]**
- **位置:** ~~`entrypoint-test.sh:34`~~ — 已归档到 `_archive/`
- **描述:** ~~developer 用户配置了无需密码的 sudo 全权限~~**不适用当前版本**`entrypoint-test.sh` 已归档到 `_archive/` 目录,当前主 `entrypoint.sh` 中无此配置。
- **影响:** 当前版本不受此问题影响。
### [高] H-02: SSH 允许 root 密码登录 + 弱密码
- **位置:**
- `Dockerfile:35``PermitRootLogin yes`
- `Dockerfile:34``chpasswd` 设置密码(默认 workpod123,可通过 `${ROOT_PASSWORD}` 覆盖)
- **描述:** SSH 服务配置为允许 root 用户使用密码登录(而非仅密钥认证)。**降级说明**:`ROOT_PASSWORD` 现在可通过环境变量覆盖,默认值仍为 `workpod123` 但可自定义。端口 22 映射到宿主机 2222 端口,对外可达。
- **影响:** 若用户未自定义 `ROOT_PASSWORD`,暴力破解攻击仍可在短时间内猜出默认密码。一旦成功,攻击者获得容器 root shell,在特权模式下进一步控制宿主机。
- **建议修复:**
1. 禁用密码登录:`PasswordAuthentication no`,仅允许密钥认证
2. 若必须保留密码登录,通过环境变量设置强密码(20+ 字符随机生成)
3. 限制 SSH 来源 IP(防火墙规则)
4. 考虑使用 fail2ban 防暴力破解
5. 更改默认 SSH 端口(虽属隐匿安全但有一定价值)
### [高] H-03: ttyd 使用 Basic Auth 且密码强度不足 **[已改进]**
- **位置:**
- `entrypoint.sh` — ttyd 密码通过 `${TTYD_CREDENTIALS:-jc:1234567}` 环境变量配置
- **描述:** ttyd 使用 `-c` 参数启用 HTTP Basic Authentication。**已改进**:默认凭据改为 `jc:1234567` 通过环境变量 `TTYD_CREDENTIALS` 配置,不再硬编码在 entrypoint.sh 中。仍存在的风险:
1. 默认密码仍偏弱(7位数字/字母组合)
2. Basic Auth 凭据以 Base64 编码传输(非加密),中间人可截获
3. 未强制 HTTPS,密码明文传输
4. ttyd `-c` 参数在 Safari 浏览器中存在兼容性问题(已知 Safari 不支持 ttyd 内置 Basic Auth
5. Web 终端端口(7681)暴露,增加攻击面
- **影响:** 默认密码可被暴力破解;未加密传输可被网络嗅探;浏览器兼容性问题可能迫使降级安全措施。
- **建议修复:**
1. 通过 TTYD_CREDENTIALS 环境变量设置强密码(16+ 字符随机字符串)
2. 在前端加反向代理(nginx/caddy)终止 TLS
3. 考虑使用 auth-proxy.js 作为认证层(已有实现),但需修复其自身安全问题
4. 限制 ttyd 端口的网络访问范围
### [高] H-04: 多实例共享网络 (workpod-network) 缺乏隔离
- **位置:** `docker-compose-alpine.yml:23-24``name: workpod-network`
- **描述:** 测试服多个 WorkPod 实例共享同一个 Docker 网络 `workpod-network`。这意味着:
1. 同一网络内的容器可以互相访问对方的开放端口
2. 一个容器被攻陷后,可作为跳板攻击同网络其他容器
3. 容器间通信不经过宿主机防火墙
- **影响:** 横向移动风险——单点突破导致整个开发环境沦陷。
- **建议修复:**
1. 为每个实例创建独立网络
2. 若需共享数据,使用专用数据卷而非网络共享
3. 在容器内部使用 iptables/nftables 限制容器间流量
4. 考虑 Docker network 的 `internal` 选项禁止外部访问
### [高] H-05: auth-proxy.js 存在多处安全隐患
- **位置:** `auth-proxy.js` 全文(已从 `_archive/` 补充回主目录)
- **描述:** 认证代理脚本存在以下安全问题:
1. **第8行**:密码硬编码在源码中 `{ wk: '1234567', admin: 'admin123' }`
2. **第15行**:Token 存储在内存 Set 中,无持久化,重启后所有 token 失效(可用性问题),但也意味着无法审计
3. **第19行**Token 生成方式可预测(`wk_用户名_时间戳`),格式固定且时间戳精度为毫秒,可被枚举猜测
4. **第77-89行**Basic Auth 凭据仅做 Base64 解码比对,无速率限制,可被暴力破解
5. **第103-105行**`/token``/ttyd/` 路径绕过 token 验证直接代理到默认工作区,形成认证旁路
6. **第49行**:代理请求时透传原始 headers(含 Authorization),可能导致凭据泄漏到后端
- **影响:** 认证层形同虚设,token 可被猜测/绕过,密码可被暴力破解,凭据可能泄漏到后端服务。
- **建议修复:**
1. Token 使用 crypto.randomBytes() 生成不可预测的随机 token
2. `/token``/ttyd/` 端点也必须验证 token
3. 添加登录失败速率限制(如每 IP 每分钟最多5次)
4. 过滤敏感 headerAuthorization、Cookie)不转发到后端
5. 密码使用环境变量或外部配置,不从源码读取
### [高] H-06: /root 目录挂载到宿主机 (docker-compose-alpine.yml)
- **位置:** `docker-compose-alpine.yml:20``./data/workpod-alpine/root:/root`
- **描述:** 将容器的 `/root` 目录 bind mount 到宿主机目录。这意味着:
1. 容器内的 root 用户 home 目录内容直接暴露在宿主机文件系统上
2. 包含 .claude/settings.json(含全权限配置)、.bashrc(含 API 密钥)、ssh 密钥等敏感信息
3. 宿主机上其他进程/用户可能有权读取这些文件
4. 容器内对 /root 的修改直接影响宿主机
- **影响:** 敏感配置和密钥在宿主机上的安全性取决于宿主机文件系统权限,增加了攻击面。
- **建议修复:**
1. 评估是否确实需要挂载 /root,若仅为持久化工具安装,考虑只挂载子目录
2. 设置挂载目录的严格文件权限(chmod 700)
3. 确保 /root 下的密钥文件权限为 600
---
### [中] M-01: heredoc 注入风险(部分已缓解)
- **位置:**
- `entrypoint-test.sh:41-48``cat > /root/.claude/settings.json << 'SETTINGS'` (已用引号包裹,安全)
- `entrypoint-test.sh:64-71``cat > /home/developer/.bashrc << BASHRC` **未用引号包裹,有风险**
- `entrypoint-test.sh:75-77``cat > /root/.bashrc << 'ROOTRC'` (已用引号包裹,安全)
- `entrypoint-test.sh:80-88``cat > /etc/profile.d/dev-tools.sh << 'PROFILE'` (已用引号包裹,安全)
- **描述:** `entrypoint-test.sh:64` 的 heredoc 定界符 `BASHRC` 未加引号,这意味着 heredoc 内容中的变量(`${ANTHROPIC_AUTH_TOKEN}``${ANTHROPIC_BASE_URL}` 等)会被 shell 展开。虽然当前内容是期望的行为(需要展开环境变量),但如果将来有人在此处添加包含特殊字符的用户输入,可能导致注入。
- **影响:** 当前风险较低(变量值来自受控的环境变量),但编码模式不规范,未来维护可能引入漏洞。
- **建议修复:**
1. 保持现状但添加注释说明此处有意使用未引用 heredoc 以展开变量
2. 或者改用双引号定界符 `"BASHRC"` 并显式使用 `${VAR}` 引用变量
### [中] M-02: ttyd-session.sh 命令注入风险(已部分缓解)
- **位置:** `ttyd-session.sh:9``SESSION_NAME=$(echo "$TTYD_QUERY_STRING" | tr '&' '\n' | grep '^session=' | head -1 | cut -d= -f2)`
- **描述:** 从 URL query string 提取 session 名称时,虽然第55行做了过滤(`tr -cd 'a-zA-Z0-9_\-'`),但在第37行的 `read` 命令中,用户交互输入的 `CHOICE` 变量在第44-46行被用于 `sed -n "${CHOICE}p"`,如果 CHOICE 包含恶意内容(如 `1; rm -rf /`),虽然 grep 数字检查提供了一定保护,但防御不够严谨。
- **影响:** 攻击者可能通过构造特殊的 session 名称或交互输入来注入命令。当前有 `tr -cd` 过滤和数字匹配作为缓解措施,降低了但未消除风险。
- **建议修复:**
1. 在 read 后立即对 CHOICE 做输入验证
2. 使用 `[[ "$CHOICE" =~ ^[0-9]+$ ]]` 替代 `grep -qE` 做更严格的数字判断
3. 使用数组索引代替 sed 行号引用
### [中] M-03: 缺少 .gitignore 导致敏感文件可能被提交
- **描述:** 项目根目录不存在 `.gitignore` 文件。以下敏感文件/目录可能被意外提交到 Git 仓库:
- `data/` — 可能包含工作区数据和挂载的 /root 内容
- `config/test-claude-settings.json` — 含全权限配置
- `packages/` — 可能包含二进制包
- `*.tar.gz``workpod-alpine-latest.tar.gz` 镜像导出文件
- `wk.1216.conf` — 可能含 nginx/服务器配置
- **影响:** 敏感数据(工作区代码、配置、镜像)泄露到 Git 仓库。
- **建议修复:** 创建 `.gitignore` 文件,至少排除:`data/``*.tar.gz``.env*``config/*.json`
### [中] M-04: Alpine 镜像使用第三方镜像源 (mirrors.aliyun.com)
- **位置:** `Dockerfile:6``Dockerfile:28``sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g'`
- **描述:** 构建过程中将 Alpine 官方包源替换为阿里云镜像源。虽然这是常见的国内加速做法,但引入了第三方信任链:
1. 包完整性依赖阿里云镜像的同步正确性
2. 中间人攻击面扩大(DNS 劫持、镜像源被入侵)
3. 生产环境构建应优先使用官方源或可信的企业镜像
- **影响:** 若镜像源被污染,可能在构建阶段植入恶意软件到最终镜像中(供应链攻击)。
- **建议修复:**
1. 构建时使用 `--no-cache` 并验证包签名
2. 考虑使用官方源配合网络代理
3. 至少在生产发布构建中使用官方源
### [中] M-05: 容器缺少资源隔离和安全增强配置
- **位置:** `docker-compose.yml``docker-compose-alpine.yml`
- **描述:** 容器配置缺少以下安全加固措施:
1. 未设置 `read_only: true` — 容器文件系统可写
2. 未设置 `security_opt: ["no-new-privileges:true"]` — 进程可通过 setuid/setgid 提权
3. 未设置 `cap_drop: [ALL]` + `cap_add: [特定能力]` — 保留全部 Linux capabilities
4. 未设置 `user:` — 默认以 root 运行
5. 未设置 `tmpfs: /tmp, /dev/shm` — 临时目录使用容器默认挂载
- **影响:** 容器逃逸向量增多,进程提权路径未被阻断。
- **建议修复:**
1. 添加 `security_opt: ["no-new-privileges:true"]`
2. 添加 `cap_drop: [ALL]` 仅保留必要能力
3. 考虑以非 root 用户运行主进程
4. 对 /tmp 等可写目录使用 tmpfs 并设置 `exec` 选项防止脚本执行
### [中] M-06: 日志和启动信息泄露敏感配置 **[已修复]**
- **位置:**
- ~~`entrypoint.sh:76-78`~~ — ~~打印连接方式和密码~~ → 已修复
- ~~`entrypoint-test.sh:124-126`~~ — ~~打印连接方式和密码~~ → 已归档
- **描述:** ~~启动时将连接地址和密码输出到 stdout/stderr~~**已修复**:当前 `entrypoint.sh` 不再打印密码。
- **影响:** `docker logs` 不再泄露密码。
- **建议修复:** 无需进一步操作。
---
### [低] L-01: HEALTHCHECK 命令不一致
- **位置:**
- `Dockerfile:49``netstat -tlnp`
- `docker-compose.yml:48``ss -tlnp | grep ...`
- **描述:** Dockerfile 中的 HEALTHCHECK 使用 `netstat`ISSUES.md 已记录 ss 不可用的问题),而 docker-compose.yml 中覆盖使用了 `ss` 命令。根据 ISSUES.md 记录,Alpine 中 `ss` 不可用(需要安装 iproute2),compose 中的 healthcheck 会因 `ss: not found` 而持续失败。
- **影响:** 健康检查不准确,可能导致编排系统错误判断容器状态。
- **建议修复:** 统一使用 `netstat -tlnp` 或安装 `iproute2` 后统一使用 `ss`
### [低] L-02: EXPOSE 指令声明过多端口
- **位置:** `Dockerfile:46``EXPOSE 22 7681`
- **描述:** Dockerfile 中声明暴露 22 (SSH) 和 7681 (ttyd) 端口。EXPOSE 主要是文档作用,但会给使用者错误的暗示。实际上 compose 文件映射了更多端口(2222-2226, 7681-7686)。
- **影响:** 信息误导,但不构成直接安全威胁。
- **建议修复:** 更新 EXPOSE 为实际使用的端口集合,或在注释中说明。
### [低] L-03: npm registry 使用第三方镜像源
- **位置:** `Dockerfile:17``npm config set registry https://registry.npmmirror.com`
- **描述:** npm 安装使用淘宝镜像源(npmmirror.com)。与 APK 镜像源类似,这引入了第三方供应链信任问题。
- **影响:** npm 包可能被篡改(供应链攻击),尤其 `@anthropic-ai/claude-code` 是核心依赖。
- **建议修复:** 生产构建使用官方 npm registry,或启用 npm 校验(`npm config set verify-signature true`)。
### [低] L-04: 镜像中包含不必要的构建工具痕迹
- **位置:** `Dockerfile:38``COPY --from=builder /opt/node /usr/local`
- **描述:** 多阶段构建是好的实践(builder 阶段的缓存和构建工具不会进入最终镜像),但最终镜像仍然较大(约 436MB)。应确认是否有不必要的文件被复制。
- **影响:** 攻击面增大(更多二进制文件 = 更多潜在漏洞),拉取/分发效率降低。
- **建议修复:**
1. 审查 `/opt/node` 下是否包含不必要的文件(文档、测试等)
2. 考虑使用 `COPY --from=builder /opt/node/bin /usr/local/bin` 仅复制必要文件
3. 使用 `docker scout` 分析镜像内容
### [低] L-05: Windows 伪影文件残留 **[风险降低]**
- **描述:** 项目目录中发现以下 Windows 伪影文件:
- `entrypoint.sh;C` — Windows 创建的 ADSAlternate Data Stream)伪影
- `ttyd-session.sh;C` — 同上
- **影响:** 这些文件可能是 Windows 资源管理器异常操作的产物,不影响 Linux 容器运行。**[风险降低]**entrypoint.sh 和 ttyd-session.sh 已改为 `COPY --chmod=755` 内置到镜像(4/4 修复),不再通过 bind mount 从 Windows 主机挂载,CRLF 换行符问题的影响已大幅减小。
- **建议修复:** 删除这些伪影文件,配置 `.gitattributes` 强制 LF 换行。
---
## 攻击路径分析
```
攻击者
├── [路径A: Web 终端]
│ ├── 1. 扫描发现 7681 端口开放的 ttyd
│ ├── 2. 暴力破解弱密码 (默认 jc:1234567,可通过 TTYD_CREDENTIALS 自定义)
│ ├── 3. 获得 shell → API 密钥不再在 .bashrc 中(已修复)
│ ├── 4. 使用 claude 执行命令(无 allow:["*"] 配置,需交互确认)
│ └── 5. privileged 容器 → 完全控制宿主机
├── [路径B: SSH]
│ ├── 1. 扫描发现 2222 端口开放的 SSH
│ ├── 2. 暴力破解 root 密码(默认 workpod123,可通过 ROOT_PASSWORD 自定义)
│ ├── 3. 直接获得 root shell
│ └── 4. privileged 容器 → 完全控制宿主机
└── [路径C: 认证代理]
├── 1. 发现 8080 端口的 auth-proxy
├── 2. 暴力破解 Basic Auth (wk:1234567)
├── 3. 利用 /token 端点的认证旁路
├── 4. 获取 token 后访问 ttyd
└── 5. 同路径 A 第 3-5 步
```
**关键发现:密码已外部化(环境变量注入),entrypoint-test.sh 已归档移除(无 allow:["*"]、无 NOPASSWD sudo、无 .bashrc 密钥写入)。当前主要风险仍为"弱默认密码 + 特权模式",自定义强密码可显著提升整体安全性。**
---
## 总结与优先级建议
### 立即处理 (P0 — 本周内)
| 编号 | 问题 | 状态 | 原因 |
|------|------|------|------|
| S-01 | 硬编码密码 | **[已修复]** / 降级[中危] | 密码已外部化,Dockerfile 默认值仍存 |
| S-02 | privileged: true | **待处理** | 最高风险的单一配置项 |
| S-03 | allow: ["*"] + skip-permissions | **[不适用]** | entrypoint-test.sh 已归档 |
| S-04 | API 密钥暴露 | **[已修复]** | 不再写入 .bashrc |
### 尽快处理 (P1 — 两周内)
| 编号 | 问题 | 状态 | 原因 |
|------|------|------|------|
| H-01 | NOPASSWD sudo | **[不适用]** | entrypoint-test.sh 已归档 |
| H-02 | SSH root 密码登录 | **[部分改进]** | ROOT_PASSWORD 可自定义,默认仍弱 |
| H-03 | ttyd 弱密码 + 明文传输 | **[已改进]** | TTYD_CREDENTIALS 可配置,默认仍偏弱 |
| H-05 | auth-proxy.js 安全缺陷 | **待处理** | 认证层可被绕过(已从 _archive 补回主目录) |
### 计划处理 (P2 — 一个月内)
| 编号 | 问题 | 原因 |
|------|------|------|
| H-04 | 共享网络隔离 | 横向移动风险 |
| H-06 | /root 目录挂载 | 敏感文件外露 |
| M-01 ~ M-06 | 各类中危问题 | 安全加固措施 |
| L-01 ~ L-05 | 低危问题 | 代码质量和 hygiene |
### 总体评价
WorkPod Alpine 是一个**开发环境工具**,其设计目标(便捷性、开箱即用)与安全性之间存在固有张力。当前配置明显偏向便利性:
**优势:**
- 采用多阶段构建减小镜像攻击面
- 使用 Alpine 基础镜像减少包数量
- 有信号处理和清理机制
- tmux session 管理支持多用户协作
**核心风险:**
- **特权模式 + root 运行 + 全权限 AI + 弱密码**的组合构成了"完美风暴"——每个单独看都是常见做法,但叠加在一起使整体安全基线极低
- 作为开发环境可以接受一定风险,但如果此容器部署在任何可从互联网访问的环境中,应当视为**已被攻陷**
**最低可行安全改进(MVP):**
1. 移除 `privileged: true`(除非有明确需求并提供理由)
2. 密码改为环境变量注入 + 强密码
3. 移除 `allow: ["*"]``--dangerously-skip-permissions`
4. SSH 禁用密码登录或使用强密码
5. 添加 `.gitignore` 防止敏感文件提交
+316
View File
@@ -0,0 +1,316 @@
# 架构设计审核报告 -- WorkPod
> 审核日期:2026-04-07
> 审核范围:`E:/wk-lab/workpod`
> 镜像大小:~436MBalpine:3.23 基础,多阶段构建)
---
## 架构概览
```
+-----------------------------+ +------------------------+
| Docker Host (测试服) | | Container |
| | | alpine:3.23 |
| workpod-alpine (2222/7681) |------>| +--------------------+|
| docker-compose.yml | | | /usr/sbin/sshd ||
| 基础实例 | | | ttyd (-W 无认证) ||
| workpod-network | | | Node.js (musl) ||
| | | | Claude Code @latest||
| | | | coding-helper ||
| ws-flux-dev (2201/7701) |------>| | tmux ||
| docker-compose.flux.yml | | +--------------------+|
| Flux 项目实例 | | |
| Dockerfile.flux 扩展 | | /opt/ttyd-session.sh |
| | | (tmux session 选择) |
| | | entrypoint.sh |
| | | (开发工具自动检测) |
| | +------------------------+
| _archive/auth-proxy.js |
| (备用认证代理) |
| nginx 反向代理 (wk.1216.top)|
+-----------------------------+
**核心组件关系**
- **Dockerfile**:多阶段构建(builder 安装 Node.js/npm 包 -> runtime 仅复制产物到 alpine:3.23),entrypoint.sh 和 ttyd-session.sh 通过 `COPY --chmod=755` 内置到镜像
- **entrypoint.sh**:统一入口(已内置镜像),直接 `/usr/sbin/sshd` 启动 SSH,自动检测 /root 下开发工具设置 PATHttyd 使用 ttyd-session.sh 做 tmux session 管理
- **Dockerfile.flux**Flux 项目实例扩展构建,基于基础镜像追加项目特定配置
- **ttyd-session.sh**:独立的会话管理脚本(已通过 `COPY --chmod=755` 内置到镜像),支持 URL 参数 `?session=name` 或交互式菜单选择/创建 tmux session
- **docker-compose.yml**:基础实例编排(context: .),workpod-network 网络,2222/7681 端口
- **docker-compose.flux.yml**Flux 项目实例编排,2201/7701 端口,挂载 Flux 工作空间
- **docker-compose-alpine.yml**Alpine 变体备用编排文件,多端口映射(7683/7684),挂载 /root
- **_archive/auth-proxy.js**Node.js 认证代理(已归档),提供登录页 + token 认证 + 多工作区路由 + WebSocket 代理,作为可选组件保留
- **wk.1216.conf**Nginx 配置,SSL 终止 + 反向代理到 ttyd
**关键特征**
- 轻量级镜像(~436MB vs Ubuntu 版 ~2.6GB
- 单一 entrypoint + Dockerfile 扩展模式:基础实例与项目实例共享核心逻辑,通过 Dockerfile.flux 按需扩展
- 开发工具"外挂"模式:安装到 /root 挂载卷,entrypoint 自动检测
- 认证代理方案(auth-proxy.js)已归档至 _archive/,作为可选组件按需启用
---
## 优势分析
### 1. 多阶段构建 -- 镜像体积控制优秀
builder 阶段完成所有重量级操作(npm install、编译依赖),runtime 阶段仅复制 `/opt/node` 到 `/usr/local`。最终镜像 ~436MB,是 Ubuntu 版的 1/6。这是本项目最突出的架构优势。
### 2. 开发工具"外挂"设计 -- 灵活性高
通过 entrypoint.sh 自动检测 `/root` 下的 `.cargo`、`.pyenv`、`go`、`.local/bin` 等目录并动态设置 PATH,实现了:
- 工具安装与镜像构建解耦
- 不同实例可安装不同工具集
- 容器重建不丢失手动安装的工具(只要 /root 已挂载)
- 新增工具类型只需在 entrypoint 添加一行检测逻辑
这一设计在 ISSUES.md 中有清晰记录,体现了"踩坑 -> 抽象 -> 文档化"的良好工程实践。
### 3. ttyd-session.sh -- 会话管理解耦
将 ttyd 的启动命令从 entrypoint 中抽离为独立脚本 `ttyd-session.sh`
- 支持 URL 参数快速进入指定 session`?session=xxx`
- 交互式菜单列出已有 session 并支持新建
- session 名安全过滤(仅允许字母数字下划线连字符)
- 共享 attach 模式(不加 `-d`,多人可同时查看同一 session)
职责划分清晰:entrypoint 负责服务启停,ttyd-session 负责终端会话逻辑。
### 4. 问题记录完整 (ISSUES.md)
ISSUES.md 记录了 11 个已解决的问题及解决方案,涵盖:
- Alpine 特有命令差异(ss vs netstat
- BuildKit 兼容性(--chmod、缓存行为)
- 运行时问题(中文输入、断线恢复、数据持久化)
- 架构决策记录(/root 安装原则)
这是非常有价值的知识资产,降低了团队其他成员的踩坑成本。
### 5. auth-proxy.js -- 完整的认证层
虽然未默认集成到容器中,但 auth-proxy.js 提供了:
- Basic Auth 登录 + Token 机制
- 内存 Token 存储(1小时过期)
- 多工作区路由(默认 + hszd)
- HTTP 和 WebSocket 双协议代理
- 自定义登录页面
这为生产化部署提供了现成的安全方案。
---
## 问题与建议
### [严重] 多实例 entrypoint 膨胀与维护噩梦 -- **[已解决]**
- **位置:** 原为 `entrypoint.sh` vs `entrypoint-test.sh`
- **现状:**
- `entrypoint-test.sh` 已归档至 `_archive/`
- 当前仅保留单一 `entrypoint.sh` 作为统一入口
- Flux 项目实例通过 `Dockerfile.flux` 扩展构建,不再需要独立 entrypoint
- **原问题描述:**
1. **代码大量重复**:两个文件约 70% 代码完全相同(信号处理、PATH 检测、sshd 启动、健康检查、启动信息打印)。修改公共逻辑需要同步两处
2. **yxl 实例的 entrypoint 未纳入仓库**:该实例应该还有第三个 entrypoint 变体,但仓库中只有两个
3. **每新增一个用户就要复制一份 entrypoint**:当前 2 个文件,如果有 10 个用户就是 10 个几乎相同的脚本
4. **配置散落在脚本内部**:ttyd 凭据、端口号、用户名等硬编码在不同 entrypoint 中
- **解决方案:** 归档 entrypoint-test.sh,统一使用单一 entrypoint.sh + Dockerfile 扩展模式。项目特定配置通过 Dockerfile.flux 注入,无需维护多份 entrypoint。
- **遗留建议(供参考):** 如未来需支持更多定制化实例,可考虑将差异项外部化为环境变量:
```bash
# entrypoint.sh 统一入口
TTYD_CREDENTIALS="${TTYD_CREDENTIALS:-ada:123}"
TTYD_PORT="${TTYD_PORT:-7681}"
SSH_PORT="${SSH_PORT:-22}"
CREATE_DEVELOPER_USER="${CREATE_DEVELOPER_USER:-false}"
DEVELOPER_NAME="${DEVELOPER_NAME:-developer}"
[ "$CREATE_DEVELOPER_USER" = "true" ] && setup_developer_user
```
### [高] 安全凭据硬编码且各实例不一致 -- **[已修复]**
- **位置:** 原为 `entrypoint.sh:45`, `entrypoint-test.sh:93`, `Dockerfile:34`
- **现状:** 所有凭据已统一通过环境变量注入,不再在各文件中硬编码
- **原问题描述:**
| 凭据类型 | 原始状态 | 当前状态 |
|----------|---------|---------|
| root 密码 | 多处硬编码 workpod123 | 环境变量注入 |
| ttyd 凭据 | 各实例不一致(ada:123 / jc:1234567 | 环境变量统一注入 |
| auth-proxy 密码 | JS 源码明文 | 已随 auth-proxy 归档,按需启用时从配置读取 |
- **原问题:**
1. **密码明文写在 4 个不同文件中**,修改密码需要改多处
2. **ttyd 凭据各实例不同**ada:123 vs jc:1234567),但没有统一的管理方式
3. **auth-proxy.js 的密码是 JS 源码中的明文**,如果该文件被包含在镜像或 Web 目录中则直接暴露
4. **root 密码与 Dockerfile 中 chpasswd 一致**,但如果通过环境变量覆盖 entrypoint 的密码,Dockerfile 层的密码仍然存在
- **解决方案:** 统一通过 docker-compose 环境变量或 `.env` 文件注入所有凭据,清除源码中的硬编码值。
### [中] auth-proxy.js 与容器架构脱节
- **位置:** `_archive/auth-proxy.js`, `static/login.html`, `wk.1216.conf`
- **现状:** auth-proxy.js 已归档至 `_archive/` 目录,作为可选组件保留在主目录中。不再作为核心组件强制集成。
- **问题:**
1. **部署方式需明确**:如需启用 auth-proxy,应补充启动说明(手动 / systemd / compose sidecar
2. **生命周期无保障**:若独立进程运行,容器重启后 auth-proxy 不会自动恢复
3. **token 存储在内存中**auth-proxy 重启后所有用户需重新登录(可接受,但应文档化)
4. **工作区配置硬编码**`WORKSPACES` 数组写死记录,新增工作区需改代码重启
5. **与 ttyd -W 标志冲突**:容器内 ttyd 启动时用了 `-W`(禁用重连提示,非认证),如果 auth-proxy 作为唯一入口则需确保 ttyd 端口不对外暴露
- **建议:**
- 如需正式使用:将 auth-proxy 集成到 docker-compose 中作为 sidecar 容器或独立 service
- 或将认证能力内置到容器内(如 ttyd 加 `-c` 参数 + Nginx basic_auth
- 工作区配置改为从环境变量或 JSON 文件读取
- 在 README 中说明 auth-proxy 的启用方式和适用场景
### [中] 编排文件职责需明确
- **位置:** `docker-compose.yml`, `docker-compose.flux.yml`, `docker-compose-alpine.yml`
- **现状:** 当前有 3 个编排文件:
- **`docker-compose.yml`**:基础实例,2222/7681 端口,挂载 workspace4G 内存限制,workpod-network 网络。适用于通用开发环境。
- **`docker-compose.flux.yml`**Flux 项目专用实例,2201/7701 端口,挂载 Flux 工作空间目录,使用 Dockerfile.flux 扩展构建。适用于 Flux 项目开发。
- **`docker-compose-alpine.yml`**Alpine 变体备用编排,2222/7683+7684 端口,挂载 workspace + /root,无资源限制。适用于需要完整 /root 挂载的调试场景。
- **问题:**
1. 三个文件用途已有区分但未在文档中集中说明
2. `docker-compose-alpine.yml` 直接引用镜像而非构建,与另外两个文件的构建模式不一致
3. 缺少 `.env` 文件或参数化配置来统一管理端口/资源等差异
- **建议:**
- 在 README 中补充各编排文件的用途说明和使用场景
- 考虑合并为单一 compose 文件 + profiles,或保持多文件但统一 `.env` 参数化
### [中] Claude Code 版本在 download-packages.sh 中滞后 -- **[已解决]**
- **位置:** 原为 `download-packages.sh:33` vs `Dockerfile:18`
- **现状:** 已统一使用 `@latest` 标签,不再锁定具体版本号
- **原问题描述:**
- download-packages.sh`claude-code@2.1.87`
- Dockerfile`@anthropic-ai/claude-code@2.1.89`
- **原问题:** 下载脚本中的版本落后于 Dockerfile 实际使用的版本。如果有人先执行下载脚本再构建,会得到错误的包。
- **解决方案:** 改用 `@latest` 标签,消除版本不一致问题。每次构建自动拉取最新版。
### [中] dev-tools PATH 检测逻辑重复
- **位置:** `entrypoint.sh:19-29`(运行时 export vs `entrypoint.sh:32-40`profile.d 写入)
- **现状:** 开发工具目录检测逻辑出现 2 次(单一 entrypoint.sh 内:一次 export 一次写入 profile.d)。原为 4 处(两个 entrypoint 各两次),归档 entrypoint-test.sh 后已减少至 2 处。
- **问题:** 违反 DRY 原则。新增一种工具类型需要修改 2 处。
- **建议:** 抽取为函数或独立脚本:
```bash
# /opt/lib/dev-tools.sh
setup_dev_paths() {
local profile=""
[ -d /root/.cargo/bin ] && { export PATH="/root/.cargo/bin:$PATH"; profile+="..."; }
# ...
[ -n "$profile" ] && echo "$profile" > /etc/profile.d/dev-tools.sh
}
```
### [低] packages/ 目录包含未在 Dockerfile 中使用的包
- **位置:** `packages/go1.26.1.linux-amd64.tar.gz`, `packages/rust-1.94.1-x86_64-unknown-linux-musl.tar.xz`
- **现状:** packages/ 目录中有 Go 和 Rust 的 musl 构建包,但 Dockerfile 仅安装了 Node.js + npm 包
- **问题:**
1. 占用 ~247MB 磁盘空间
2. download-packages.sh 也没有下载这两个包的步骤(说明可能是手动放入的)
3. PACKAGES.md 不存在(Alpine 版缺少此文档),无法确认这些包的用途
- **建议:**
- 如果是为"开发工具外挂"模式准备的:在文档中明确说明
- 如果不再需要:清理掉以减小仓库体积
- 补充 PACKAGES.md 说明软件包策略
### [低] entrypoint-test.sh 中的 Claude Code 别名策略不一致
- **位置:** `entrypoint-test.sh:65` vs `entrypoint-test.sh:76`
- **现状:**
- developer 用户:`alias claude='claude --dangerously-skip-permissions'`
- root 用户:`alias claude='claude --allow-dangerously-skip-permissions'`
- **问题:**
1. root 用 `--allow-dangerously-skip-permissions`developer 用 `--dangerously-skip-permissions`,参数不同但效果类似,容易混淆
2. 注释说"root 禁止 --dangerously-skip-permissions",但这应该是 Claude Code 对 root 用户的限制而非 CLI 参数限制
3. 这个别名仅在 bashrc 中,非交互式 shell(如 `docker exec` 执行命令)不会加载
- **建议:** 统一别名策略或在注释中更清晰地解释原因差异。考虑用 wrapper 脚本替代 alias 以覆盖所有调用场景。
### [低] ttyd-session.sh 的 CHOICE 输入无超时
- **位置:** `ttyd-session.sh:37`
- **现状:** `read -p "session: " CHOICE` 无超时设置
- **问题:** 如果用户打开 Web 终端但不输入选择,read 会一直阻塞。在自动化场景或意外断开的情况下可能导致僵尸进程。
- **建议:** 添加 `-t 30` 超时(30秒),超时后自动选择 default session。
---
## 架构改进路线图
### P0 -- 紧急(影响安全与可维护性)
| 序号 | 改进项 | 工作量 | 说明 |
|------|--------|--------|------|
| 1 | entrypoint 配置驱动重构 | 3h → **降低优先级** | 单 entrypoint 场景下紧迫性下降。当前 entrypoint.sh + Dockerfile.flux 扩展模式已满足需求,配置驱动重构可作为 P2 优化项储备 |
| 2 | 凭据外部化管理 | **[已完成]** | 所有密码/密钥已通过环境变量注入,清除源码中的硬编码 |
| 3 | auth-proxy 集成到编排体系 | 2h | auth-proxy 已归档为可选组件。如需正式启用,编写 docker-compose 或 systemd unit 确保生命周期受控 |
### P1 -- 重要(提升工程质量)
| 序号 | 改进项 | 工作量 | 说明 |
|------|--------|--------|------|
| 4 | dev-tools 检测逻辑去重 | 1h | 抽取为共享函数/脚本,消除 2 处重复(原 4 处) |
| 5 | 版本号单一数据源 | **[已完成]** | 已统一使用 @latest 标签 |
| 6 | 编排文件职责文档化 | 1h | 明确 3 个 yml 文件(yml / .flux.yml / -alpine.yml)的用途和使用场景,补充 README 说明 |
### P2 -- 优化(改善体验)
| 序号 | 改进项 | 工作量 | 说明 |
|------|--------|--------|------|
| 7 | 补充 PACKAGES.md | 0.5h | 说明 Alpine 版的包策略(含 Go/Rust 预留包) |
| 8 | 清理冗余包或文档化 | 0.25h | 决定 packages/ 中 Go/Rust 包的去留 |
| 9 | ttyd-session 超时保护 | 0.15h | read 添加超时 |
| 10 | Claude Code alias 统一 | 0.25h | 明确策略或改为 wrapper 脚本 |
---
## 两版本协同改进建议
由于 WorkPod Ubuntu 版依赖 Alpine 版的 entrypoint 脚本,两个版本的改进应协调进行:
### 共享资产清单
| 资产 | 当前归属 | 建议归属 |
|------|---------|---------|
| entrypoint.sh (核心逻辑) | Alpine 版(Ubuntu 版引用) | 提取为共享模块 |
| ttyd-session.sh | Alpine 版(Ubuntu 版引用) | 提取为共享模块 |
| PACKAGES.md | 仅 Ubuntu 版有 | 两版本都应有 |
| auth-proxy.js | Alpine 版(独立运行) | 可供两版本共用 |
| ISSUES.md | 仅 Alpine 版有 | Ubuntu 版也应建立 |
### 推荐的共享方案
```
workpod-common/ # 新建共享仓库或目录
├── entrypoint-core.sh # 公共逻辑(信号处理、sshd、健康检查)
├── ttyd-session.sh # 会话管理(已有,可直接复用)
├── lib-dev-tools.sh # 开发工具检测函数
└── versions.env # 版本号定义(NODE_VERSION=, CLAUDE_CODE_VERSION= ...
workpod/ # Ubuntu 版
├── entrypoint.sh # source core + Ubuntu 特定逻辑
└── Dockerfile # ubuntu 构建
workpod-alpine/ # Alpine 版
├── entrypoint.sh # source core + Alpine 特定逻辑
├── entrypoint-test.sh # source core + test 实例特定逻辑
└── Dockerfile # alpine 多阶段构建
```
---
## 总结评价
WorkPod 的架构质量持续改善。多阶段构建、开发工具外挂设计、ttyd-session 解耦、完整的问题记录都是亮点。~436MB 的镜像是务实的选择 -- 在功能完备性和体积之间取得了良好平衡。
本次审核中,多项历史问题已得到解决或修复:
- **entrypoint 膨胀问题 [已解决]**:归档 entrypoint-test.sh,统一单一 entrypoint.sh + Dockerfile.flux 扩展模式
- **安全凭据硬编码 [已修复]**:统一通过环境变量注入
- **Claude Code 版本滞后 [已解决]**:改用 @latest 标签
当前架构已从"多实例复制 entrypoint"模式演进为"单入口 + 构建扩展"模式,可维护性显著提升。P0-1(entrypoint 配置驱动重构)的紧迫性随之降低,可作为 P2 优化项储备。
安全层面,auth-proxy.js 已归档至 `_archive/` 作为可选组件保留。如需正式启用认证层,建议按 P0-3 方案集成到编排体系中。
整体而言,WorkPod 已整合为主目录项目,Alpine 作为基础镜像方案继续演进,Flux 等项目实例通过 Dockerfile.flux 按需扩展。
@@ -0,0 +1,290 @@
# Docker 最佳实践审核报告
> 审核对象:WorkPod Alpine 版
> 审核日期:2026-04-07
> 审核范围:Dockerfile、docker-compose.yml、entrypoint.sh、ttyd-session.sh、.gitignore、Dockerfile.flux、docker-compose.flux.yml
---
## 镜像分析
| 指标 | Alpine 版 | Ubuntu 版(对照) | 建议 |
|------|----------|-------------------|------|
| 大小 | ~436MB | ~2.6GB | Alpine 版体积控制优秀 |
| 阶段数 | 2(多阶段) | 1(单阶段) | 多阶段构建设计合理 |
| 基础镜像 | alpine:3.23 (3.4MB) | ubuntu:22.04 (77MB) | Alpine 最小化基础镜像选择正确 |
| 最终层数 | ~8 层 | ~7 层 | 层数合理 |
| 包管理器 | apk (--no-cache) | apt (--no-install-recommends) | 两者都做了优化,apk 更彻底 |
---
## 问题清单
### [严重] S1 - 密码硬编码在 Dockerfile 中 **[已修复]**
- **位置:** `Dockerfile:34`
- **最佳实践:** 密码绝不应写入镜像层,应通过环境变量、Docker secrets 或运行时注入
- **现状:**
```dockerfile
echo "root:${ROOT_PASSWORD:-workpod}" | chpasswd
```
ROOT_PASSWORD 已通过环境变量 `${ROOT_PASSWORD}` 注入,不再硬编码在镜像层中。entrypoint.sh 已移除密码明文输出。ttyd 认证也已改用 `${TTYD_CREDENTIALS}` 环境变量(见 M4)。
- **建议修复:** 已完成。ROOT_PASSWORD 和 TTYD_CREDENTIALS 均通过 docker-compose 环境变量注入。
### [严重] S2 - 以 root 用户运行所有服务
- **位置:** `Dockerfile` 全文、`entrypoint.sh`
- **最佳实践:** 容器内应以非特权用户运行应用进程
- **现状:** sshd、ttyd、tmux、Node.js 全部以 root 运行,PermitRootLogin yes
- **说明:** 当前为开发环境的故意设计。WorkPod 作为全栈开发环境容器,需要 root 权限来安装工具链、管理包、配置系统服务等。生产环境部署时应考虑降权。
- **建议修复:**
- 创建 `developer` 用户运行 ttyd 和开发工具
- sshd 可保留 root 但应禁用密码登录,仅允许密钥认证
- 添加 `USER` 指令
### [严重] S3 - Compose healthcheck 命令语法错误(管道无法在 CMD 数组中工作) **[已修复]**
- **位置:** `docker-compose.yml:48`
- **最佳实践:** Compose healthcheck 的 CMD 数组形式中,`|`(管道)不会被 shell 解释
- **原问题:**
```yaml
test: ["CMD", "ss", "-tlnp", "|", "grep", "-qE", ":(22|7681)\\b"]
```
这会执行 `ss -tlnp | grep -qE ...` 吗?**不会**。CMD 数组形式是 exec 执行,`|` 被当作 ss 的参数而非管道符。实际效果等同于:
```
ss -tlnp "|" grep -qE ":(22|7681)\b"
```
这会导致健康检查**始终失败或行为异常**。
- **对比:** 镜像内 HEALTHCHECK`Dockerfile:48-49`)使用的是 shell 形式 `CMD netstat ...`,可以正常解析管道。
- **额外问题:** Alpine 默认不安装 `ss` 命令(属于 iproute2 包,未在 apk add 列表中),即使语法修复也会因命令不存在而失败。
- **修复状态:** 已改为 CMD-SHELL 形式并使用 netstat
```yaml
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
```
### [高] H1 - privileged: true 过于宽泛 **[部分修复]**
- **位置:** `docker-compose.yml:11`、`docker-compose.flux.yml`
- **最佳实践:** privileged 应作为最后手段
- **现状:**
- `docker-compose.flux.yml`:已移除 `privileged: true`,改用细粒度 capabilities
- `docker-compose.yml`(基础版):仍保留 `privileged: true`
- **建议修复:** 基础版 docker-compose.yml 也应同步移除 privileged,统一使用 capabilities 方案。
### [高] H2 - npm 包版本不固定 (@latest)
- **位置:** `Dockerfile:19`
- **最佳实践:** 所有依赖应固定版本号
- **现状:**
```dockerfile
npm install -g "@z_ai/coding-helper@latest"
```
与 Ubuntu 版相同问题。
- **说明:** 用户决定保持 `@latest`,以便每次构建时自动获取 coding-helper 最新版本。开发环境可接受此策略。
- **建议修复:** 如需稳定构建可固定版本号(如 `"@z_ai/coding-helper@0.0.7"`),当前保持 @latest 为有意设计。
### [高] H3 - 缺少 .dockerignore 文件 **[已修复]**
- **位置:** 项目根目录
- **最佳实践:** 排除无关文件以减小构建上下文
- **原问题:** 没有 `.dockerignore`。以下文件会被发送到 Docker daemon
- `workpod-alpine-latest.tar.gz`~100MB 导出镜像)
- `static/` 目录(前端静态文件)
- `auth-proxy.js`, `nginx-map-patch.sh`, `wk.1216.conf`(部署辅助文件)
- `.claude/` 配置目录
- `ISSUES.md`, `docker-compose-alpine.yml`(文档和备用 compose
- `packages/go1.26.1.linux-amd64.tar.gz`, `packages/rust-*.tar.xz`(未在 Dockerfile 中使用的包)
- **关键发现:** packages 目录中有 Go 和 Rust 的 tarball,但 Dockerfile 只使用了 node 包。这些大文件(Go ~150MB, Rust ~100MB)每次构建都会被发送到 Docker context。
- **修复状态:** 已创建 `.dockerignore`,构建上下文缩小至约 275 bytes,排除了所有无关文件和大型包。
### [高] H4 - builder 阶段未清理 npm 缓存和临时文件
- **位置:** `Dockerfile:12-20`
- **最佳实践:** 多阶段构建的 builder 阶段虽不影响最终镜像大小,但影响构建缓存和构建速度
- **现状:**
```dockerfile
RUN mkdir -p /opt/node \
&& cd /tmp/packages \
&& tar -xzf node-v24.14.1-linux-x64-musl.tar.gz -C /opt/node --strip-components=1 \
&& export PATH="/opt/node/bin:$PATH" \
&& npm install -g npm@10 \
&& npm config set registry https://registry.npmmirror.com \
&& npm install -g "@anthropic-ai/claude-code@2.1.89" \
&& npm install -g "@z_ai/coding-helper@latest" \
&& npm cache clean --force
```
有 `npm cache clean --force`,但 `/tmp/packages` 目录未被删除(虽然不影响最终镜像,因为 COPY --from=builder 只复制 /opt/node)。
- **建议修复:** 当前做法可接受。如追求极致可添加 `&& rm -rf /tmp/packages /root/.npm`。
### [中] M1 - COPY 后单独 RUN chmod 而非使用 --chmod **[已修复]**
- **位置:** `Dockerfile:42-44`
- **最佳实践:** 利用 BuildKit 的 `COPY --chmod` 减少层数
- **原问题:**
```dockerfile
COPY entrypoint.sh /entrypoint.sh
COPY ttyd-session.sh /opt/ttyd-session.sh
RUN chmod 755 /entrypoint.sh /opt/ttyd-session.sh
```
单独一个 RUN chmod 创建了一个额外的镜像层(约几十字节),且与 Ubuntu 版的做法不一致(Ubuntu 版用了 `--chmod=755`)。
- **修复状态:** 已统一为 `COPY --chmod=755`,移除了多余的 `RUN chmod` 层。
### [中] M2 - 多个 docker-compose 文件缺乏明确分工说明
- **位置:** `docker-compose.yml` / `docker-compose.flux.yml` / `docker-compose-alpine.yml`
- **最佳实践:** 项目应有唯一的 compose 文件或明确的文件用途区分
- **现状:** 当前共有 3 个 compose 文件:
- `docker-compose.yml`:基础版,带完整配置(healthcheck、资源限制、日志轮转、privileged: true
- `docker-compose.flux.yml`Flux 版,移除 privileged、使用 capabilities、更精细的配置
- `docker-compose-alpine.yml`:精简版(无 healthcheck、无资源限制、无日志配置、不同端口映射)
- **风险:** 开发者可能混淆该用哪个文件;alpine 备用文件缺配置。
- **建议修复:**
- 在 README 中明确说明各文件用途
- 或合并为一个主文件 + 环境覆盖文件
### [中] M3 - openrc 初始化方式不够健壮
- **位置:** `Dockerfile:32`
- **最佳实践:** Alpine 容器中应谨慎使用 openrc
- **现状:**
```dockerfile
&& mkdir -p /run/openrc && touch /run/openrc/softlevel
```
这是让 openrc 命令可用的标准做法,但 entrypoint.sh 中并未使用 `service ssh start`(而是直接调用 `/usr/sbin/sshd`),所以 openrc 的初始化可能是多余的。
- **建议修复:**
- 如果不用 openrc 管理 sshd:移除 `openrc` 依赖和 softlevel 初始化,改用 `apk add openssh-server --no-cache`(不带 openrc
- 如果将来要用 openrc:保留当前做法并在注释中说明意图
### [中] M4 - ttyd 认证凭证硬编码 **[已修复]**
- **位置:** `entrypoint.sh:45`
- **最佳实践:** 认证信息不应硬编码
- **原问题:**
```bash
ttyd -W -c ada:123 -t fontSize=16 ...
```
用户名 `ada` 密码 `123` 写死在脚本中。虽然比 Ubuntu 版的无认证好,但仍然是弱凭证+硬编码。
- **修复状态:** 已改用 `${TTYD_CREDENTIALS}` 环境变量注入,entrypoint 中动态拼接 `-c` 参数。默认值保留为兼容性兜底。
### [中] M5 - 资源限制可能偏低 **[已修复]**
- **位置:** `docker-compose.yml:42-44`
- **最佳实践:** 资源限制应根据实际负载设置
- **原问题:**
```yaml
limits:
memory: 4G
reservations:
memory: 512M
```
Alpine 版虽然基础镜像小,但如果用户安装 Rust/Go/Python 工具链并编译大型项目,512M 保留内存可能不足导致 OOM Kill。
- **修复状态:** reservation 已从 512M 提升到 1G,更适合 Claude Code 等高内存占用工具的运行。
### [低] L1 - 缺少 LABEL 元数据 **[已修复]**
- **位置:** `Dockerfile`
- **最佳实践:** 镜像应包含维护者、版本等标签
- **原问题:** 无任何 LABEL 指令。
- **修复状态:** 已添加 maintainer + Open Containers 标准标签(org.opencontainers.image.*)。
### [低] L2 - 基础镜像未指定 digest
- **位置:** `Dockerfile:4`, `Dockerfile:23`
- **最佳实践:** 生产级镜像应 pin 到具体 digest
- **现状:** 两处 `FROM alpine:3.23` 均未锁定 digest。
- **建议修复:** 开发环境可接受,CI/CD 环境建议锁定。
### [低] L3 - HEALTHCHECK 使用 netstat 而非 ss
- **位置:** `Dockerfile:48-49`
- **最佳实践:** 优先使用更现代的工具
- **现状:**
```dockerfile
CMD netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\b' || exit 1
```
netstat 已被标记为 deprecatedAlpine 的 busybox 提供 netstat 但功能有限。`netstat -p`(显示 PID/程序名)在 busybox 版本中可能不支持。
- **验证:** Alpine 的 busybox netstat 不支持 `-p` 参数,因此 `netstat -tlnp` 中的 `-p` 会被忽略,但仍能检查端口是否存在。
- **建议修复:** 当前可用但不够精确。如需进程级检查,需安装 `iproute2-ss`(提供 ss)或 `net-tools`(提供完整 netstat)。当前做法作为端口可达性检查足够。
### [低] L4 - entrypoint.sh 中 PATH 注入写到 /etc/profile.d/
- **位置:** `entrypoint.sh:32-40`
- **最佳实践:** 容器内动态修改系统配置目录应注意幂等性
- **现状:** 每次启动都会覆写 `/etc/profile.d/dev-tools.sh`,如果文件内容需要更新则没问题,但写入操作本身在容器中是不必要的(因为每次启动都是全新状态,除非挂载了持久化 root 目录)。
- **建议修复:** 当前后果无害。如果 data/home/root 被挂载为持久化,这个设计是有意义的(让 SSH 登录也能获得正确的 PATH)。保持现状即可。
### [低] L5 - docker-compose-alpine.yml 缺少关键配置
- **位置:** `docker-compose-alpine.yml`
- **最佳实践:** 所有 compose 文件应包含最低限度的运维配置
- **现状:** 该文件缺少:
- healthcheck(服务不可观测)
- logging 配置(日志无限增长风险)
- resource limits(无资源隔离)
- restart 策略(有 `restart: unless-stopped`,此项 OK
- **建议修复:** 标注此文件为"快速启动/测试专用",或补齐缺失配置。
---
## 与 Ubuntu 版的关键差异分析
| 对比维度 | Ubuntu 版 | Alpine 版 | 评价 |
|---------|----------|----------|------|
| 构建策略 | 单阶段 | 多阶段 | Alpine 更优 |
| 镜像体积 | ~2.6GB | ~436MB | Alpine 优势明显(6倍差距) |
| ttyd 认证 | 无(`-W` | 有(`-c ada:123`) | Alpine 更安全(尽管凭证弱) |
| 健康检查命令 | `ss -tlnp`(镜像)/ `netstat`compose | `netstat`(镜像)/ `ss`compose**语法错误** | Ubuntu compose 覆盖合理;Alpine compose 有 bug |
| COPY --chmod | 使用 | **已使用**(统一为 `COPY --chmod=755` | 两者一致 |
| 密码输出 | 明文打印 | 明文打印 | 两者都有问题 |
| 开发工具支持 | Node.js only | Node.js + Rust + Go + Python 自动检测 | Alpine 功能更丰富 |
| extra compose 文件 | 无 | 有(docker-compose-alpine.yml | 增加维护复杂度 |
---
## 优化建议汇总(按收益排序)
| 排名 | 建议 | 严重度 | 状态 | 实施难度 |
|------|------|--------|------|---------|
| 1 | **修复 Compose healthcheck 语法错误**CMD -> CMD-SHELLss -> netstat | 严重 | **[已修复]** | 低(改一行) |
| 2 | 创建 `.dockerignore`(排除 Go/Rust 包 ~250MB + 其他无用文件) | 高 | **[已修复]** | 低(5分钟) |
| 3 | 密码外部化(SSH + ttyd 双重硬编码) | 严重 | **[已修复]** | 中(改 compose + entrypoint |
| 4 | 固定 coding-helper 版本 @latest -> 具体版本 | 高 | 保持 @latest(有意设计) | 低(改一行) |
| 5 | COPY 统一使用 --chmod(消除多余 RUN 层) | 中 | **[已修复]** | 低(改几行) |
| 6 | 明确多个 compose 文件的定位(当前 3 个:yml / .flux.yml / -alpine.yml | 中 | 待改进 | 低(加注释或重构) |
| 7 | 移除不必要的 openrc 依赖(减小攻击面) | 中 | 待处理 | 低(删几行) |
| 8 | 提升 memory reservation 到 1G+ | 中 | **[已修复]** | 低(改数字) |
| 9 | 添加 LABEL 元数据 | 低 | **[已修复]** | 低(加几行) |
| 10 | privileged 降级为 capabilitiesflux 版已完成,基础版待同步) | 高 | **[部分修复]** | 高(需测试兼容性) |
---
## 总体评价
| 维度 | 评分 | 说明 |
|------|------|------|
| 镜像优化 | A- | 多阶段构建、体积控制优秀、apk --no-cache、.dockerignore 已就位 |
| 编写规范 | A- | COPY --chmod 统一、LABEL 完整、结构清晰 |
| Compose 配置 | B | healthcheck 已修复、3 个 compose 文件需明确分工、flux 版已移除 privileged |
| 安全性 | C | 密码已外部化、ttyd 凭证环境变量化、root 运行为开发有意设计、privileged 部分修复 |
| 运维友好度 | A- | 信号处理完善、开发工具自动检测、启动信息详细、labels 完整、reservation 提升至 1G |
**综合评级:B+**
Alpine 版在镜像构建方面表现优秀(多阶段、体积小、apk 高效、.dockerignore)。上一轮审核中的关键功能性 bug(healthcheck 语法错误)和严重安全问题(密码硬编码)均已修复。安全性从 D+ 提升到 C,主要得益于密码/凭证外部化和 healthcheck 修复。剩余待改进项:基础版 docker-compose.yml 的 privileged 移除、多 compose 文件策略统一、openrc 依赖清理。
### 最高优先级修复项
**已完成:**
- ~~`docker-compose.yml:48`~~ — healthcheck 命令已改为 CMD-SHELL + netstat **[已修复]**
- ~~密码和 ttyd 凭证~~ — 已通过环境变量注入 **[已修复]**
- ~~`.dockerignore`~~ — 已创建,构建上下文 ~275 bytes **[已修复]**
- ~~COPY --chmod~~ — 已统一使用 **[已修复]**
- ~~LABEL 元数据~~ — 已添加 maintainer + OC labels **[已修复]**
- ~~memory reservation~~ — 已提升至 1G **[已修复]**
- ~~privileged 降级~~ — flux 版已完成,基础版待同步 **[部分修复]**
**待处理:**
- 基础版 `docker-compose.yml` 移除 `privileged: true`,同步 flux 版的 capabilities 方案
- 明确 3 个 compose 文件的分工定位(README 或文件内注释)
- openrc 依赖评估与清理(如不使用 service 命令可移除)
- `@z_ai/coding-helper@latest` 版本固定(当前保持 @latest 为有意设计)
@@ -0,0 +1,204 @@
# Shell 脚本质量审核报告
> 审核日期:2026-04-07
> 审核范围:WorkPod Alpine(含 Test 变体)全部 Shell 脚本
> 审核人:Shell 脚本专家
---
## 脚本清单
| 脚本 | 行数 | 用途 | 复杂度 |
|------|------|------|--------|
| entrypoint.sh | 81 | Alpine 版容器入口,启动 SSH + ttyd + 开发工具检测 | 中 |
| entrypoint-test.sh | 129 | Test 容器入口,额外创建 developer 用户 + Claude 全权限配置 | 中高 | 已归档(_archive/),非当前活跃 |
| ttyd-session.sh | 63 | tmux 会话管理,URL 参数解析 + 交互式菜单 | 中 |
| **合计** | **273** | | |
---
## 逐脚本审核
### entrypoint.shAlpine 版)
**文件路径:** `E:/wk-lab/workpod/entrypoint.sh`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **SSHD_PID 未赋值(与 Ubuntu 版相同的问题)** | 高 | `cleanup()``kill "$SSHD_PID"` 因 PID 始终为 0 而跳过。`/usr/sbin/sshd` 在第42行前台启动(无 `&`),实际上 sshd 作为守护进程运行后返回,但未捕获其 PID。应改为 `/usr/sbin/sshd & SSHD_PID=$!` 或在 cleanup 中用 `pkill sshd`。 |
| R2 | **sshd 未使用 `-D` 前台模式** | 中 | 第42行直接调用 `/usr/sbin/sshd`,它会 fork 为守护进程。如果后续命令失败导致脚本退出,sshd 会成为孤儿进程。建议用 `-D` 参数保持前台或正确追踪 PID。 |
| R3 | **sleep 1 硬编码等待时间** | 低 | 与 Ubuntu 版相同,仅等待 1 秒判断 ttyd 存活。慢速系统上可能不够。 |
| R4 | **dev-tools.sh 中的 ls -d 可能匹配多个目录** | 低 | 第38行 `ls -d /root/rust-*/bin` 如果存在多个 rust 版本目录,`head -1` 只取第一个。行为可预测但隐式依赖排序。 |
| R5 | **cleanup 中 wait 无参数** | 低 | 第11行 `wait 2>/dev/null` 等待所有后台子进程。由于只跟踪了 TTYD_PID 和 SSHD_PID(后者实际为 0),行为基本正确但语义不精确。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **shebang 使用 bash 但环境为 Alpine/musl** | 中 | `#!/bin/bash` 要求安装 `bash` 包。Dockerfile 第31行已包含 `bash` 依赖,所以当前可工作。但如果未来精简镜像可能出问题。脚本中未使用任何 bashism(数组、`[[ ]]` 等),理论上可改用 `#!/bin/sh`。 |
| P2 | **pidof 替代 pgrep** | 正面 | 第55行使用 `pidof sshd` 而非 `pgrep -x sshd`,这是正确的 POSIX 兼容选择,在 BusyBox 环境下可靠工作。 |
| P3 | **gvm source 命令** | 低 | dev-tools.sh 第34行 `source /root/gvm/scripts/gvm` 依赖 gvm 的初始化脚本格式。如果 gvm 不存在该路径会静默失败(有 `2>/dev/null`)。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **ttyd 密码过于简单 `ada:123`** | **[已改进]** | 原第45行 `-c ada:123` 已改为从 `${TTYD_CREDENTIALS:-jc:1234567}` 环境变量读取,不再硬编码在脚本中。密码强度可通过环境变量灵活配置。 |
| S2 | **硬编码密码明文出现在脚本和日志中** | **[已改进]** | 同上,密码已改为通过环境变量 `TTYD_CREDENTIALS` 传入,不再明文出现在脚本源码中。 |
| S3 | **dev-tools.sh 使用单引号 heredoc(安全)** | 正面 | 第32行 `<< 'PROFILE'` 阻止变量展开,防止注入。这是正确的做法。 |
| S4 | **无用户隔离** | 信息 | Alpine 默认版直接以 root 运行一切,没有像 Test 版那样创建 developer 用户。作为开发环境可接受。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **开发工具检测逻辑完善** | 正面 | 覆盖 cargo/rustup/gvm/pyenv/go/rust独立安装/local bin 共 7 种开发工具路径。 |
| Q2 | **双轨 PATH 设置** | 正面 | 既在当前 shell 设置 export(第19-29行),又写入 /etc/profile.d/dev-tools.sh(第32-40行)确保新会话生效。设计合理。 |
| Q3 | **启动信息展示完整** | 正面 | 展示 Node/npm/Claude/Rust/Go/Python 版本 + 连接方式,便于快速确认环境状态。 |
| Q4 | **版本检测容错好** | 正面 | 所有 `--version` 命令都有 `2>/dev/null \|\| echo '未安装'` 保护。 |
| Q5 | **cleanup 函数与 Ubuntu 版完全重复** | 低 | cleanup/trap 模式在三份 entrypoint 中复制粘贴。 |
#### 代码质量评分:**7.5 / 10**
> 扣分项:SSHD_PID 未赋值(-1)、密码过弱(-0.5)、sshd 前台模式(-0.5)、代码重复(-0.5)
---
### entrypoint-test.shTest 容器入口) **[归档]**
> 此文件已移至 `_archive/`,以下问题仅作历史记录。
**文件路径:** `E:/wk-lab/workpod-alpine/entrypoint-test.sh`(已归档至 `_archive/`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **SSHD_PID 未赋值(继承自 Alpine 版)** | 高 | 同 entrypoint.sh 的 R1 问题。 |
| R2 | **adduser -D 无密码设置** | 中 | 第32行 `adduser -D -s /bin/bash developer` 创建用户时未设置密码。developer 用户只能通过 sudo 操作,不能直接 SSH 登录(除非配置了密钥认证)。这可能是故意的,但应明确注释说明。 |
| R3 | **sudoers 文件写入无原子性保护** | 低 | 第34行直接 `echo ... > /etc/sudoers.d/developer`。如果脚本在中途被中断,可能留下不完整的 sudoers 文件导致 sudo 不可用。建议先写临时文件再 mv。 |
| R4 | **chown -R 递归操作范围大** | 低 | 第37行 `chown -R developer:developer /home/developer` 对整个 home 目录递归修改所有权。如果有其他进程正在向该目录写入文件,可能出现竞态。但在容器启动阶段风险极低。 |
| R5 | **settings.json 双引号 heredoc 安全** | 正面 | 第41行和第51行都使用 `<< 'SETTINGS'` 单引号 heredocJSON 内容不会被 shell 解释。做法正确。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **adduser -D 是 Alpine 特有** | 信息 | `-D` 标志表示"不分配密码",是 BusyBox adduser 的扩展。本脚本专用于 Alpine,无移植需求。 |
| P2 | **bash 依赖同 Alpine 版** | 中 | 同 entrypoint.sh 的 P1 问题。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **Claude Code settings.json 全权限 allow:["*"]** | **严重 [归档]** | 第43-47行和第53-57行为 root 和 developer 用户都设置了 `"allow": ["*"]`,意味着 Claude Code 可以执行任意文件读写、命令执行等操作而无需用户确认。虽然这是开发环境的故意设计(沙箱用途),但应在文档中明确标注此安全策略的影响范围。**归档文件中的配置,不影响当前版本。** |
| S2 | **.bashrc 中环境变量传递方式** | 中 | 第67-70行使用双引号 heredoc `<< BASHRC` 并在其中引用 `${ANTHROPIC_AUTH_TOKEN}` 等变量。这里变量会在 heredoc 写入时展开——即使用 entrypoint 进程的环境变量值。这意味着:(1) 如果环境变量未设置,会写入空字符串;(2) 如果值包含特殊字符(如 `$`` `),可能被二次解释。当前用法基本安全但需注意边界情况。 |
| S3 | **NOPASSWD:ALL sudo 权限** | 高 [归档] | 第34行给 developer 用户无密码完整 sudo 权限。配合 Claude Code 全权限配置,developer 用户等同于 root。这是有意的设计决策(全权限沙箱),但安全边界完全消失。**同上,归档文件中的配置。** |
| S4 | **.bashrc 中 PATH 包含 `$PATH`** | 正面 | 第66行 `export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH"` 使用 `\$PATH` 在双引号 heredoc 中转义 `$`,确保写入文件的是字面量 `$PATH` 而非展开后的值。这个转义是正确的。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **功能模块划分清晰** | 正面 | 按顺序分为:开发工具检测 -> 用户创建 -> Claude 配置(root) -> Claude 配置(developer) -> 别名配置(root) -> 启动服务 -> 信息展示。结构清晰。 |
| Q2 | **root 和 developer 配置对称处理** | 正面 | settings.json 和 .bashrc 分别为两个用户配置,且 chown 所有权正确。 |
| Q3 | **alias 区分 root/developer 参数** | 正面 | root 用 `--allow-dangerously-skip-permissions`(第76行),developer 用 `--dangerously-skip-permissions`(第65行)。准确反映了 Claude Code 对 root 用户的限制。 |
| Q4 | **与 entrypoint.sh (Alpine) 大量重复** | 高 [归档] | 开发工具检测(第19-29行 vs 第19-29行完全相同)、dev-tools.sh 写入(第80-88行 vs 第32-40行完全相同)、cleanup 函数(第7-14行相同)、服务启动和检查(第90-106行几乎相同)。唯一差异是用户创建和 Claude 配置部分(第31-78行)。重复率约 65%。**因归档而不再是活跃问题。** |
| Q5 | **环境变量默认值处理良好** | 正面 | 第69-70行 `${API_TIMEOUT_MS:-3000000}``${CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1}` 提供了合理的默认值。 |
| Q6 | **端口/标题/密码硬编码** | 低 | 7682、2223、"WorkPod Test"、`jc:1234567` 全部硬编码。(注:此为已归档的 entrypoint-test.sh 中的历史值;当前基础实例已使用 2222/7681 端口,凭据通过环境变量注入) |
#### 代码质量评分:**7 / 10** (历史评分,文件已归档)
> 扣分项:与 entrypoint.sh 高度重复(-1.5)、SSHD_PID 未赋值(-1)、全权限安全策略未文档化(-0.5)
---
### ttyd-session.shtmux 会话管理)
**文件路径:** `E:/wk-lab/workpod/ttyd-session.sh`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **set -e 缺失** | 中 | 脚本没有 `set -e`。如果中间某条命令失败(如 `tmux list-sessions`),脚本会继续执行可能导致错误状态扩散。考虑到脚本的交互性质(需要 read 输入),`set -euo pipefail` 更合适但需注意 `read` 在 set -e 下的行为(read 到 EOF 返回非零不会终止脚本,因为它是条件上下文的一部分)。 |
| R2 | **SESSION_NAME 初始为空字符串** | 低 | 第4行 `SESSION_NAME=""` 初始化为空。如果 TTYD_QUERY_STRING 解析也得到空值(第9行的 grep 无匹配),则进入交互菜单分支。流程正确但变量生命周期不够明确。 |
| R3 | **tmux session 名过滤后可能为空** | 中 | 第55行 `tr -cd 'a-zA-Z0-9_\-'` 过滤后如果 SESSION_NAME 变成空字符串(例如原始输入全是特殊字符),第58行 `tmux new-session -t ""` 会创建一个名为空字符串的 session。虽然 tmux 允许这样做但不推荐。应在过滤后检查是否为空并赋予默认值 "default"。 |
| R4 | **sed -n "${CHOICE}p" 未做范围校验** | 中 | 第45行 `sed -n "${CHOICE}p"` 直接将用户输入的数字传给 sed。虽然前面有 `grep -qE '^[0-9]+$'` 校验了纯数字,但没有检查数字是否在有效范围内(1 到 session 数量之间)。超出范围的 CHOICE 会导致 sed 输出空行,SESSION_NAME 被设为空字符串,然后触发 R3 的问题。 |
| R5 | **EXISTING 变量的 word splitting** | 低 | 第20行 `tmux list-sessions` 输出类似 `my_session: (attached)` 格式,awk 提取第一列后得到 `my_session:`(带冒号)。第24行 `for s in $EXISTING` 依赖 word splitting 来遍历 session 名。如果 session 名包含空格(虽然 tmux 不允许),会出错。实际风险低。 |
| R6 | **exec tmux attach/new 替换进程** | 正面 | 第59-61行使用 `exec` 替换当前 shell 进程为 tmux,避免多余的 shell 进程残留。这是正确的做法。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **BusyBox awk 兼容性(已修复)** | 已解决 | 历史上使用 `grep -oP`Perl 正则)解析 query string,在 BusyBox 中不可用。当前版本第9行改用 `tr '\&' '\n' \| grep '^session=' \| cut -d= -f2` 的管道链,完全兼容 BusyBox。 |
| P2 | **tr -cd 字符类兼容性** | 低 | 第55行 `tr -cd 'a-zA-Z0-9_\-'` 中的 `\-` 在 GNU tr 和 BusyBox tr 中都能正确解释为字面量连字符。POSIX 标准中字符类内的 `-` 放在首位或末位或转义均可。 |
| P3 | **printf 格式化** | 正面 | 第25行 `printf "║ [%d] %-33s║\n" "$I" "$s"` 使用标准 printf 格式,跨实现兼容。 |
| P4 | **clear 命令** | 信息 | 第51行 `clear` 依赖 terminfo 数据库。在 Docker 的 ttyd 环境中通常可用。 |
| P5 | **grep -qE** | 正面 | 第44行使用 `grep -qE`(扩展正则),BusyBox awk 支持此选项。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **TTYD_QUERY_STRING 注入防护** | 正面 | 第9行通过管道链 `tr \| grep \| cut` 解析,最终结果经过第55行 `tr -cd` 过滤为安全字符集。即使攻击者构造恶意的 query string(如 `session=;rm -rf /`),也会被过滤掉。安全性良好。 |
| S2 | **用户输入 SESSION_NAME 注入防护** | 正面 | 第55行将用户交互输入同样通过 `tr -cd` 过滤,tmux session 名被限制在 `[a-zA-Z0-9_-]` 字符集中。无法注入 shell 命令。 |
| S3 | **无外部命令拼接** | 正面 | 所有变量传递给 tmux 时都加了双引号(`"$SESSION_NAME"`),防止 word splitting 和 globbing。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **UI 设计精美** | 正面 | 使用 Unicode 制表符绘制边框,视觉效果专业。编号选择 + 自定义名称的双重输入方式用户体验好。 |
| Q2 | **双模式设计合理** | 正面 | URL 带 `?session=xxx` 直接进入(自动化场景),不带参数显示菜单(手动场景)。覆盖了两种主要使用方式。 |
| Q3 | **默认值 fallback** | 正面 | 第39-41行用户输入空时默认为 "default"。 |
| Q4 | **缺少 set -e/o pipefail** | 低 | 如 R1 所述。对于这种交互式脚本,建议至少加 `set -uo pipefail`(不含 -e 以免影响 read)。 |
| Q5 | **冒号未从 session 名中去除** | 低 | 第20行 awk `{print $1}` 提取的是 `session_name:`(带尾部冒号)。这个带冒号的名字会被用于显示(第25行)和选择(第45行 sed 取行),最终传入 tmux 的也是带冒号的名字。tmux 本身允许 session 名包含冒号,所以功能不受影响,但显示上不够干净。可以用 `tr -d ':'` 清理。 |
| Q6 | **代码简洁高效** | 正面 | 仅 63 行实现了 query string 解析、交互式菜单、session 创建/附加的完整功能。没有冗余逻辑。 |
#### 代码质量评分:**8 / 10**
> 扣分项:缺少 set -euo pipefail-0.5)、session 名空值/超范围未防护(-1)、冒号未清理(-0.5)
---
## 跨脚本问题
| # | 问题 | 影响范围 | 说明 |
|---|----------|----------|------|
| X1 | **entrypoint.sh 与 entrypoint-test.sh(归档) 高度重复** | 2 个文件(其中1个已归档) | 开发工具检测(12行)、dev-tools.sh 写入(8行)、cleanup函数(8行)、trap注册(1行)、SSH启动+检查(10行)、ttyd启动+检查(8行)、信息展示(18行)、exec sleep(1行) —— 约 66 行完全相同,占总代码量 66/210 = 31%(不计公共部分则重复率更高)。**优先级大幅降低(entrypoint-test.sh 已归档)。** |
| X2 | **三个版本的 cleanup 函数逐字相同** | 3 个文件 | Ubuntu/Alpine/Test 三份 entrypoint 的 cleanup 函数(第7-14行)完全一致。这是典型的 DRY 违反。 |
| X3 | **ttyd 认证密码三套不同策略** | 3 个文件 | Ubuntu版无密码 / Alpine版 `ada:123` / Test版 `jc:1234567`。安全级别不一致,容易在部署时混淆。**[已修复]** — 当前版本统一从 `${TTYD_CREDENTIALS}` 环境变量读取。 |
| X4 | **dev-tools.sh 内容在 Alpine 和 Test 版中逐字相同** | 2 个文件 | entrypoint.sh 第80-88行与 entrypoint-test.sh 第32-40行的 heredoc 内容完全一致。 |
| X5 | **SSHD_PID 跟踪在所有版本中都无效** | 3 个文件(其中1个已归档) | 三个版本的 entrypoint 都声明了 `SSHD_PID=0` 且从未赋值,cleanup 中的 `kill "$SSHD_PID"` 分支永远不会执行。这是一个系统性缺陷。(低优先级,未修复) |
| X6 | **端口号/标题/密码散落在各脚本中** | 3 个文件(其中1个已归档) | 修改端口或密码需要在多个文件中同步修改,遗漏任一文件会导致配置不一致。**[已改善]** — 当前版本已集中到 docker-compose.yml 的 environment 配置中管理。 |
---
## Top 10 改进建议
| 优先级 | 建议 | 涉及脚本 | 收益 |
|--------|-------|----------|------|
| P0 | **重构 entrypoint 公共框架(配置驱动)** | entrypoint.sh (x2, 其中1个已归档) | 消除 ~66 行重复代码,新增容器变体只需配置文件(优先级降低:entrypoint-test.sh 已归档) |
| P1 | **修复 SSHD_PID 追踪(统一方案)** | entrypoint.sh (x3) | 实现 sshd 优雅关闭,防止孤儿进程 |
| P2 | **ttyd-session.sh 添加 set -uo pipefail + 输入校验** | ttyd-session.sh | 防止空 session 名和越界访问 |
| P3 | **统一 ttyd 认证策略,密码从环境变量读取** | entrypoint.sh (x3) | 消除安全配置不一致,支持灵活部署(**已完成**) |
| P4 | **ttyd-session.sh 清理 session 名尾部冒号** | ttyd-session.sh | UI 显示更干净,避免意外行为 |
| P5 | **entrypoint-test.sh sudoers 文件写入增加原子性** | entrypoint-test.sh | 防止中断导致 sudo 不可用 |
| P6 | **document 全权限安全策略的影响范围** | entrypoint-test.sh | 让使用者明确了解 `allow:["*"]` + `NOPASSWD:ALL` 的安全含义 |
| P7 | **端口/标题/密码等配置外部化为环境变量** | entrypoint.sh (x3) | docker-compose.yml 集中管理,消除散落配置 |
| P8 | **考虑 entrypoint.sh shebang 改为 /bin/sh** | entrypoint.sh (Alpine x2) | 减少 bash 依赖,Alpine 环境更轻量(需确认无 bashism) |
| P9 | **sshd 启动增加 -D 模式或 PID 文件追踪** | entrypoint.sh (Alpine x2) | 更精确的进程生命周期管理 |
---
## 附录:历史 Bug 回顾
| Bug | 影响 | 当前状态 |
|-----|------|----------|
| grep -P 在 BusyBox 不可用 | Alpine 环境 TTYD_QUERY_STRING 解析失败 | ttyd-session.sh 第9行已改用 `tr & '\n' \| grep '^session=' \| cut -d= -f2` 管道链,完全兼容 BusyBox |
| heredoc 单引号阻止变量展开 | .bashrc 中 ANTHROPIC_AUTH_TOKEN 等环境变量无法传递到 developer 用户 | entrypoint-test.sh 第64行已改用 `<< BASHRC` 双引号 heredoc,变量在写入时正确展开;`\$PATH` 转义确保字面量输出 |
| dev-tools.sh 反斜杠转义问题 | profile.d 脚本中 `\$PATH``\\` 导致语法错误 | 当前版本 dev-tools.sh 使用单引号 heredoc `<< 'PROFILE'`,内容原样写入无需转义,已修复 |
@@ -0,0 +1,333 @@
# 性能与可维护性审核报告
> 审核日期:2026-04-07
> 审核范围:WorkPod Alpine 版 (E:/wk-lab/workpod)
> 镜像大小:~436MB | 基础镜像:alpine:3.23(多阶段构建)| 内存限制:4G / 1G reservation
> 部署环境:测试服 4 个实例共享镜像
---
## 一、性能审核
### 1.1 镜像性能
| 指标 | 当前值 | 行业标准(同类开发容器) | 评价 |
|------|--------|------------------------|------|
| **镜像大小** | ~436 MB | 200~500 MB 为佳 | **优** -- 多阶段构建效果显著 |
| **基础镜像** | alpine:3.23 (~7MB) | alpine/debian-slim | **优** -- 最小化基础 |
| **构建层数** | 5 层有效(2阶段) | 3~6 层为佳 | **优** -- 多阶段构建,builder 层不进入最终镜像 |
| **层缓存命中率** | 中等 | >70% 为佳 | 中 -- npm install 每次可能因版本变化失效 |
| **构建产物清理** | builder 阶段自动丢弃 | 必须清理 | **优** -- 多阶段天然隔离 |
| **推送/拉取效率** | 436 MB 全量传输 | <500 MB 理想 | **优** -- 比 Ubuntu 版小 6 倍 |
| **存储占用** | ~436 MB/实例 x 4 = ~1.7 GB | <2 GB 总量 | **优** -- 4 实例总占用仍小于 Ubuntu 单实例 |
#### 层分析(多阶段构建)
```
========== Builder 阶段 (不进入最终镜像) ==========
Layer B1 (FROM): alpine:3.23 ~7 MB
Layer B2 (RUN apk): xz + libstdc++ ~15 MB
Layer B3 (COPY pkgs): 本地 packages/ ~325 MB
Layer B4 (RUN npm): Node.js + claude + helper ~400 MB (含缓存)
─────────────────────────────────────────────────────
Builder 阶段总计: ~747 MB (构建时临时)
========== 运行阶段 (最终镜像) ==========
Layer R1 (FROM): alpine:3.23 ~7 MB
Layer R2 (RUN apk): curl/git/ssh/ttyd/tmux/bash ~80 MB
Layer R3 (COPY): /opt/node -> /usr/local ~200 MB (仅产物)
Layer R4 (COPY+RUN): entrypoint + ttyd-session <10 KB
─────────────────────────────────────────────────────
最终镜像总计: ~436 MB (压缩后)
```
#### 与 Ubuntu 版对比
| 对比项 | Ubuntu 版 | Alpine 版 | 倍率 |
|--------|----------|-----------|------|
| 镜像大小 | 2.6 GB | 436 MB | **6x 更小** |
| 基础镜像 | ubuntu:22.04 (77MB) | alpine:3.23 (7MB) | **11x 更小** |
| 内存基线 | ~400 MB | ~80 MB | **5x 更省** |
| 构建策略 | 单阶段 | 多阶段 | Alpine 更优 |
| 推送时间(100Mbps) | ~3.5 min | ~35 s | **6x 更快** |
### 1.2 运行时性能
| 指标 | 当前值 | 行业标准 | 评价 |
|------|--------|---------|------|
| **内存基线占用** | ~80-120 MB (Alpine base) | 50-150 MB | **优** -- Alpine 最小化系统 |
| **应用内存** | Node.js ~50 MB + ttyd ~8 MB + sshd ~3 MB + tmux ~2 MB | 合理范围 | **优** |
| **总内存限制** | 4G limit / 1G reservation | 开发容器 2-4G 即够 | **良** -- reservation 1G 合理 |
| **启动时间** | ~3-5s (entrypoint 执行) | <10s 可接受 | **良** |
| **entrypoint 流程** | PATH检测 -> 用户创建 -> 配置写入 -> SSH启动 -> ttyd启动 -> 健康检查 | 较复杂 | 中 -- 步骤较多但必要 |
| **信号处理** | trap SIGTERM/SIGINT/SIGQUIT + cleanup() | 必须具备 | 良 -- 有优雅关闭 |
| **ttyd 性能** | WebSocket 直连 + tmux 会话管理 | 标准 | **优** -- 会话恢复能力强 |
| **特权模式** | privileged: true | 应避免 | 差 -- 安全风险 |
| **4 实例并发** | 共享同一镜像 | 标准 | **优** -- 镜像小,存储压力低 |
#### 启动流程时间分解
```
t=0s entrypoint.sh 开始执行
t≈0.05s PATH 自动检测 (6 个目录检查)
t≈0.1s 创建 developer 用户 + sudoers + .claude 配置 (~10 个文件操作)
t≈0.3s 写入 /etc/profile.d/dev-tools.sh
t≈0.5s /usr/sbin/sshd 启动
t≈0.6s ttyd -W -c ... /opt/ttyd-session.sh & (后台启动)
t≈1.6s sleep 1 (等待 ttyd 就绪)
t≈1.7s kill -0 $TTYD_PID (健康验证)
t≈1.8s pidof sshd (SSH 验证)
t≈2.5s 版本检测输出 (node/npm/claude/rust/go/python)
t≈2.5s exec sleep infinity (PID 1 接管)
总计:约 2.5-4 秒(比 Ubuntu 稍慢,因多了用户创建和配置步骤)
```
### 1.3 I/O 性能
| 指标 | 当前配置 | 评价 |
|------|---------|------|
| **卷挂载方式** | bind mount (`./data/workspace:/workspace`) | 标准 |
| **日志驱动** | json-file, max-size=10m, max-file=3 | 良 -- 有轮转 |
| **/root 持久化** | docker-compose-alpine.yml 有挂载 (`./data/workpod-alpine/root:/root`) | 良 -- 支持工具持久化 |
| **tmpfs 使用** | 未使用 | 中 -- /tmp、/run 可用 tmpfs 提升性能 |
| **entrypoint.sh / ttyd-session.sh 内置** | `COPY --chmod=755 entrypoint.sh /entrypoint.sh` + `COPY --chmod=755 ttyd-session.sh /opt/ttyd-session.sh` (Dockerfile) | **优** -- 已内置到镜像,符合 immutable artifact 最佳实践 |
#### entrypoint / ttyd-session 内置到镜像
entrypoint.sh 和 ttyd-session.sh 均已通过 `COPY --chmod=755` 固化到镜像中(Dockerfile),不再使用 bind mount 外挂。这是 4/4 修复 execvp failed 问题时做的改动。
**改进效果**
- 消除了宿主机文件缺失导致容器启动失败的风险(4/4 execvp failed 根因)
- 文件权限由 Docker COPY 的 `--chmod=755` 保证,不受 Windows/Linux 路径转换影响
- 符合"镜像即 immutable artifact"的最佳实践
- 代价:开发阶段修改脚本后需要 rebuild 镜像(可接受)
---
## 二、可维护性审核
### 2.1 文档评估
| 文档 | 完整度 | 时效性 | 问题 |
|------|--------|--------|------|
| **ISSUES.md** | 9/10 | 最新 (2026-04-07) | 问题记录详实,含根因分析和解决方案;是项目最有价值的运维文档 |
| **README.md** | 缺失 | N/A | **仍然缺失** -- 新人无法快速上手(P0 级缺口) |
| **SPECS.md** | 缺失 | N/A | **无技术规格文档** -- 架构决策无书面记录 |
| **CHANGELOG** | 缺失 | N/A | **无变更日志** -- ISSUES.md 部分承担此功能但不规范 |
| **PACKAGES.md** | 缺失 | N/A | **仍然缺失** -- 包清单未独立维护 |
| **API 文档** | 0/10 | 不适用 | auth-proxy.js 的 API 无文档 |
#### 关键文档问题
1. **缺少 README.md** -- 作为测试服部署的正式版本,没有入门文档是不可接受的。新运维人员无法知道如何构建、启动、连接。
2. **ISSUES.md 承担了过多角色** -- 它同时充当了 CHANGELOG、FAQ、故障排查指南的角色,结构上不如独立文档清晰。
3. **auth-proxy.js 和 login.html 无文档** -- 认证代理是安全关键组件,其工作原理、配置方法应有说明。
### 2.2 代码可维护性
#### 2.2.1 版本管理(硬编码集中度)
| 组件 | Dockerfile | download-packages.sh | 分散度 |
|------|-----------|---------------------|--------|
| Node.js | v24.14.1 (musl) | v24.14.1 (musl) | 低 |
| Claude Code | @latest (Dockerfile) | 2.1.87 (download-packages.sh) | 中 -- Dockerfile 已改用 @latest,构建时拉取最新版 |
| coding-helper | @latest | 0.0.7 | **中 (@latest 不确定)** -- 用户决策:保持 @latest 以自动跟进更新 |
| OpenClaw | 未安装 | 2026.3.28 (下载了但未安装) | 中 -- download-packages.sh 已补回主目录,保留下载逻辑 |
| ttyd | (apk, 版本由仓库决定) | - | 低 |
**问题状态**
1. **Claude Code 版本不一致** -- **[已解决]** Dockerfile 已改为 `@anthropic-ai/claude-code@latest`Dockerfile:18),不再硬编码版本号。download-packages.sh 仍保留固定版本 2.1.87 用于离线缓存,两者不再冲突。
2. **OpenClaw 下载了但未安装** -- 保持原状。download-packages.sh 已从 workpod-alpine 补回主目录,保留 OpenClaw 下载逻辑供未来使用。
3. **coding-helper@latest** -- 用户决策保持 `@latest`,接受构建结果不确定性以换取自动更新便利。
#### 2.2.2 配置管理
| 配置项 | 硬编码位置 | 是否可通过环境变量覆盖 |
|--------|-----------|---------------------|
| SSH 密码 | Dockerfile (`ROOT_PASSWORD:-workpod123`) | **是** -- ROOT_PASSWORD 环境变量(docker-compose.yml:26 |
| ttyd 凭据 | entrypoint.sh (`TTYD_CREDENTIALS:-jc:1234567`) | **是** -- TTYD_CREDENTIALS 环境变量(docker-compose.yml:27 |
| ttyd 主题色 | entrypoint.sh (`#1a1a2e`) | 否 |
| npm registry | Dockerfile (`npmmirror.com`) | 否 |
| Claude Code 别名 | .bashrc (`--dangerously-skip-permissions` / `--allow-dangerously-skip-permissions`) | 否 |
| 时区 | Dockerfile ENV + docker-compose | 是 |
| 内存限制 | docker-compose | 是 |
**问题状态**
- **SSH 密码硬编码** -- **[已改善]** Dockerfile 改为 `${ROOT_PASSWORD:-workpod123}`Dockerfile:34),docker-compose.yml 通过环境变量传入 `ROOT_PASSWORD`。默认值仍为 workpod123 但已可外部配置。
- **ttyd 凭据硬编码** -- **[已修复]** entrypoint.sh 改为 `${TTYD_CREDENTIALS:-jc:1234567}`entrypoint.sh:50),docker-compose.yml 通过 `TTYD_CREDENTIALS` 环境变量传入。凭据完全外部化。
- **两套 entrypoint 凭据不一致** -- **[不适用]** entrypoint-test.sh 已归档,当前仅保留一份 entrypoint.sh,不存在多份脚本凭据不一致问题。
- **Claude Code 全权限别名硬编码**`--dangerously-skip-permissions` 直接写在 .bashrc 中,无法通过环境变量控制。
#### 2.2.3 当前代码规模(Alpine 主版本)
> 注:Ubuntu 版已不在主目录中,不再进行跨版本对比。
| 文件 | 行数 | 说明 |
|------|------|------|
| Dockerfile | 58 | 多阶段构建,含 ROOT_PASSWORD/TTYD_CREDENTIALS 环境变量支持 |
| entrypoint.sh | 86 | 单一入口脚本,凭据通过 TTYD_CREDENTIALS 环境变量读取 |
| ttyd-session.sh | 63 | ttyd 会话管理脚本(Alpine 独有) |
| docker-compose.yml | 64 | 含 ROOT_PASSWORD/TTYD_CREDENTIALS 环境变量配置 |
| download-packages.sh | 65 | 包下载脚本(已补回主目录) |
| auth-proxy.js | ~173 | 认证代理(Alpine 独有) |
**代码质量改善**
- entrypoint 从 3 份脚本(entrypoint.sh x2 + entrypoint-test.sh)精简为 **1 份**entrypoint.sh),消除了脚本间的不一致风险。
- 凭据统一通过环境变量注入(ROOT_PASSWORD、TTYD_CREDENTIALS),不再硬编码在多份文件中。
#### 2.2.4 代码质量问题
| # | 位置 | 问题 | 严重程度 | 状态 |
|---|------|------|---------|------|
| 1 | Dockerfile:18 vs download-packages.sh:32 | Claude Code 版本不一致(已改 @latest | 高 | **[已解决]** -- Dockerfile 改为 @latest,不再硬编码版本号 |
| 2 | download-packages.sh:40-48 | OpenClaw 下载但 Dockerfile 未安装 | 中 | 保持 -- download-packages.sh 已补回主目录,保留供未来使用 |
| 3 | entrypoint.sh:50 (历史) | ttyd 凭据不一致(多脚本时代) | 中 | **[已解决]** -- 单一 entrypoint.sh + TTYD_CREDENTIALS 环境变量 |
| 4 | Dockerfile:19 | `@z_ai/coding-helper@latest` 不确定版本 | 中 | 用户决策保持 @latest |
| 5 | entrypoint-test.sh (已归档) | ANTHROPIC_AUTH_TOKEN 等 env 直接嵌入 .bashrc | 低 | **[不适用]** -- entrypoint-test.sh 已归档 |
| 6 | auth-proxy.js:8 | 密码明文写死在源码中 (`1234567`, `admin123`) | **高** | 保持 -- auth-proxy.js 已补回主目录,密码外部化待后续处理 |
| 7 | docker-compose.yml (历史) | entrypoint.sh 外挂而非内置镜像 | 中 | **[已解决]** -- 改为 COPY --chmod=755 内置到镜像 |
### 2.3 运维评估
#### 2.3.1 故障排查便利性
| 能力 | 具备情况 | 评价 |
|------|---------|------|
| **健康检查** | HEALTHCHECK (netstat) + docker-compose healthcheck | 良 -- 已修复 ss->netstat 问题(见 ISSUES.md |
| **问题追踪** | ISSUES.md 实时记录 | **优** -- 这是项目最大的运维亮点 |
| **结构化日志** | 无 -- 仅文本输出 | 中 |
| **日志轮转** | json-file driver, 10m*3 | 良 |
| **启动诊断输出** | entrypoint 打印完整版本信息(Node/npm/Claude/Rust/Go/Python | **优** -- 比 Ubuntu 版更全面 |
| **错误退出码** | ttyd/sshd 失败 exit 1 | 良 |
| **监控指标** | 无 | 差 |
| **会话恢复** | tmux + ttyd-session.sh | **优** -- 断线重连不丢失上下文 |
#### 2.3.2 升级流程复杂度
| 升级场景 | 步骤数 | 复杂度 |
|---------|-------|--------|
| 升级 Node.js | 3 步(改下载脚本 -> 下载 musl 包 -> rebuild | 中 -- musl 包源不同 |
| 升级 Claude Code | 3 步(需同步改 Dockerfile + download-packages.sh | **中偏高** -- 两处版本号要一致 |
| 升级 ttyd | 1 步(rebuildapk 自动拉取) | 低 -- 但版本不可控 |
| 新增开发实例 | 复制 docker-compose-alpine.yml 改端口 | 低 |
| 同步修复到 Ubuntu | 手动对比 + 双份修改 | **高** |
#### 2.3.3 回滚能力
| 场景 | 回滚方式 | 可行性 |
|------|---------|--------|
| 镜像回滚 | `docker load < workpod-alpine-latest.tar.gz` (已存在) | **优** -- 有导出文件 |
| 数据回滚 | /root 已 bind mount,可手动备份 | 中 |
| 配置回滚 | git checkout | 良 |
| 快速回退 | 无原生支持 | 中 |
#### 2.3.4 测试服 4 实例运营评估
| 项目 | 当前状态 | 评价 |
|------|---------|------|
| **镜像共享** | 4 实例共用 workpod-alpine:latest | **优** -- 存储高效 |
| **端口规划** | 2222(SSH 基础实例), 7681(ttyd 基础实例) / 2201(SSH Flux), 7701(ttyd Flux) | 中 -- 需要文档化端口分配表 |
| **数据隔离** | 各实例独立 data/ 目录 | **优** |
| **认证代理** | auth-proxy.js (端口 8080) | **优** -- 统一入口 + token 认证 |
| **负载均衡** | 无 (各实例独立端口) | 中 -- 小规模够用 |
| **扩容** | 复制 compose 文件改端口 | 低 -- 手动但简单 |
#### 2.3.5 安全评估
| 项目 | 当前状态 | 风险等级 |
|------|---------|---------|
| privileged: true | 启用 | **高** |
| root 用户运行 | 默认 root | **高** |
| SSH 密码 | ROOT_PASSWORD 环境变量(默认 workpod123 | **中→改善** -- 已可外部配置 |
| ttyd 认证 | TTYD_CREDENTIALS 环境变量(默认 jc:1234567 | **低→更好** -- 凭据完全外部化,比硬编码显著改善 |
| auth-proxy | Basic Auth + Token | **良** -- 有认证层 |
| auth-proxy 密码 | 明码硬编码 (1234567, admin123) | **高** -- auth-proxy.js 自身问题,待后续外部化 |
| 端口暴露 | 2222 + 7681 (基础实例) / 2201 + 7701 (Flux 实例) | **中** |
| developer 用户 | sudo NOPASSWD | **中** |
| Claude Code 全权限 | --dangerously-skip-permissions 默认开启 | **中** |
---
## 三、综合评分
| 维度 | Alpine 版得分 | 说明 |
|------|-------------|------|
| **性能** | **8.5/10** | 镜像极小(436MB)、内存占用低、多阶段构建优秀;entrypoint 已内置镜像(+0.5);privileged 仍扣分 |
| **可维护性** | **6/10** | 较上期 +1:版本统一(@latest)、凭据外部化(ROOT_PASSWORD/TTYD_CREDENTIALS)、单 entrypoint 脚本、entrypoint 内置镜像;仍缺 README/SPECS/PACKAGES、@latest tag 不确定、auth-proxy 密码未外部化 |
| **文档** | **4/10** | ISSUES.md 一枝独秀(9/10);但零 README(仍然缺失)、零 SPECS、零 CHANGELOG、零 PACKAGES,新人完全无法入手 |
| **运维** | **6.5/10** | 较上期 +0.5:健康检查完善、问题追踪及时、tmux 会话管理优秀、认证代理加分、凭据可配置化改善运维体验;无监控、安全配置有改进空间 |
| **总分** | **25/40** | 性能突出,可维护性和运维较上期有实质改善;文档短板仍是最大拖累,属于"技术优秀、工程化持续改善中"水平 |
---
## 四、优先改进项(按 ROI 排序)
| # | 改进项 | 影响 | 成本 | ROI | 说明 |
|---|--------|------|------|-----|------|
| 1 | **创建 README.md** | 新人可快速上手 | 低 | **极高** | **仍然是 P0** -- 作为测试服正式版本,这是最紧迫的缺口 |
| 2 | ~~统一 Claude Code 版本号~~ | 消除构建不确定性 | 极低 | **极高** | **[完成]** -- Dockerfile 已改为 @latest,不再硬编码版本号 |
| 3 | ~~移除 OpenClaw 下载或安装它~~ | 消除无用依赖 | 极低 | 高 | **降级** -- download-packages.sh 已补回主目录,保留下载逻辑供未来使用;不再紧迫 |
| 4 | **提取 entrypoint 公共模块** | 消除重复代码 | 中 | 中 | **优先级降低** -- 当前仅单 entrypoint 场景(entrypoint-test.sh 已归档),跨版本同步成本已消除 |
| 5 | ~~统一 ttyd 凭据管理~~ | 消除配置混乱 | 低 | **高** | **[完成]** -- TTYD_CREDENTIALS 环境变量已实现凭据外部化 |
| 6 | **auth-proxy 密码外部化** | 消除安全隐患 | 低 | **高** | auth-proxy.js 明文密码待改为环境变量或配置文件读取 |
| 7 | **coding-helper 改固定版本** | 构建可重现 | 极低 | **中** | 用户当前选择保持 @latest,如需可重现构建则替换为具体版本号 |
| 8 | ~~将 entrypoint.sh 内置到镜像~~ | 符合 immutable 镜像最佳实践 | 低 | **中** | **[完成]** -- COPY --chmod=755 已实现内置 |
| 9 | **添加 SPECS.md / PACKAGES.md** | 文档体系完善 | 中 | **中** | 多阶段架构、认证代理设计、包清单值得记录 |
| 10 | **评估取消 privileged** | 安全性提升 | 中 | 测试是否真正需要,尝试 `--cap-add SYS_ADMIN` 等细粒度替代 |
---
## 五、与 Ubuntu 版对比摘要
| 维度 | Alpine 版 | Ubuntu 版 | 胜出者 |
|------|----------|-----------|--------|
| 镜像大小 | 436 MB | 2.6 GB | **Alpine (6x)** |
| 内存基线 | ~80 MB | ~400 MB | **Alpine (5x)** |
| 构架先进性 | 多阶段构建 | 单阶段构建 | **Alpine** |
| 功能完整性 | 含 tmux session 管理 | 含 OpenClaw | 各有侧重 |
| 安全性(ttyd) | 有认证 (-c) | 无认证 (-W) | **Alpine** |
| 文档体系 | ISSUES.md 优秀但单一 | SPECS+PACKAGES+REVIEW 较全 | **Ubuntu (广度)** |
| 文档时效性 | ISSUES 实时更新 | SPECS 过时 | **Alpine** |
| 代码重复度 | 高(3 份脚本) | 中(2 份脚本) | 都差 |
| 版本一致性 | 有偏差(Claude Code | 基本一致 | **Ubuntu** |
| 生产就绪度 | 测试服 4 实例运行中 | 本地开发为主 | **Alpine** |
**结论**:Alpine 版在性能和生产适用性上是明确的胜出者。其主要债务在于文档缺失(尤其是 README)和版本号不一致。建议以 Alpine 版为主线版本,Ubuntu 版降级为本地调试辅助版本。
---
## 六、附录:auth-proxy.js 架构评审
auth-proxy.js 是 Alpine 版独有的认证代理组件,值得单独关注。
### 架构概览
```
浏览器 --> :8080 auth-proxy.js --> :7681 ttyd 实例
|-- /login 登录页
|-- /auth/check Basic Auth -> Token
|-- /api/workspaces 工作空间列表
|-- /* Token 验证 -> 代理到对应 ttyd
|-- (WebSocket) upgrade -> 双向管道
```
### 优点
- 轻量实现(173 行),无第三方依赖
- Token 机制(1 小时过期)避免密码反复传输
- WebSocket 代理支持终端正常工作
- 多工作空间路由
### 风险点
| # | 风险 | 说明 |
|---|------|------|
| 1 | **Token 存储在内存** | 进程重启后所有 Token 失效,用户需重新登录 |
| 2 | **密码明文硬编码** | `VALID_PASSWORDS = { wk: '1234567', admin: 'admin123' }` |
| 3 | **无 HTTPS** | 密码和 Token 明文传输 |
| 4 | **单进程无集群** | 无法横向扩展 |
| 5 | **无速率限制** | 暴力破解密码无防护 |
### 建议
- 密码改为环境变量:`process.env.AUTH_PASSWORDS`
- 生产环境前置 nginx/Terminus 做 HTTPS 结束
- 添加登录失败次数限制(内存计数器即可)
-95
View File
@@ -1,95 +0,0 @@
# Dev Box 软件包下载脚本 (Windows PowerShell)
# 使用国内镜像源,加速下载
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$PackagesDir = Join-Path $ScriptDir "packages"
New-Item -ItemType Directory -Force -Path $PackagesDir | Out-Null
Set-Location $PackagesDir
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Dev Box 软件包下载(国内镜像)" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# ============ Node.js ============
$NodeVersion = "v24.14.0"
$NodeFile = "node-$NodeVersion-linux-x64.tar.xz"
Write-Host "[1/5] Node.js $NodeVersion..." -ForegroundColor Yellow
if (-not (Test-Path $NodeFile)) {
# 淘宝镜像
Invoke-WebRequest -Uri "https://npmmirror.com/mirrors/node/$NodeVersion/$NodeFile" -OutFile $NodeFile
Write-Host " ✓ 下载完成" -ForegroundColor Green
} else {
Write-Host " - 已存在,跳过" -ForegroundColor Gray
}
# ============ Go ============
$GoVersion = "1.26.1"
$GoFile = "go$GoVersion.linux-amd64.tar.gz"
Write-Host "[2/5] Go $GoVersion..." -ForegroundColor Yellow
if (-not (Test-Path $GoFile)) {
# Go 官方中国镜像
Invoke-WebRequest -Uri "https://golang.google.cn/dl/$GoFile" -OutFile $GoFile
Write-Host " ✓ 下载完成" -ForegroundColor Green
} else {
Write-Host " - 已存在,跳过" -ForegroundColor Gray
}
# ============ Rust ============
$RustVersion = "1.94.0"
$RustFile = "rust-$RustVersion-x86_64-unknown-linux-gnu.tar.xz"
Write-Host "[3/5] Rust $RustVersion..." -ForegroundColor Yellow
if (-not (Test-Path $RustFile)) {
# 中科大镜像
Invoke-WebRequest -Uri "https://mirrors.ustc.edu.cn/rust-static/dist/$RustFile" -OutFile $RustFile
Write-Host " ✓ 下载完成" -ForegroundColor Green
} else {
Write-Host " - 已存在,跳过" -ForegroundColor Gray
}
# ============ Claude Code ============
Write-Host "[4/5] Claude Code..." -ForegroundColor Yellow
if (-not (Test-Path "claude-code-2.1.79.tgz")) {
# 设置淘宝镜像并下载
npm pack @anthropic-ai/claude-code@2.1.79 --registry=https://registry.npmmirror.com
# 重命名
Get-ChildItem "anthropic-ai-claude-code-*.tgz" | ForEach-Object {
Rename-Item $_.FullName "claude-code-2.1.79.tgz" -Force
}
Write-Host " ✓ 下载完成" -ForegroundColor Green
} else {
Write-Host " - 已存在,跳过" -ForegroundColor Gray
}
# ============ OpenClaw ============
Write-Host "[5/5] OpenClaw..." -ForegroundColor Yellow
if (-not (Test-Path "openclaw-2026.3.13.tgz")) {
# 设置淘宝镜像并下载
npm pack openclaw@2026.3.13 --registry=https://registry.npmmirror.com
# 重命名
Get-ChildItem "openclaw-*.tgz" | ForEach-Object {
Rename-Item $_.FullName "openclaw-2026.3.13.tgz" -Force
}
Write-Host " ✓ 下载完成" -ForegroundColor Green
} else {
Write-Host " - 已存在,跳过" -ForegroundColor Gray
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 下载完成" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "文件列表:" -ForegroundColor White
Get-ChildItem $PackagesDir -Filter "*.tar.*" | ForEach-Object { Write-Host " $($_.Name) ($('{0:N0}' -f ($_.Length/1MB)) MB)" }
Get-ChildItem $PackagesDir -Filter "*.tgz" | ForEach-Object { Write-Host " $($_.Name) ($('{0:N0}' -f ($_.Length/1MB)) MB)" }
$TotalSize = (Get-ChildItem $PackagesDir | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host ""
Write-Host "总大小: $('{0:N0}' -f $TotalSize) MB" -ForegroundColor Cyan
+29 -61
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Dev Box 软件包下载脚本 # WorkPod Alpine 软件包下载脚本
# 使用国内镜像源,加速下载 # 使用国内镜像源,加速下载
set -e set -e
@@ -11,75 +11,48 @@ mkdir -p "$PACKAGES_DIR"
cd "$PACKAGES_DIR" cd "$PACKAGES_DIR"
echo "========================================" echo "========================================"
echo " Dev Box 软件包下载(国内镜像)" echo " WorkPod Alpine 软件包下载"
echo "========================================" echo "========================================"
echo "" echo ""
# ============ Node.js ============ # ============ Node.js (musl build) ============
NODE_VERSION="v24.14.0" NODE_VERSION="v24.14.1"
NODE_FILE="node-${NODE_VERSION}-linux-x64.tar.xz" NODE_FILE="node-${NODE_VERSION}-linux-x64-musl.tar.gz"
echo "[1/5] Node.js ${NODE_VERSION}..." echo "[1/4] Node.js ${NODE_VERSION} (musl)..."
if [ ! -f "$NODE_FILE" ]; then if [ ! -f "$NODE_FILE" ]; then
# 淘宝镜像 curl -L -o "$NODE_FILE" "https://unofficial-builds.nodejs.org/download/release/${NODE_VERSION}/${NODE_FILE}"
curl -L -o "$NODE_FILE" "https://npmmirror.com/mirrors/node/${NODE_VERSION}/${NODE_FILE}" echo " OK"
echo " ✓ 下载完成"
else
echo " - 已存在,跳过"
fi
# ============ Go ============
GO_VERSION="1.26.1"
GO_FILE="go${GO_VERSION}.linux-amd64.tar.gz"
echo "[2/5] Go ${GO_VERSION}..."
if [ ! -f "$GO_FILE" ]; then
# Go 官方中国镜像
curl -L -o "$GO_FILE" "https://golang.google.cn/dl/${GO_FILE}"
echo " ✓ 下载完成"
else
echo " - 已存在,跳过"
fi
# ============ Rust ============
RUST_VERSION="1.94.0"
RUST_FILE="rust-${RUST_VERSION}-x86_64-unknown-linux-gnu.tar.xz"
echo "[3/5] Rust ${RUST_VERSION}..."
if [ ! -f "$RUST_FILE" ]; then
# 中科大镜像
curl -L -o "$RUST_FILE" "https://mirrors.ustc.edu.cn/rust-static/dist/${RUST_FILE}"
echo " ✓ 下载完成"
else else
echo " - 已存在,跳过" echo " - 已存在,跳过"
fi fi
# ============ Claude Code ============ # ============ Claude Code ============
echo "[4/5] Claude Code..." echo "[2/4] Claude Code..."
if [ ! -f "claude-code-2.1.78.tgz" ]; then if [ ! -f "claude-code-2.1.87.tgz" ]; then
# 需要从 npm 下载,设置淘宝镜像 npm pack @anthropic-ai/claude-code@2.1.87 --registry=https://registry.npmmirror.com
npm pack @anthropic-ai/claude-code --registry=https://registry.npmmirror.com 2>/dev/null || \ mv anthropic-ai-claude-code-*.tgz "claude-code-2.1.87.tgz"
npm pack @anthropic-ai/claude-code@2.1.78 --registry=https://registry.npmmirror.com echo " OK"
# 重命名为固定名称
for f in anthropic-ai-claude-code-*.tgz; do
[ -f "$f" ] && mv "$f" "claude-code-2.1.78.tgz"
done
echo " ✓ 下载完成"
else else
echo " - 已存在,跳过" echo " - 已存在,跳过"
fi fi
# ============ OpenClaw ============ # ============ OpenClaw ============
echo "[5/5] OpenClaw..." echo "[3/4] OpenClaw..."
if [ ! -f "openclaw-2026.3.13.tgz" ]; then if [ ! -f "openclaw-2026.3.28.tgz" ]; then
# 从 npm 下载 npm pack openclaw@2026.3.28 --registry=https://registry.npmmirror.com
npm pack openclaw@2026.3.13 --registry=https://registry.npmmirror.com 2>/dev/null || \ mv openclaw-*.tgz "openclaw-2026.3.28.tgz"
npm pack openclaw --registry=https://registry.npmmirror.com echo " OK"
# 重命名为固定名称 else
for f in openclaw-*.tgz; do echo " - 已存在,跳过"
[ -f "$f" ] && mv "$f" "openclaw-2026.3.13.tgz" fi
done
echo " ✓ 下载完成" # ============ coding-helper ============
echo "[4/4] coding-helper..."
if [ ! -f "z_ai-coding-helper-0.0.7.tgz" ]; then
npm pack @z_ai/coding-helper@latest --registry=https://registry.npmmirror.com
mv z_ai-coding-helper-*.tgz "z_ai-coding-helper-0.0.7.tgz"
echo " OK"
else else
echo " - 已存在,跳过" echo " - 已存在,跳过"
fi fi
@@ -88,9 +61,4 @@ echo ""
echo "========================================" echo "========================================"
echo " 下载完成" echo " 下载完成"
echo "========================================" echo "========================================"
echo "" ls -lh "$PACKAGES_DIR"
echo "文件列表:"
ls -lh "$PACKAGES_DIR"/*.tar.* "$PACKAGES_DIR"/*.tgz 2>/dev/null || true
echo ""
echo "总大小:"
du -sh "$PACKAGES_DIR"
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
set -e
SSHD_PID=0
TTYD_PID=0
cleanup() {
echo "[entrypoint] 收到终止信号,正在关闭服务..."
[ "$TTYD_PID" -ne 0 ] && kill "$TTYD_PID" 2>/dev/null
[ "$SSHD_PID" -ne 0 ] && kill "$SSHD_PID" 2>/dev/null
wait 2>/dev/null
echo "[entrypoint] 服务已关闭"
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
# 自动检测 /root 下的开发工具并设置 PATH
[ -d /root/.cargo/bin ] && export PATH="/root/.cargo/bin:$PATH"
[ -d /root/.rustup ] && export RUSTUP_HOME=/root/.rustup CARGO_HOME=/root/.cargo
[ -d /root/gvm/gos ] && export PATH="/root/gvm/gos/current/bin:$PATH"
[ -d /root/.pyenv/shims ] && export PATH="/root/.pyenv/shims:$PATH"
[ -d /root/.pyenv/bin ] && export PATH="/root/.pyenv/bin:$PATH"
[ -d /root/go/bin ] && export PATH="/root/go/bin:$PATH" && export GOROOT=/root/go
[ -d /root/.local/bin ] && export PATH="/root/.local/bin:$PATH"
# 检测 Rust 独立安装(非 rustup
RUST_DIR=$(ls -d /root/rust-*/bin 2>/dev/null | head -1)
[ -n "$RUST_DIR" ] && export PATH="$RUST_DIR:$PATH"
# 创建非 root 用户 developer(用于 Claude Code 全权限沙箱)
id developer 2>/dev/null || adduser -D -s /bin/bash developer
mkdir -p /etc/sudoers.d
echo "developer ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/developer
chmod 440 /etc/sudoers.d/developer
mkdir -p /home/developer/.claude
chown -R developer:developer /home/developer
# Claude Code 全权限配置 —— root 用户
mkdir -p /root/.claude
cat > /root/.claude/settings.json << 'SETTINGS'
{
"permissions": {
"allow": ["*"],
"deny": []
}
}
SETTINGS
# Claude Code 全权限配置 —— developer 用户
cat > /home/developer/.claude/settings.json << 'SETTINGS'
{
"permissions": {
"allow": ["*"],
"deny": []
}
}
SETTINGS
chown developer:developer /home/developer/.claude/settings.json
# 创建便捷别名(含 Claude Code 密钥环境变量)
# root 用 --allow-dangerously-skip-permissionsroot 禁止 --dangerously-skip-permissions
# developer 用 --dangerously-skip-permissions
cat > /home/developer/.bashrc << BASHRC
alias claude='claude --dangerously-skip-permissions'
export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH"
export ANTHROPIC_AUTH_TOKEN="${ANTHROPIC_AUTH_TOKEN}"
export ANTHROPIC_BASE_URL="${ANTHROPIC_BASE_URL}"
export API_TIMEOUT_MS="${API_TIMEOUT_MS:-3000000}"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="${CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1}"
BASHRC
chown developer:developer /home/developer/.bashrc
# root 的全局别名
cat > /root/.bashrc << 'ROOTRC'
alias claude='claude --allow-dangerously-skip-permissions'
ROOTRC
# 写入 /etc/profile.d/ 让所有 shell 会话生效
cat > /etc/profile.d/dev-tools.sh << 'PROFILE'
[ -d /root/.cargo/bin ] && export PATH="/root/.cargo/bin:$PATH" && export RUSTUP_HOME=/root/.rustup CARGO_HOME=/root/.cargo
[ -d /root/gvm/gos ] && source /root/gvm/scripts/gvm 2>/dev/null
[ -d /root/.pyenv/shims ] && export PATH="/root/.pyenv/shims:/root/.pyenv/bin:$PATH"
[ -d /root/go/bin ] && export PATH="/root/go/bin:$PATH" && export GOROOT=/root/go
[ -d /root/.local/bin ] && export PATH="/root/.local/bin:$PATH"
RUST_DIR=$(ls -d /root/rust-*/bin 2>/dev/null | head -1)
[ -n "$RUST_DIR" ] && export PATH="$RUST_DIR:$PATH"
PROFILE
/usr/sbin/sshd
echo "[entrypoint] SSH 已启动"
ttyd -W -c jc:1234567 -t fontSize=16 -t theme='{"background":"#1a1a2e"}' /opt/ttyd-session.sh &
TTYD_PID=$!
sleep 1
if ! kill -0 "$TTYD_PID" 2>/dev/null; then
echo "[entrypoint] 错误: ttyd 启动失败"
exit 1
fi
echo "[entrypoint] ttyd 已启动 (PID: $TTYD_PID)"
if ! pidof sshd > /dev/null; then
echo "[entrypoint] 错误: sshd 启动失败"
exit 1
fi
echo ""
echo "========================================"
echo " WorkPod Test 开发环境已就绪"
echo "========================================"
echo "Node: $(node --version)"
echo "npm: $(npm --version)"
echo ""
echo "AI 工具:"
echo " Claude Code: $(claude --version 2>/dev/null || echo '未安装')"
echo ""
echo "开发工具:"
echo " Rust: $(rustc --version 2>/dev/null || echo '未安装')"
echo " Go: $(go version 2>/dev/null || echo '未安装')"
echo " Python: $(python3 --version 2>/dev/null || echo '未安装')"
echo ""
echo "连接方式:"
echo " Web: http://localhost:7682"
echo " SSH: ssh root@localhost -p 2223 (密码: workpod123)"
echo "========================================"
exec sleep infinity
+91 -13
View File
@@ -1,30 +1,108 @@
#!/bin/bash #!/bin/bash
set -e set -e
# 启动 SSH 服务 SSHD_PID=0
service ssh start TTYD_PID=0
cleanup() {
echo "[entrypoint] 收到终止信号,正在关闭服务..."
[ "$TTYD_PID" -ne 0 ] && kill "$TTYD_PID" 2>/dev/null
[ "$SSHD_PID" -ne 0 ] && kill "$SSHD_PID" 2>/dev/null
wait 2>/dev/null
echo "[entrypoint] 服务已关闭"
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
# 自动检测 /root 下的开发工具并设置 PATH
# Java (通过环境变量或挂载点)
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME/bin" ] && export PATH="${JAVA_HOME}/bin:$PATH"
[ -n "$MAVEN_HOME" ] && [ -d "$MAVEN_HOME/bin" ] && export PATH="${MAVEN_HOME}/bin:$PATH"
[ -d /root/.cargo/bin ] && export PATH="/root/.cargo/bin:$PATH"
[ -d /root/.rustup ] && export RUSTUP_HOME=/root/.rustup CARGO_HOME=/root/.cargo
[ -d /root/gvm/gos ] && export PATH="/root/gvm/gos/current/bin:$PATH"
[ -d /root/.pyenv/shims ] && export PATH="/root/.pyenv/shims:$PATH"
[ -d /root/.pyenv/bin ] && export PATH="/root/.pyenv/bin:$PATH"
[ -d /root/go/bin ] && export PATH="/root/go/bin:$PATH" && export GOROOT=/root/go
[ -d /root/.local/bin ] && export PATH="/root/.local/bin:$PATH"
# 检测 Rust 独立安装(非 rustup
RUST_DIR=$(ls -d /root/rust-*/bin 2>/dev/null | head -1)
[ -n "$RUST_DIR" ] && export PATH="$RUST_DIR:$PATH"
# ===== Claude Code 自动认证配置 =====
# 当 ANTHROPIC_API_KEY 环境变量存在时,自动写入凭证文件
# 解决新容器首次启动 claude 走 OAuth 流程的问题
_init_claude_auth() {
local auth_key="$ANTHROPIC_API_KEY"
local api_url="$ANTHROPIC_BASE_URL"
if [ -n "$auth_key" ] || [ -n "$api_url" ]; then
mkdir -p ~/.claude
fi
if [ -n "$auth_key" ]; then
# 用 printf 安全写入,避免 JSON 特殊字符注入
printf '{"authToken":"%s"}' "$auth_key" > ~/.claude/.credentials.json
chmod 600 ~/.claude/.credentials.json
fi
if [ -n "$api_url" ]; then
printf '{"apiBaseUrl":"%s"}' "$api_url" > ~/.claude/settings.json
chmod 600 ~/.claude/settings.json
fi
}
_init_claude_auth
# 写入 /etc/profile.d/ 让所有 shell 会话生效
cat > /etc/profile.d/dev-tools.sh << 'PROFILE'
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME/bin" ] && export PATH="$JAVA_HOME/bin:$PATH"
[ -n "$MAVEN_HOME" ] && [ -d "$MAVEN_HOME/bin" ] && export PATH="$MAVEN_HOME/bin:$PATH"
[ -d /root/.cargo/bin ] && export PATH="/root/.cargo/bin:$PATH" && export RUSTUP_HOME=/root/.rustup CARGO_HOME=/root/.cargo
[ -d /root/gvm/gos ] && source /root/gvm/scripts/gvm 2>/dev/null
[ -d /root/.pyenv/shims ] && export PATH="/root/.pyenv/shims:/root/.pyenv/bin:$PATH"
[ -d /root/go/bin ] && export PATH="/root/go/bin:$PATH" && export GOROOT=/root/go
[ -d /root/.local/bin ] && export PATH="/root/.local/bin:$PATH"
RUST_DIR=$(ls -d /root/rust-*/bin 2>/dev/null | head -1)
[ -n "$RUST_DIR" ] && export PATH="$RUST_DIR:$PATH"
PROFILE
/usr/sbin/sshd
echo "[entrypoint] SSH 已启动"
ttyd -W -c "${TTYD_CREDENTIALS:-jc:1234567}" -t fontSize=16 -t theme='{"background":"#1a1a2e"}' /opt/ttyd-session.sh &
TTYD_PID=$!
sleep 1
if ! kill -0 "$TTYD_PID" 2>/dev/null; then
echo "[entrypoint] 错误: ttyd 启动失败"
exit 1
fi
echo "[entrypoint] ttyd 已启动 (PID: $TTYD_PID)"
if ! pidof sshd > /dev/null; then
echo "[entrypoint] 错误: sshd 启动失败"
exit 1
fi
echo "" echo ""
echo "========================================" echo "========================================"
echo " WorkPod 开发环境已就绪" echo " WorkPod Alpine 开发环境已就绪"
echo "========================================" echo "========================================"
echo "Python: $(python --version 2>&1)"
echo "Node: $(node --version)" echo "Node: $(node --version)"
echo "Go: $(go version | awk '{print $3}')" echo "npm: $(npm --version)"
echo "Rust: $(rustc --version 2>/dev/null || echo '未安装')"
echo "" echo ""
echo "AI 工具:" echo "AI 工具:"
echo " Claude Code: $(claude --version 2>/dev/null || echo '未安装')" echo " Claude Code: $(claude --version 2>/dev/null || echo '未安装')"
echo " OpenClaw: $(openclaw --version 2>/dev/null || echo '未安装')"
echo "" echo ""
echo "数据库服务:" echo "开发工具:"
echo " MySQL: mysqld --user=mysql --datadir=/var/lib/mysql &" echo " Rust: $(rustc --version 2>/dev/null || echo '未安装')"
echo " Redis: redis-server --daemonize yes" echo " Go: $(go version 2>/dev/null || echo '未安装')"
echo " Python: $(python3 --version 2>/dev/null || echo '未安装')"
echo "" echo ""
echo "连接方式:" echo "连接方式:"
echo " SSH: ssh root@localhost -p 2222 (密码: workpod123)" echo " Web: http://localhost:7681"
echo " Exec: docker exec -it workpod bash" echo " SSH: ssh root@localhost -p 2222"
echo "========================================" echo "========================================"
# 保持容器运行
exec sleep infinity exec sleep infinity
+66
View File
@@ -0,0 +1,66 @@
# WorkPod 实例模板 — 复制此文件到 instances/{name}/ 后修改
# 修改项(搜索 { } 占位符):
# 1. {name} → 实例名(如 suke, case, hszd
# 2. {SSH_PORT} → SSH 端口(按端口分配规则)
# 3. {WEB_PORT} → Web 端口
# 4. {PROJECT_PATH}→ 项目路径(如 E:/wk-suke
# 5. 取消注释需要的 extra_volumes 和 extra_env
services:
ws-{name}-dev:
build:
context: ../../
dockerfile: instances/{name}/Dockerfile
image: ws-{name}-dev:latest
container_name: ws-{name}-dev
hostname: ws-{name}-dev
restart: unless-stopped
ports:
- "{SSH_PORT}:22"
- "{WEB_PORT}:7681"
volumes:
- {PROJECT_PATH}:/workspace
# Java 项目取消注释:
# - D:/Java/jdk-musl-17:/opt/jdk-musl-17:ro
# - D:/Java/maven-mvnd-3.9.14:/opt/maven-mvnd-3.9.14:ro
# 持久化 home 目录(手动安装的工具不会因重建丢失):
- ../../data/home/{name}:/root
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
- ROOT_PASSWORD=${ROOT_PASSWORD:-workpod123}
- TTYD_CREDENTIALS=${TTYD_CREDENTIALS:-jc:1234567}
# - WORKPOD_PROJECT={name}
# - JAVA_HOME=/opt/jdk-musl-17
# - MAVEN_HOME=/opt/maven-mvnd-3.9.14
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 1G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
working_dir: /workspace
tty: true
stdin_open: true
networks:
default:
name: workpod-{name}-network
+88
View File
@@ -0,0 +1,88 @@
# WorkPod 实例管理指南
## 快速概览
| 实例 | SSH | Web | 用途 | 状态 |
|------|-----|-----|------|------|
| workpod-alpine | 2222 | 7681 | 基础开发环境 | `registry.yaml` |
| ws-flux-dev | 2201 | 7701 | Flux 项目 | `registry.yaml` |
| workpod-test | 2223 | 7682 | 测试实例 | `registry.yaml` |
详细注册表见 **[registry.yaml](./registry.yaml)**
---
## 端口分配规则
### 本机
| 类型 | SSH 范围 | Web 范围 | 说明 |
|------|---------|---------|------|
| 基础/通用 | 2201-2210 | 7681-7690 | base, test 等通用实例 |
| **项目开发** | **2211-2290** | **7701-7790** | 各业务项目专用 |
### 已分配
| 端口 | 实例 | 备注 |
|------|------|------|
| 2222 / 7681 | workpod-alpine | 基础实例 |
| 2223 / 7682 | workpod-test | 测试实例 |
| **2201 / 7701** | **ws-flux-dev** | **Flux 项目** |
| 2211 / 7711 | (预留) | suke |
| 2212 / 7712 | (预留) | case |
| 2213 / 7713 | (预留) | hszd |
### 测试服 (flux_dev)
复用本机规则 + 偏移,详见 registry.yaml → `remote` 段。
---
## 新建项目实例(3 步)
```bash
# 1. 从模板创建
mkdir -p instances/{name}
cp instances/.template/docker-compose.yml instances/{name}/
# 2. 编辑 compose 文件,修改以下占位符:
# {name} → 实例名
# {SSH_PORT} → 按 2211+ 分配
# {WEB_PORT} → 按 7711+ 分配
# {PROJECT_PATH} → E:/wk-{name}
# 3. 注册到 registry.yaml 并启动
cd instances/{name} && docker compose up -d --build
```
如需自定义镜像(如加 JDK),在 `instances/{name}/` 下创建 Dockerfile
```dockerfile
FROM workpod-alpine:latest
# 额外的 LABEL、VOLUME 声明等
ENTRYPOINT ["/entrypoint.sh"]
```
---
## 常用命令
```bash
# 查看所有实例状态
docker ps --filter name=workpod --format "table {{.Names}}\t{{.Status}}\t{{.Ports}"
# 启动指定实例
cd instances/{name} && docker compose up -d
# 停止指定实例
cd instances/{name} && docker compose down
# 重建(修改 Dockerfile/compose 后)
cd instances/{name} && docker compose up -d --build
# 进入容器
docker exec -it {container_name} bash
# 查看日志
docker logs -f {container_name}
```
+53
View File
@@ -0,0 +1,53 @@
services:
workpod-alpine:
build:
context: ../../
dockerfile: Dockerfile
image: workpod-alpine:latest
container_name: workpod-alpine
hostname: workpod-alpine
privileged: true
ports:
- "2222:22"
- "7681:7681"
volumes:
- ../../data/workspace:/workspace
environment:
- TZ=${TZ:-Asia/Shanghai}
- TERM=xterm-256color
- ROOT_PASSWORD=${ROOT_PASSWORD:-workpod123}
- TTYD_CREDENTIALS=${TTYD_CREDENTIALS:-jc:1234567}
tty: true
stdin_open: true
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 1G
healthcheck:
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
restart: unless-stopped
working_dir: /workspace
networks:
default:
name: workpod-alpine-network
+10
View File
@@ -0,0 +1,10 @@
# ws-flux-dev - Flux 金融线索管理 (Java 17 + Node 24)
FROM workpod-alpine:latest
LABEL org.opencontainers.image.title="ws-flux-dev" \
org.opencontainers.image.description="Flux 全栈开发环境 (Java 17 + Node 24)" \
org.opencontainers.image.version="1.0.0"
EXPOSE 22 7681
ENTRYPOINT ["/entrypoint.sh"]
+88
View File
@@ -0,0 +1,88 @@
services:
ws-flux-dev:
build:
context: ../../
dockerfile: instances/flux/Dockerfile
image: ws-flux-dev:latest
container_name: ws-flux-dev
hostname: ws-flux-dev
restart: unless-stopped
ports:
- "2201:22"
- "7701:7681"
volumes:
# ===== 项目源码 =====
- E:/wk-flux:/workspace
# ===== 开发工具 (JDK/Maven) =====
- D:/Java/jdk-musl-17:/opt/jdk-musl-17:ro
- D:/Java/maven-mvnd-3.9.14:/opt/maven-mvnd-3.9.14:ro
# ===== 代理工具 (Linux static binaries) =====
- E:/wk-oth/rust-work/dist-Linux/mysql-proxy:/usr/local/bin/mysql-proxy:ro
- E:/wk-oth/rust-work/dist-Linux/ssh-proxy:/usr/local/bin/ssh-proxy:ro
- E:/wk-oth/rust-work/dist-Linux/redis-proxy:/usr/local/bin/redis-proxy:ro
- E:/wk-oth/rust-work/dist-Linux/mongo-proxy:/usr/local/bin/mongo-proxy:ro
# ===== SSH 密钥 + 代理配置 (仅 Flux 相关) =====
- C:/Users/23780/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro
- ../../dev-envs/flux/ssh-proxy.toml:/root/.dev-env/ssh-proxy.toml:ro
- ../../dev-envs/flux/mysql-proxy.toml:/root/.dev-env/mysql-proxy.toml:ro
- ../../dev-envs/flux/redis-proxy.toml:/root/.dev-env/redis-proxy.toml:ro
# ===== 项目级配置 (dev-envs) =====
- ../../dev-envs/flux/servers.yaml:/root/.dev-env/servers.yaml:ro
- ../../dev-envs/flux/databases.yaml:/root/.dev-env/databases.yaml:ro
- ../../dev-envs/flux/norms.md:/root/.dev-env/norms.md:ro
- ../../dev-envs/flux/docs:/root/.dev-env/docs:ro
- ../../dev-envs/flux/claude/CLAUDE.md:/root/.dev-env/claude/CLAUDE.md:ro
- ../../dev-envs/flux/claude/settings.local.json:/root/.dev-env/claude/settings.local.json:ro
# ===== 持久化 Home =====
- ../../data/home/flux-dev:/root
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
- ROOT_PASSWORD=${ROOT_PASSWORD:-workpod123}
- TTYD_CREDENTIALS=${TTYD_CREDENTIALS:-jc:1234567}
- WORKPOD_PROJECT=flux
- JAVA_HOME=/opt/jdk-musl-17
- MAVEN_HOME=/opt/maven-mvnd-3.9.14
# === Claude Code (智谱 AI) ===
- ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
- ANTHROPIC_API_KEY=${ANTHROPIC_AUTH_TOKEN}
- ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5v-turbo
- ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.1
- ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 1G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
working_dir: /workspace
tty: true
stdin_open: true
networks:
default:
external: true
name: workpod-flux-network
+10
View File
@@ -0,0 +1,10 @@
# ws-lab-x-dev - Lab 通用开发环境
FROM workpod-alpine:latest
LABEL org.opencontainers.image.title="ws-lab-x-dev" \
org.opencontainers.image.description="Lab 通用开发环境" \
org.opencontainers.image.version="1.0.0"
EXPOSE 22 7681
ENTRYPOINT ["/entrypoint.sh"]
+61
View File
@@ -0,0 +1,61 @@
services:
ws-lab-x-dev:
build:
context: ../../
dockerfile: instances/lab-x/Dockerfile
image: ws-lab-x-dev:latest
container_name: ws-lab-x-dev
hostname: ws-lab-x-dev
restart: unless-stopped
ports:
- "2202:22"
- "7702:7681"
volumes:
# ===== 项目源码 =====
- E:/wk-lab:/workspace
# ===== 持久化 Home =====
- ../../data/home/lab-x:/root
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
- ROOT_PASSWORD=${ROOT_PASSWORD:-workpod123}
- TTYD_CREDENTIALS=${TTYD_CREDENTIALS:-jc:1234567}
- WORKPOD_PROJECT=lab-x
# === Claude Code (智谱 AI) ===
- ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
- ANTHROPIC_API_KEY=${ANTHROPIC_AUTH_TOKEN}
- ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5v-turbo
- ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.1
- ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 1G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
working_dir: /workspace
tty: true
stdin_open: true
networks:
default:
name: workpod-lab-x-network
+46
View File
@@ -0,0 +1,46 @@
services:
workpod-test:
image: ae5a15cc0c94
container_name: workpod-test
hostname: workpod-test
restart: unless-stopped
ports:
- "2223:22"
- "7682:7681"
volumes:
- ../../data/workspace-test:/workspace
- ../../data/home/test:/root
environment:
- TZ=Asia/Shanghai
- TERM=xterm-256color
deploy:
resources:
limits:
memory: 4G
reservations:
memory: 1G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "netstat -tlnp 2>/dev/null | grep -qE ':(22|7681)\\b' || exit 1"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
working_dir: /workspace
tty: true
stdin_open: true
networks:
default:
name: workpod-test-network
-51
View File
@@ -1,51 +0,0 @@
# Docker Desktop 数据迁移到 D 盘脚本
# 以管理员身份运行 PowerShell
Write-Host "=== Docker Desktop 数据迁移脚本 ===" -ForegroundColor Cyan
Write-Host ""
# 1. 停止 Docker Desktop
Write-Host "[1/6] 停止 Docker Desktop..." -ForegroundColor Yellow
Stop-Process -Name "Docker Desktop" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
# 2. 停止 WSL
Write-Host "[2/6] 停止 WSL..." -ForegroundColor Yellow
wsl --shutdown
Start-Sleep -Seconds 3
# 3. 创建目标目录
Write-Host "[3/6] 创建目标目录..." -ForegroundColor Yellow
$targetDir = "D:\DockerData"
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
# 4. 导出 docker-desktop-data
Write-Host "[4/6] 导出 docker-desktop-data (这可能需要几分钟)..." -ForegroundColor Yellow
$exportPath = "$targetDir\docker-desktop-data.tar"
wsl --export docker-desktop-data $exportPath
if (Test-Path $exportPath) {
Write-Host " 导出成功: $exportPath" -ForegroundColor Green
# 5. 注销原有的
Write-Host "[5/6] 注销原有发行版..." -ForegroundColor Yellow
wsl --unregister docker-desktop-data
# 6. 导入到新位置
Write-Host "[6/6] 导入到新位置..." -ForegroundColor Yellow
wsl --import docker-desktop-data "$targetDir\wsl" $exportPath --version 2
# 删除临时文件
Remove-Item $exportPath -Force
Write-Host ""
Write-Host "=== 迁移完成 ===" -ForegroundColor Green
Write-Host "Docker 数据已迁移到: $targetDir" -ForegroundColor Cyan
} else {
Write-Host " 导出失败,请检查 docker-desktop-data 是否存在" -ForegroundColor Red
Write-Host " 运行 'wsl --list -v' 查看所有发行版" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "请手动启动 Docker Desktop 完成迁移" -ForegroundColor Yellow
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# 在 nginx.conf http 块开头添加 map 定义
sed -i '/^http {/a\\n map $http_upgrade $connection_upgrade {\n default upgrade;\n '\'''\'' close;\n }' /etc/nginx/nginx.conf
nginx -t
+191
View File
@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>WorkPod</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container { width: 90%; max-width: 400px; padding: 20px; }
.logo {
text-align: center;
margin-bottom: 32px;
}
.logo h1 {
font-size: 28px;
color: #fff;
margin-bottom: 8px;
}
.logo p { color: #888; font-size: 14px; }
.card {
background: #16213e;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.input-group { margin-bottom: 16px; }
.input-group label {
display: block;
font-size: 14px;
color: #aaa;
margin-bottom: 6px;
}
.input-group input {
width: 100%;
padding: 12px 16px;
border: 1px solid #333;
border-radius: 8px;
background: #0f3460;
color: #fff;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
.input-group input:focus { border-color: #e94560; }
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary {
background: #e94560;
color: #fff;
}
.btn-primary:hover { opacity: 0.85; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.error {
color: #e94560;
font-size: 14px;
text-align: center;
margin-top: 12px;
display: none;
}
.hidden { display: none !important; }
.workspaces {
margin-top: 20px;
}
.workspaces h3 {
font-size: 16px;
color: #aaa;
margin-bottom: 12px;
}
.workspace-item {
display: block;
width: 100%;
padding: 16px;
margin-bottom: 8px;
background: #0f3460;
border: 1px solid #333;
border-radius: 8px;
color: #fff;
text-decoration: none;
font-size: 15px;
text-align: left;
cursor: pointer;
transition: border-color 0.2s;
}
.workspace-item:hover { border-color: #e94560; }
.workspace-item .name { font-weight: 600; }
.workspace-item .desc { font-size: 12px; color: #888; margin-top: 4px; }
</style>
</head>
<body>
<!-- 登录页 -->
<div class="container" id="loginView">
<div class="logo">
<h1>WorkPod</h1>
<p>Web Terminal Access</p>
</div>
<div class="card">
<div class="input-group">
<label>访问密码</label>
<input type="password" id="password" placeholder="请输入密码" autocomplete="off">
</div>
<button class="btn btn-primary" id="loginBtn" onclick="login()">登录</button>
<div class="error" id="loginError">密码错误</div>
</div>
</div>
<!-- 工作空间选择页 -->
<div class="container hidden" id="workspaceView">
<div class="logo">
<h1>WorkPod</h1>
<p>选择工作空间</p>
</div>
<div class="card">
<div class="workspaces">
<h3>可用终端</h3>
<a class="workspace-item" onclick="openTerminal('')">
<div class="name">默认工作区</div>
<div class="desc">/workspace</div>
</a>
<a class="workspace-item" onclick="openTerminal('hszd')">
<div class="name">华商智地</div>
<div class="desc">/workspace/wk-hszd</div>
</a>
</div>
</div>
</div>
<script>
var token = localStorage.getItem('wk_token') || '';
// 已有 token 则直接显示工作空间
if (token) {
showWorkspaces();
}
// 回车登录
document.getElementById('password').addEventListener('keydown', function(e) {
if (e.key === 'Enter') login();
});
function login() {
var password = document.getElementById('password').value;
if (!password) return;
var btn = document.getElementById('loginBtn');
btn.disabled = true;
btn.textContent = '验证中...';
fetch('/auth/check?t=' + Date.now(), {
headers: { 'Authorization': 'Basic ' + btoa('wk:' + password) }
}).then(function(r) {
if (r.ok) return r.text();
throw new Error();
}).then(function(t) {
token = t.trim();
localStorage.setItem('wk_token', token);
showWorkspaces();
}).catch(function() {
document.getElementById('loginError').style.display = 'block';
btn.disabled = false;
btn.textContent = '登录';
});
}
function showWorkspaces() {
document.getElementById('loginView').classList.add('hidden');
document.getElementById('workspaceView').classList.remove('hidden');
}
function openTerminal(path) {
var url = path ? '/' + path + '/?token=' + token : '/?token=' + token;
window.location.href = url;
}
</script>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# ttyd 会话入口:根据 URL 参数或用户选择分配 tmux session
SESSION_NAME=""
# ttyd 会将 query string 存入 TTYD_QUERY_STRING 环境变量
if [ -n "$TTYD_QUERY_STRING" ]; then
# 解析 session 参数
SESSION_NAME=$(echo "$TTYD_QUERY_STRING" | tr '&' '\n' | grep '^session=' | head -1 | cut -d= -f2)
fi
if [ -z "$SESSION_NAME" ]; then
# 没有指定 session,显示选择菜单
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ 选择或创建 tmux session ║"
echo "╠══════════════════════════════════════╣"
# 列出已有 session
EXISTING=$(tmux list-sessions 2>/dev/null | awk '{print $1}')
if [ -n "$EXISTING" ]; then
echo "║ 已有 session: ║"
I=1
for s in $EXISTING; do
printf "║ [%d] %-33s║\n" "$I" "$s"
I=$((I + 1))
done
else
echo "║ (无已有 session) ║"
fi
echo "╠══════════════════════════════════════╣"
echo "║ 输入编号选择,或输入新名称创建 ║"
echo "╚══════════════════════════════════════╝"
echo ""
read -p "session: " CHOICE
if [ -z "$CHOICE" ]; then
CHOICE="default"
fi
# 判断是数字编号还是名称
if echo "$CHOICE" | grep -qE '^[0-9]+$' && [ -n "$EXISTING" ]; then
SESSION_NAME=$(echo "$EXISTING" | sed -n "${CHOICE}p")
else
SESSION_NAME="$CHOICE"
fi
# 清屏
clear
fi
# session 名只保留字母数字下划线连字符
SESSION_NAME=$(echo "$SESSION_NAME" | tr -cd 'a-zA-Z0-9_\-')
# 如果 session 已存在,共享 attach;否则新建
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
exec tmux attach-session -t "$SESSION_NAME"
else
exec tmux new-session -s "$SESSION_NAME"
fi
+28
View File
@@ -0,0 +1,28 @@
# WorkPod Web 终端 - wk.1216.top
server {
listen 80;
server_name wk.1216.top;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name wk.1216.top;
ssl_certificate /etc/nginx/sslkey/_.1216.top.pem;
ssl_certificate_key /etc/nginx/sslkey/_.1216.top.key;
location / {
proxy_pass http://127.0.0.1:7683;
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 Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
proxy_buffering off;
}
}