feat(docker): add Compose stack for MySQL, Redis and Nexus API

- Dockerfile with healthcheck and entrypoint for deps + .env persistence
- generate_env.py for local secrets; design docs and changelog

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-03 00:58:46 +08:00
parent 51948469c2
commit 3cec226b89
11 changed files with 472 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
.git
.gitignore
.claude
.cursor
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
.coverage
htmlcov/
frontend/node_modules
frontend/dist
node_modules
.env
docker/.env
docker/runtime
tests/
docs/
standards/
*.md
!docker/README.md
web/uploads
backups/
deploy/gate_log.jsonl
+3
View File
@@ -71,6 +71,9 @@ web/data/*.db
# Backups
backups/
# Docker local secrets
docker/.env
# Deploy
deploy/*.key
+29
View File
@@ -0,0 +1,29 @@
# Nexus 6.0 — API + 静态 SPA (web/app)
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/app/ ./web/app/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
+87
View File
@@ -0,0 +1,87 @@
# Nexus 6.0 — Docker Compose (dev / staging)
# Usage:
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
# docker compose up -d --build
# open http://localhost:8600/app/
name: nexus
services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-nexus_root_dev}
MYSQL_DATABASE: nexus
MYSQL_USER: nexus
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-nexus_dev_pass}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
interval: 10s
timeout: 5s
retries: 8
start_period: 40s
ports:
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 8
nexus:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
environment:
MYSQL_HOST: mysql
MYSQL_PORT: "3306"
REDIS_HOST: redis
REDIS_PORT: "6379"
NEXUS_PERSIST_DIR: /var/lib/nexus
NEXUS_DOCKER_WRITE_ENV: "1"
NEXUS_HOST: "0.0.0.0"
NEXUS_PORT: "8600"
NEXUS_DEPLOY_PATH: /app
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
NEXUS_REDIS_URL: redis://redis:6379/0
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
env_file:
- path: docker/.env
required: false
volumes:
- nexus-state:/var/lib/nexus
- nexus-web-data:/app/web/data
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 90s
volumes:
mysql-data:
redis-data:
nexus-state:
nexus-web-data:
+56
View File
@@ -0,0 +1,56 @@
# Nexus Docker
## 前置
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)Windows/macOS)或 Docker Engine + Compose v2
## 快速启动(正常模式)
```bash
# 1. 生成密钥(写入 docker/.env,已 gitignore
python docker/generate_env.py
# 2. 构建并启动
docker compose up -d --build
# 3. 健康检查
curl -s http://localhost:8600/health # 期望: ok
# 4. 打开 SPA
# http://localhost:8600/app/
```
默认管理员需在安装向导或已有 DB 中配置;若 DB 为空且未跑过 install,可删卷后走安装模式。
## 安装向导模式
不创建 `docker/.env`(或清空 `NEXUS_SECRET_KEY`)时,应用以 **安装模式** 启动:
- http://localhost:8600/app/install.html
完成向导后 `.env` 会写入容器;`nexus-state` 卷会在重启时恢复配置。
## 常用命令
```bash
docker compose logs -f nexus
docker compose down # 停止,保留数据卷
docker compose down -v # 停止并删除数据卷(清空 DB
docker compose build --no-cache nexus
```
## 服务
| 服务 | 端口 | 说明 |
|------|------|------|
| nexus | 8600 | FastAPI + 静态 `/app/` |
| mysql | 3306 | 数据库 `nexus` |
| redis | 6379 | 仅容器内网(未映射到主机) |
## 生产注意
- 修改 `MYSQL_*``NEXUS_*` 为强密码,勿使用 `generate_env.py` 默认值上生产。
- 建议前置 Nginx/TLS,仅暴露 443。
- 多实例部署需共享 MySQL、Redis;后台任务由 Redis primary lock 保证单实例执行。
详见 `docs/design/specs/2026-06-03-docker-design.md`
+72
View File
@@ -0,0 +1,72 @@
#!/bin/sh
# Nexus Docker entrypoint — wait for deps, sync .env with persistent volume, exec uvicorn.
set -eu
PERSIST_DIR="${NEXUS_PERSIST_DIR:-/var/lib/nexus}"
PERSIST_ENV="${PERSIST_DIR}/.env"
wait_tcp() {
host="$1"
port="$2"
echo "entrypoint: waiting for ${host}:${port}..."
until python3 -c "
import socket
s = socket.socket()
s.settimeout(2)
s.connect(('${host}', ${port}))
s.close()
" 2>/dev/null; do
sleep 2
done
}
restore_persisted_env() {
if [ -f "${PERSIST_ENV}" ]; then
cp "${PERSIST_ENV}" /app/.env
chmod 600 /app/.env
echo "entrypoint: restored .env from ${PERSIST_ENV}"
fi
}
write_env_from_environment() {
if [ -f /app/.env ]; then
return 0
fi
if [ "${NEXUS_DOCKER_WRITE_ENV:-}" != "1" ]; then
return 0
fi
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
return 0
fi
mkdir -p "${PERSIST_DIR}"
{
echo "# Generated by docker/entrypoint.sh — do not commit"
env | grep '^NEXUS_' | sort
} > /app/.env
chmod 600 /app/.env
cp /app/.env "${PERSIST_ENV}"
echo "entrypoint: wrote .env from container environment"
}
persist_env_on_exit() {
if [ -f /app/.env ]; then
mkdir -p "${PERSIST_DIR}"
cp /app/.env "${PERSIST_ENV}"
chmod 600 "${PERSIST_ENV}" 2>/dev/null || true
fi
}
trap persist_env_on_exit EXIT INT TERM
MYSQL_HOST="${MYSQL_HOST:-mysql}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
REDIS_HOST="${REDIS_HOST:-redis}"
REDIS_PORT="${REDIS_PORT:-6379}"
wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}"
wait_tcp "${REDIS_HOST}" "${REDIS_PORT}"
restore_persisted_env
write_env_from_environment
exec "$@"
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Generate docker/.env with random secrets for local Compose."""
from __future__ import annotations
import base64
import secrets
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "docker" / ".env"
EXAMPLE = ROOT / "docker" / ".env.example"
def _token(n: int = 32) -> str:
return secrets.token_urlsafe(n)
def main() -> None:
if OUT.exists():
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
mysql_root = _token(24)
mysql_pass = _token(24)
secret = _token(32)
api_key = _token(24)
# Fernet-compatible (same as install wizard)
enc_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
text = EXAMPLE.read_text(encoding="utf-8")
lines: list[str] = []
for line in text.splitlines():
if line.startswith("MYSQL_ROOT_PASSWORD="):
lines.append(f"MYSQL_ROOT_PASSWORD={mysql_root}")
elif line.startswith("MYSQL_PASSWORD="):
lines.append(f"MYSQL_PASSWORD={mysql_pass}")
elif line.startswith("NEXUS_SECRET_KEY="):
lines.append(f"NEXUS_SECRET_KEY={secret}")
elif line.startswith("NEXUS_API_KEY="):
lines.append(f"NEXUS_API_KEY={api_key}")
elif line.startswith("NEXUS_ENCRYPTION_KEY="):
lines.append(f"NEXUS_ENCRYPTION_KEY={enc_key}")
else:
lines.append(line)
OUT.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
print(f"Wrote {OUT}")
print("Next: docker compose up -d --build")
if __name__ == "__main__":
main()
+29
View File
@@ -0,0 +1,29 @@
# Docker 引入审计
| 项 | 内容 |
|----|------|
| 日期 | 2026-06-03 |
| 范围 | Dockerfile, docker-compose, entrypoint, generate_env |
## Step 3 / Closure
| H | 判定 | 依据 |
|---|------|------|
| H-密钥 | PASS | `docker/.env` gitignore;示例无真实密钥 |
| H-网络 | PASS | Redis 未映射主机;MySQL 可配置端口 |
| H-持久化 | PASS | named volumes`.env` chmod 600 |
| H-安装模式 | PASS | 无密钥时不写 `.env`,保留 install.html 流程 |
## 改动文件清单
- `Dockerfile`, `docker-compose.yml`, `.dockerignore`
- `docker/entrypoint.sh`, `docker/generate_env.py`, `docker/.env.example`, `docker/README.md`
## DoD
- [x] 设计 spec + plan + changelog
- [ ] 本机 `docker compose up`(需安装 Docker Desktop
## 结论
☑ 0 FINDING,待用户本机 Docker 验证
+27
View File
@@ -0,0 +1,27 @@
# 2026-06-03 Docker 容器化
## 日期
2026-06-03
## 变更摘要
新增 `Dockerfile``docker-compose.yml`MySQL + Redis + Nexus)、entrypoint 与 `docker/generate_env.py`,支持本地一键 `docker compose up`
## 动机
用户需要 Docker 部署/开发环境;统一依赖版本与可复现启动流程,不替代现有 Supervisor 裸机方案。
## 涉及文件
- `Dockerfile``.dockerignore``docker-compose.yml`
- `docker/entrypoint.sh``docker/.env.example``docker/generate_env.py``docker/README.md`
- `docs/design/specs/2026-06-03-docker-design.md`
- `docs/design/plans/2026-06-03-docker.md`
- `.gitignore``docker/.env`
## 迁移 / 重启
无。与裸机并存:Docker 使用独立数据卷,不影响现有 `/www/wwwroot/...` 部署。
## 验证方式
```bash
python docker/generate_env.py
docker compose up -d --build
curl -s http://localhost:8600/health
```
+31
View File
@@ -0,0 +1,31 @@
# Docker 实施计划
## 文件清单
| 文件 | 作用 |
|------|------|
| `Dockerfile` | Python 3.12 镜像,打包 server + web/app |
| `docker-compose.yml` | mysql、redis、nexus |
| `docker/entrypoint.sh` | 等待依赖、`.env` 持久化、启动 uvicorn |
| `docker/.env.example` | 开发环境变量模板 |
| `docker/generate-env.sh` | 生成随机密钥到 `docker/.env` |
| `.dockerignore` | 缩小构建上下文 |
| `docker/README.md` | 使用说明 |
## 步骤
1. 编写设计文档(本计划对应 spec)
2. 实现 Dockerfile / entrypoint / compose
3. 本地 `docker compose build && docker compose up -d`
4. 验证 `/health` 与安装页
5. changelog + 审计摘要
## 回滚
删除容器与卷:`docker compose down -v`。不影响裸机部署。
## 测试要点
- 冷启动:仅 install 模式
- 填入 `docker/.env` 后:正常模式 + 表初始化
- 容器重启:`.env``nexus-state` 恢复
@@ -0,0 +1,57 @@
# Docker 容器化设计
## 背景与目标
Nexus 此前仅支持 Supervisor + 裸机部署。需要提供 **Docker Compose** 一键拉起 MySQL、Redis 与 FastAPI,便于本地开发、CI 与可选生产部署,且不改变 Clean Architecture 与安装向导流程。
## 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| 仅 Dockerfile | 简单 | 仍需自备 MySQL/Redis |
| Compose 全栈 | 一键、可复现 | 需处理 `.env` 与安装模式 |
| K8s | 大规模 | 过重,非当前需求 |
**选定**`Dockerfile` + `docker-compose.yml`mysql + redis + nexus)。
## 架构
```
┌─────────────┐ ┌──────────┐ ┌─────────┐
│ Browser │────▶│ nexus │────▶│ mysql │
│ :8600/app │ │ :8600 │ │ :3306 │
└─────────────┘ └────┬─────┘ └─────────┘
┌─────────┐
│ redis │
│ :6379 │
└─────────┘
```
- 单容器内 `uvicorn` 单 worker(后台任务由 Redis primary lock 协调;多副本可水平扩展)。
- 静态前端:镜像内 `web/app/`(与 Git 跟踪的 Vite 产物一致)。
- **安装模式**:无 `.env` 时访问 `/app/install.html`
- **正常模式**`docker/.env` 或 Compose `environment` + entrypoint 写入 `/app/.env` 并持久化到 `nexus-state` 卷。
## 接口与配置
| 变量 | 说明 |
|------|------|
| `NEXUS_DATABASE_URL` | `mysql+aiomysql://nexus:pass@mysql:3306/nexus` |
| `NEXUS_REDIS_URL` | `redis://redis:6379/0` |
| `NEXUS_SECRET_KEY` / `API_KEY` / `ENCRYPTION_KEY` | 正常模式必填 |
| `NEXUS_DOCKER_WRITE_ENV` | `1` 时由 entrypoint 从环境生成 `.env` |
## 安全与性能
- 默认密码仅用于开发;生产必须改 `docker/.env` 与 Compose secrets。
- MySQL/Redis 数据用 named volume`.env` 同步到 `nexus-state` 卷(`chmod 600`)。
- 健康检查:`GET /health``ok`
## 验收标准
- [ ] `docker compose up -d --build``curl http://localhost:8600/health``ok`
- [ ]`docker/.env` 时可打开 `/app/install.html`
- [ ] 配置 `docker/.env` 后重启仍保持登录态与 DB 数据
- [ ] `docs/changelog``docker/README` 含启动说明