-
Notifications
You must be signed in to change notification settings - Fork 4
feat(deploy): 容器化后端 + /health 探针(已按评审拆出 CORS 到 #140) #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 构建上下文排除项。 | ||
| # | ||
| # 不加这个文件的话,`docker build ./backend` 会把本地开发产物一起送进 daemon: | ||
| # 实测 backend/.venv 单独就有 161MB,而它在镜像里会被 builder 阶段重新装一遍, | ||
| # 送过去纯属浪费;.git 与缓存目录同理,还会让 layer cache 因无关文件变动而失效。 | ||
|
|
||
| # 本地虚拟环境(镜像内由 uv sync 重建) | ||
| .venv/ | ||
| venv/ | ||
|
|
||
| # 版本库与编辑器 | ||
| .git/ | ||
| .gitignore | ||
| .idea/ | ||
| .vscode/ | ||
|
|
||
| # Python 缓存与构建产物 | ||
| __pycache__/ | ||
| *.py[cod] | ||
| *.egg-info/ | ||
| build/ | ||
| dist/ | ||
|
|
||
| # 各类工具缓存 | ||
| .pytest_cache/ | ||
| .ruff_cache/ | ||
| .import_linter_cache/ | ||
| .mypy_cache/ | ||
| .coverage | ||
| htmlcov/ | ||
|
|
||
| # 环境变量与密钥:绝不进镜像 | ||
| .env | ||
| .env.* | ||
| !.env.example |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # ── 后端 Dockerfile ────────────────────────────────────────────────── | ||
| # 多阶段构建:builder 装依赖 → runtime 只拷贝产物,镜像更小。 | ||
| # | ||
| # 两处不是随手写的,都是在部署服务器上实测踩出来的(2026-08-04): | ||
| # | ||
| # 1. builder 与 runtime **必须同路径**。uv 装出来的 venv 里,可执行脚本的 shebang | ||
| # 与 workspace 包的 .pth 都是**绝对路径**。若 builder 在 /build、runtime 在 /app, | ||
| # 拷过去之后 uvicorn 会报 "no such file or directory" —— 报的不是脚本本身, | ||
| # 而是它 shebang 指向的 /build/.venv/bin/python;同时 workspace 包 import 不到。 | ||
| # | ||
| # 2. 国内网络必须换源并拉长超时。实测宿主机访问 pypi.org 需 8s,构建容器内默认 | ||
| # 超时会在下载大包(uvloop)时 "operation timed out" 直接失败。 | ||
|
|
||
| FROM python:3.12-slim AS builder | ||
|
|
||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| ENV UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \ | ||
| UV_HTTP_TIMEOUT=180 | ||
|
|
||
| # 与 runtime 同路径 —— 见文件头第 1 条 | ||
| WORKDIR /app | ||
|
|
||
| # 先拷依赖定义,利用 layer cache | ||
| COPY pyproject.toml uv.lock ./ | ||
| COPY packages/common/pyproject.toml packages/common/ | ||
| COPY packages/framework/pyproject.toml packages/framework/ | ||
| COPY packages/ai_engine/pyproject.toml packages/ai_engine/ | ||
| COPY packages/app/pyproject.toml packages/app/ | ||
|
|
||
| RUN uv sync --frozen --no-dev --no-install-workspace | ||
|
|
||
| COPY packages/ packages/ | ||
| RUN uv sync --frozen --no-dev | ||
|
minorcell marked this conversation as resolved.
|
||
|
|
||
| # ── 运行时 ── | ||
| FROM python:3.12-slim AS runtime | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY --from=builder /app/.venv /app/.venv | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The runtime stage never creates or switches to a non-root user, so RUN useradd -r -u 1001 appuser && chown -R appuser /app
USER appuser |
||
| COPY --from=builder /app/packages /app/packages | ||
|
|
||
| ENV PATH="/app/.venv/bin:$PATH" | ||
|
|
||
| EXPOSE 8000 | ||
|
|
||
| # 打 /health 而不是 /docs:生产通常关掉交互文档(docs_url=None),那时探针会永远失败。 | ||
| HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ | ||
| CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| CMD ["uvicorn", "windup_app.bootstrap.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Single uvicorn process (default one worker). On a multi-core host this caps throughput and makes the service prone to head-of-line blocking on the sync Qiniu upload / Postgres paths. Consider env-driven concurrency ( |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """部署形态的两颗钉子:镜像装齐运行期依赖 + 探针可达。 | ||
|
|
||
| 两条都属于"容器能起来 ≠ 请求能成功"这一类问题,只在真实部署后才暴露, | ||
| 所以在 CI 里各钉一颗。CORS 相关的断言在 ``test_cors.py``。 | ||
| """ | ||
|
|
||
| import importlib.util | ||
|
|
||
| from fastapi.testclient import TestClient | ||
|
|
||
| from windup_app.bootstrap.app import create_app | ||
|
|
||
|
|
||
| def test_media_upload_dependency_is_declared(): | ||
| """``server/media/service.py`` 在函数体里延迟 import qiniu。 | ||
|
|
||
| 不在 pyproject/uv.lock 里声明的话,镜像照样能构建、能启动、``/docs`` 也正常, | ||
| 直到第一次 ``POST /media/upload`` 才 ``ModuleNotFoundError: qiniu``。 | ||
| """ | ||
| assert importlib.util.find_spec("qiniu") is not None | ||
|
|
||
|
|
||
| def test_health_endpoint_is_reachable(): | ||
| """容器 HEALTHCHECK 打的是 /health,不是 /docs。 | ||
|
|
||
| /docs 在生产会被关掉(``docs_url=None``),那时探针永远失败、容器被反复判死。 | ||
| """ | ||
| resp = TestClient(create_app()).get("/health") | ||
|
|
||
| assert resp.status_code == 200 | ||
| assert resp.json() == {"status": "ok"} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # ── Windup 本地 / 服务器部署编排 ────────────────────────────────────── | ||
| # 启动: docker compose up -d --build | ||
| # 日志: docker compose logs -f backend | ||
| # 停止: docker compose down (加 -v 会删库数据) | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:16-alpine | ||
| container_name: windup-postgres | ||
| restart: unless-stopped | ||
| environment: | ||
| POSTGRES_USER: ${POSTGRES_USER:-root} | ||
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?请在 .env 里设置,不要用默认值} | ||
| POSTGRES_DB: ${POSTGRES_DB:-windup} | ||
| ports: | ||
| - "${POSTGRES_EXTERNAL_PORT:-7856}:5432" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Postgres is published to the host (default |
||
| volumes: | ||
| - postgres_data:/var/lib/postgresql/data | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-root} -d ${POSTGRES_DB:-windup}"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 | ||
| networks: [windup-net] | ||
|
|
||
| backend: | ||
| build: | ||
| context: ./backend | ||
| dockerfile: Dockerfile | ||
| container_name: windup-backend | ||
| restart: unless-stopped | ||
| depends_on: | ||
| postgres: { condition: service_healthy } | ||
| environment: | ||
| POSTGRES_HOST: postgres | ||
| POSTGRES_PORT: 5432 | ||
| POSTGRES_USER: ${POSTGRES_USER:-root} | ||
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| POSTGRES_DB: ${POSTGRES_DB:-windup} | ||
| # 七牛 Kodo(media 上传用) | ||
| QINIU_ACCESS_KEY: ${QINIU_ACCESS_KEY} | ||
| QINIU_SECRET_KEY: ${QINIU_SECRET_KEY} | ||
| QINIU_BUCKET_NAME: ${QINIU_BUCKET_NAME} | ||
| QINIU_BUCKET_DOMAIN: ${QINIU_BUCKET_DOMAIN} | ||
| QINIU_PRIVATE_SPACE: ${QINIU_PRIVATE_SPACE:-false} | ||
| # AI provider | ||
| AI_BASE_URL: ${AI_BASE_URL} | ||
| AI_API_KEY: ${AI_API_KEY} | ||
| # 允许跨域的前端来源,逗号分隔;不设则用代码里的开发默认值 | ||
| WINDUP_CORS_ORIGINS: ${WINDUP_CORS_ORIGINS:-} | ||
| # 预览域名正则(可选)。只写自家项目的预览域名形态,别写成整个平台通配 —— | ||
| # 后端开了 allow_credentials,通配等于放行该平台下任意第三方应用。 | ||
| # 例: https://<项目名>-[a-z0-9-]+\.vercel\.app | ||
| WINDUP_CORS_ORIGIN_REGEX: ${WINDUP_CORS_ORIGIN_REGEX:-} | ||
| ports: | ||
| - "${WINDUP_PORT:-8000}:8000" | ||
| networks: [windup-net] | ||
|
|
||
| volumes: | ||
| postgres_data: | ||
| driver: local | ||
|
|
||
| networks: | ||
| windup-net: | ||
| driver: bridge | ||
| # 云主机链路 MTU 常小于 1500(实测某部署机 eno1 为 1480)。compose 自建网络 | ||
| # **不继承** daemon.json 里的 mtu 设置,默认仍是 1500 → 大包被丢,表现为 | ||
| # TLS 握手超时(对象存储上传挂死、pip 下载卡死),而不是明确报错。 | ||
| driver_opts: | ||
| com.docker.network.driver.mtu: "1450" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VITE_API_BASE_URLis documented (with a "未配置时启动直接报错" fail-fast guarantee) but not implemented: noVITE_API_BASE_URL,import.meta.env, or HTTP client reference exists anywhere underfrontend/(verified by grep). As written, following these steps produces a frontend build that is not actually wired to the backend. Either add the frontend wiring in this PR or flag this section as not-yet-implemented.