构建:新增完整容器化部署

This commit is contained in:
Zhu Junhao
2026-09-04 15:30:58 +08:00
parent 99d16bb3b9
commit 595c8da595
11 changed files with 335 additions and 31 deletions

22
.env.example Normal file
View File

@@ -0,0 +1,22 @@
# 复制为 .env 后必须替换以下三个敏感值;不要把真实 .env 提交到 Git。
POSTGRES_PASSWORD=replace-with-a-strong-database-password
APP_MASTER_KEY=replace-with-at-least-32-random-characters
APP_ADMIN_PASSWORD=replace-with-a-strong-admin-password
# 百炼知识库未启用时可以保留为空;模型 API Key 仍通过页面配置并加密保存。
DASHSCOPE_API_KEY=
# 非敏感运行参数。
POSTGRES_DB=smart_factory_agent
POSTGRES_USER=smart_factory
APP_ADMIN_USERNAME=admin
APP_RUN_TIMEOUT=60m
SESSION_COOKIE_SECURE=false
MANUAGENT_DATA_ROOT=/srv/manuagent/data
# 默认值即部署要求的 0.0.0.0:5173普通 Linux 部署无需修改。
FRONTEND_PORT=5173
# 可选镜像标签,便于私有镜像仓库或版本升级时覆盖。
AGENT_RUNTIME_IMAGE=smart-factory-agent-runtime:0.1.0
BACKEND_IMAGE=manuagent-backend:0.1.0
FRONTEND_IMAGE=manuagent-frontend:0.1.0

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
# 容器内执行的 Shell 脚本必须使用 LF避免 Windows 检出时生成 CRLF 导致解释器无法识别。
*.sh text eol=lf
# Docker 与 Nginx 配置统一使用 LF便于在 Linux 容器内直接加载。
Dockerfile text eol=lf
*.conf text eol=lf

View File

