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:
@@ -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
|
||||
@@ -71,6 +71,9 @@ web/data/*.db
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
# Docker local secrets
|
||||
docker/.env
|
||||
|
||||
# Deploy
|
||||
deploy/*.key
|
||||
|
||||
|
||||
+29
@@ -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"]
|
||||
@@ -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:
|
||||
@@ -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`。
|
||||
@@ -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 "$@"
|
||||
@@ -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()
|
||||
@@ -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 验证
|
||||
@@ -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
|
||||
```
|
||||
@@ -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` 含启动说明
|
||||
Reference in New Issue
Block a user