From 828704e3e8a105710b375dfc3d7038e516194519 Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Tue, 13 Oct 2026 02:41:13 -0700 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 25 ++++++++ .gitea/workflows/deploy.yml | 123 ++++++++++++++++++++++++++++++++++++ Dockerfile | 39 ++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2bce76e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitea +.agents +.claude +.hermes +.idea +.omc +.jbeval +.venv +**/__pycache__ +**/*.py[cod] +.*cache* +.env* +*.log +.mcp.json +data +logs +docs +tests +evals +examples +results +AGENTS.md +README.md +skills-lock.json diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..c84494c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,123 @@ +name: my-v0-server +run-name: "${{ gitea.event.head_commit.message }}" + +on: + push: + branches: + - main + +concurrency: + group: deploy-my-v0-server + cancel-in-progress: true + +env: + IMAGE_NAME: my-v0-server + HOST_PORT: 33020 + # Docker daemon 所在宿主机的目录;不要求 runner 容器能直接读取 + DEPLOY_DIR: /app/my-v0-server + DOCKER_BUILDKIT: "0" + +jobs: + deploy: + runs-on: + - vps_jp + timeout-minutes: 20 + steps: + - name: Prepare environment + run: | + set -eu + STEP_START=$(date +%s) + + if ! command -v docker > /dev/null 2>&1; then + apk add docker-cli + fi + if ! command -v git > /dev/null 2>&1; then + apk add git + fi + if ! command -v node > /dev/null 2>&1; then + apk add nodejs + fi + + docker --version + echo "Environment preparation took $(( $(date +%s) - STEP_START ))s" + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build application image + run: | + set -eu + STEP_START=$(date +%s) + SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8) + docker build \ + --label "git.sha=${{ gitea.sha }}" \ + -t "$IMAGE_NAME:latest" \ + -t "$IMAGE_NAME:$SHORT_SHA" . + docker images "$IMAGE_NAME:latest" --format "Image size: {{.Size}}" + echo "Application image step took $(( $(date +%s) - STEP_START ))s" + + - name: Validate host environment + run: | + set -eu + SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8) + # --mount 从宿主机读取;文件缺失会直接失败,在此之前不会停止旧服务 + docker run --rm \ + --mount "type=bind,src=$DEPLOY_DIR/.env,dst=/app/.env,readonly" \ + "$IMAGE_NAME:$SHORT_SHA" \ + python -c "from pathlib import Path; p = Path('/app/.env'); assert p.is_file() and p.stat().st_size > 0, 'Missing or empty host .env'; from app.config import settings; print('Host environment file loaded')" + + - name: Deploy container + run: | + set -eu + STEP_START=$(date +%s) + SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8) + + if docker ps -a --filter "name=^${IMAGE_NAME}$" -q | grep -q .; then + docker stop --timeout 30 "$IMAGE_NAME" + docker rm "$IMAGE_NAME" + fi + + docker run -d \ + --name "$IMAGE_NAME" \ + --network host \ + --mount "type=bind,src=$DEPLOY_DIR/.env,dst=/app/.env,readonly" \ + -v "$DEPLOY_DIR/data:/app/data" \ + -v "$DEPLOY_DIR/logs:/app/logs" \ + -e UVICORN_HOST=127.0.0.1 \ + -e UVICORN_PORT="$HOST_PORT" \ + -e TZ=Asia/Shanghai \ + --restart unless-stopped \ + "$IMAGE_NAME:$SHORT_SHA" + + MAX_WAIT=120 + WAITED=0 + HEALTH="unknown" + RUNNING_ID="" + while [ "$WAITED" -lt "$MAX_WAIT" ]; do + RUNNING_ID=$(docker ps --filter "name=^${IMAGE_NAME}$" --filter "status=running" -q) + HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$IMAGE_NAME" 2>/dev/null || echo "unknown") + if [ -n "$RUNNING_ID" ] && [ "$HEALTH" = "healthy" ]; then + break + fi + sleep 2 + WAITED=$((WAITED + 2)) + done + + if [ -n "$RUNNING_ID" ] && [ "$HEALTH" = "healthy" ]; then + docker ps --filter "name=^${IMAGE_NAME}$" --format "Status: {{.Status}}" + echo "Startup wait: ${WAITED}s" + else + echo "Container not healthy after ${MAX_WAIT}s (health=$HEALTH)" + docker inspect "$IMAGE_NAME" --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' 2>/dev/null || true + docker logs --tail 30 "$IMAGE_NAME" || true + exit 1 + fi + + echo "Deployment step took $(( $(date +%s) - STEP_START ))s" + + - name: Clean up old images + run: docker image prune -f + + - name: Show startup logs + if: always() + run: docker logs --tail 15 "$IMAGE_NAME" || true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..283b125 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.11-slim-bookworm AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.12.17 /uv /usr/local/bin/uv + +WORKDIR /app + +ENV UV_PYTHON_DOWNLOADS=never + +COPY pyproject.toml uv.lock ./ +RUN uv sync --locked --no-dev --no-install-project + +FROM python:3.11-slim-bookworm + +# PyArrow、DuckDB 的 Linux wheels 需要 C++ 运行库 +RUN apt-get update \ + && apt-get install -y --no-install-recommends libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATH="/app/.venv/bin:$PATH" \ + UVICORN_HOST=0.0.0.0 \ + UVICORN_PORT=8000 + +WORKDIR /app + +COPY --from=builder /app/.venv ./.venv +COPY app ./app +COPY mcp_servers ./mcp_servers +COPY mcp_servers.json ./ +COPY skills ./skills +COPY scripts/seed_materials.py ./scripts/seed_materials.py + +EXPOSE 8000 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \ + CMD python -c "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['UVICORN_PORT'] + '/api/files/limits', timeout=3).close()" || exit 1 + +CMD ["python", "-m", "uvicorn", "app.main:app", "--workers", "1"] -- 2.54.0