@@ -11,14 +11,14 @@
## 本地启动
```bash
docker compose --profile build-only build agent-runtime
docker compose -f docker-compose.yml build agent-runtime
docker compose up -d postgres
mvn -q -f server/pom.xml spring-boot:run
npm --prefix client install
npm --prefix client run dev
npm --prefix web-ui install
npm --prefix web-ui run dev
```
打开 <http://127.0.0.1:5173>,本地默认账号为 `admin / admin123`
本地开发服务器默认使用 Vite 配置的端口。容器化部署固定从 <http://127.0.0.1:5173> 访问
模型连接只能在“模型配置”页面新增并持久化到 PostgreSQLAPI Key 会使用 `APP_MASTER_KEY`
环境变量提供的主密钥加密后保存。`dashscope_key.txt` 仅供百炼知识库使用,不参与模型配置。
@@ -29,12 +29,53 @@ npm --prefix client run dev
$env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>'
```
## Docker Compose 部署
部署包含 Nginx 前端、Spring Boot 后端、Agent Runtime 镜像和 PostgreSQL。Agent Runtime
不是常驻 API 服务:`agent-runtime` 服务完成镜像构建后会以状态码 0 退出,后端再通过 Docker
Socket 为每个 Agent Session 动态创建隔离容器。
1. 复制环境变量模板并替换其中的三个必填密码/密钥;生产环境建议使用密码管理系统生成随机值。
```bash
cp .env.example .env
```
2. 确认 `MANUAGENT_DATA_ROOT` 是 Docker 宿主机上的绝对路径。这个路径会以完全相同的路径挂载到
后端容器,供动态 Agent Runtime 继续挂载项目材料、工作目录和产物。Linux 默认值为
`/srv/manuagent/data`。
3. 构建并启动完整服务:
```bash
docker compose -f docker-compose.yml up -d --build
docker compose -f docker-compose.yml ps
```
4. 打开 <http://127.0.0.1:5173>,使用 `.env` 中的 `APP_ADMIN_USERNAME` 和
`APP_ADMIN_PASSWORD` 登录。查看日志或停止服务:
```bash
docker compose -f docker-compose.yml logs -f backend
docker compose -f docker-compose.yml down
```
默认端口映射为 `0.0.0.0:5173:80`。若 Windows 的动态端口排除范围占用了 5173可仅在本机
验证时临时执行 `$env:FRONTEND_PORT = '15173'`Linux 服务器部署应保留默认的 5173。
PostgreSQL 数据保存在 `postgres-data` 命名卷中项目材料、Agent 工作区、快照和生成文件保存在
`MANUAGENT_DATA_ROOT`。`docker compose down` 不会删除它们,只有显式增加 `--volumes` 才会删除
PostgreSQL 卷。后端挂载 Docker Socket 等价于授予其管理宿主机容器的高权限,应只在受信任的
Docker 主机上运行,并限制 5173 端口的网络访问范围。通过 HTTPS 反向代理部署时,请将
`SESSION_COOKIE_SECURE` 设为 `true`。
## 验证
```bash
mvn -q -f server/pom.xml test
npm --prefix client run test -- --run
npm --prefix client run build
npm --prefix web-ui run test
npm --prefix web-ui run build
docker compose -f docker-compose.yml config --quiet
```
产品、数据库与验收设计见 [docs](docs/)。内置 Skill 与资源由 Flyway 种子迁移写入数据库,无需额外导入文件。

View File

@@ -1,25 +0,0 @@
services:
agent-runtime:
image: smart-factory-agent-runtime:0.1.0
build:
context: sandbox
profiles: ["build-only"]
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: smart_factory_agent
POSTGRES_USER: smart_factory
POSTGRES_PASSWORD: smart_factory
ports:
- "127.0.0.1:54330:5432"
volumes:
- smart-factory-pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U smart_factory -d smart_factory_agent"]
interval: 3s
timeout: 3s
retries: 20
volumes:
smart-factory-pg:

131
docker-compose.yml Normal file
View File

@@ -0,0 +1,131 @@
name: manuagent
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-smart_factory_agent}
POSTGRES_USER: ${POSTGRES_USER:-smart_factory}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- manuagent-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
# 此服务负责构建并登记 AgentScope 使用的 Runtime 镜像。
# 它成功退出后,后端会通过 Docker Socket 按 Session 动态创建真正执行任务的 Runtime 容器。
agent-runtime:
image: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0}
build:
context: ./sandbox
dockerfile: Dockerfile
command: ["/bin/true"]
restart: "no"
networks:
- manuagent-network
backend:
image: ${BACKEND_IMAGE:-manuagent-backend:0.1.0}
build:
context: ./server
dockerfile: Dockerfile
restart: unless-stopped
init: true
depends_on:
postgres:
condition: service_healthy
restart: true
agent-runtime:
condition: service_completed_successfully
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-smart_factory_agent}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-smart_factory}
# Spring Boot 将 secrets 目录中的点分文件名作为配置属性加载,避免把密码写进普通环境变量。
SPRING_CONFIG_IMPORT: optional:configtree:/run/secrets/
APP_DATA_ROOT: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
APP_DASHSCOPE_KEY_FILE: /run/secrets/dashscope_key
APP_ADMIN_USERNAME: ${APP_ADMIN_USERNAME:-admin}
APP_SANDBOX_IMAGE: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0}
APP_SANDBOX_NETWORK: manuagent-network
APP_RUN_TIMEOUT: ${APP_RUN_TIMEOUT:-60m}
SERVER_SERVLET_SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false}
secrets:
- source: postgres_password
target: spring.datasource.password
- source: app_master_key
target: app.master-key
- source: admin_password
target: app.admin-password
- source: dashscope_api_key
target: dashscope_key
volumes:
# DockerSandbox 的 bind mount 源路径由宿主机 daemon 解释,因此容器内外必须使用相同绝对路径。
- type: bind
source: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
target: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data}
# AgentScope 需要通过宿主机 Docker Engine 创建、执行并销毁隔离的 Runtime 容器。
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
expose:
- "8080"
networks:
- manuagent-network
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080/api/auth/csrf"]
interval: 10s
timeout: 5s
retries: 18
start_period: 30s
stop_grace_period: 30s
frontend:
image: ${FRONTEND_IMAGE:-manuagent-frontend:0.1.0}
build:
context: ./web-ui
dockerfile: Dockerfile
restart: unless-stopped
init: true
depends_on:
backend:
condition: service_healthy
restart: true
ports:
# 默认严格监听 0.0.0.0:5173仅当宿主机保留该端口时才通过 FRONTEND_PORT 临时覆盖。
- "0.0.0.0:${FRONTEND_PORT:-5173}:80"
networks:
- manuagent-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--output-document=/dev/null", "http://127.0.0.1/"]
interval: 10s
timeout: 5s
retries: 6
start_period: 10s
networks:
manuagent-network:
# 固定网络名,确保后端动态创建的 Agent Runtime 容器可以加入同一个网络。
name: manuagent-network
driver: bridge
volumes:
postgres-data:
secrets:
postgres_password:
environment: POSTGRES_PASSWORD
app_master_key:
environment: APP_MASTER_KEY
admin_password:
environment: APP_ADMIN_PASSWORD
dashscope_api_key:
environment: DASHSCOPE_API_KEY

6
server/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
target
.idea
*.iml
*.log
.env
.env.*

37
server/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# Maven 构建阶段使用与项目一致的 JDK 21并利用 BuildKit 缓存减少重复下载依赖的时间。
FROM maven:3.9.13-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml ./
COPY src ./src
# 直接打包只解析项目真正需要的依赖;独立 go-offline 会额外下载大量未参与构建的报告插件。
RUN --mount=type=cache,target=/root/.m2 mvn -B -DskipTests package
# AgentScope DockerSandbox 通过 docker 命令创建 Runtime直接复用官方镜像中的 CLI 二进制。
FROM docker:29-cli AS docker-cli
FROM eclipse-temurin:21-jre-jammy
# curl 用于容器健康检查gosu 用于完成目录和 Docker Socket 权限初始化后降权运行 Java。
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gosu \
&& rm -rf /var/lib/apt/lists/*
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
RUN groupadd --gid 10001 manuagent \
&& useradd --uid 10001 --gid 10001 --create-home --shell /bin/bash manuagent \
&& mkdir -p /opt/manuagent /srv/manuagent/data \
&& chown -R manuagent:manuagent /opt/manuagent /srv/manuagent
WORKDIR /opt/manuagent
COPY --from=builder /workspace/target/*.jar app.jar
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["java", "-XX:MaxRAMPercentage=75.0", "-Djava.security.egd=file:/dev/urandom", "-jar", "/opt/manuagent/app.jar"]

View File

@@ -0,0 +1,25 @@
#!/bin/sh
set -eu
# Agent Runtime 的目录通过宿主机 Docker daemon 再次挂载,因此后端必须能写入共享数据根目录。
data_root="${APP_DATA_ROOT:-/srv/manuagent/data}"
mkdir -p "$data_root"
chown manuagent:manuagent "$data_root"
# Docker Socket 的组 ID 在不同 Linux 发行版和 Docker Desktop 中并不固定。
# 启动时读取真实组 ID 并把低权限应用用户加入对应组,避免以 root 身份运行 Spring Boot。
docker_socket="/var/run/docker.sock"
if [ ! -S "$docker_socket" ]; then
echo "错误:未挂载 $docker_socket,后端无法创建 Agent Runtime 容器。" >&2
exit 1
fi
docker_gid="$(stat -c '%g' "$docker_socket")"
docker_group="$(getent group "$docker_gid" | cut -d: -f1 || true)"
if [ -z "$docker_group" ]; then
docker_group="docker-host"
groupadd --gid "$docker_gid" "$docker_group"
fi
usermod -aG "$docker_group" manuagent
exec gosu manuagent "$@"

7
web-ui/.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
client
*.tsbuildinfo
npm-debug.log
.env
.env.*

21
web-ui/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1
# 第一阶段只负责安装锁定依赖并生成 Vite 静态资源,避免把 Node.js 和源码带入运行镜像。
FROM node:22-bookworm-slim AS builder
WORKDIR /workspace
# 先复制依赖清单以复用 Docker 构建缓存;只有依赖变化时才重新执行 npm ci。
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二阶段使用精简 Nginx 提供静态页面,并把同源 /api 请求转发给后端服务。
FROM nginx:1.29.8-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /workspace/dist /usr/share/nginx/html
EXPOSE 80

33
web-ui/nginx.conf Normal file
View File

@@ -0,0 +1,33 @@
server {
listen 80;
server_name _;
# 企业材料允许上传到 160 MB与 Spring Boot 的请求上限保持一致。
client_max_body_size 160m;
root /usr/share/nginx/html;
index index.html;
# 前后端保持同源,浏览器中的 Session Cookie、CSRF Token 与流式事件接口无需额外跨域配置。
location /api/ {
proxy_pass http://backend:8080;
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;
# Agent 事件采用长连接流式返回,关闭代理缓冲后事件才能及时到达前端。
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no;
}
# Vue Router 使用 history 模式,未命中的前端路由统一回退到入口页面。
location / {
try_files $uri $uri/ /index.html;
}
}