mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
189
.github/workflows/docker-publish.yml
vendored
189
.github/workflows/docker-publish.yml
vendored
@@ -4,177 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
build_base:
|
|
||||||
description: 'Rebuild base image'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
BASE_IMAGE_NAME: fawney19/aether-base
|
|
||||||
APP_IMAGE_NAME: fawney19/aether
|
APP_IMAGE_NAME: fawney19/aether
|
||||||
GITHUB_REPO: fawney19/Aether
|
GITHUB_REPO: fawney19/Aether
|
||||||
# Base image hash inputs:
|
|
||||||
# - Dockerfile.base
|
|
||||||
# - pyproject.toml (dependency fingerprint only; ignores tool/optional deps)
|
|
||||||
# - frontend/package-lock.json
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-base-changes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: read
|
|
||||||
outputs:
|
|
||||||
base_changed: ${{ steps.check.outputs.base_changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Check if base image needs rebuild
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event.inputs.build_base }}" == "true" ]; then
|
|
||||||
echo "base_changed=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Calculate current hash of base-related inputs (dependency-only fingerprint)
|
|
||||||
PY_FINGERPRINT=$(python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import tomllib
|
|
||||||
|
|
||||||
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text("utf-8"))
|
|
||||||
project = data.get("project") or {}
|
|
||||||
build = data.get("build-system") or {}
|
|
||||||
|
|
||||||
fingerprint = {
|
|
||||||
"requires-python": project.get("requires-python"),
|
|
||||||
"dependencies": sorted(project.get("dependencies") or []),
|
|
||||||
"build-backend": build.get("build-backend"),
|
|
||||||
"build-requires": sorted(build.get("requires") or []),
|
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(fingerprint, sort_keys=True, separators=(",", ":")))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
CURRENT_HASH=$(
|
|
||||||
(
|
|
||||||
cat Dockerfile.base
|
|
||||||
printf '%s\n' "$PY_FINGERPRINT"
|
|
||||||
cat frontend/package-lock.json
|
|
||||||
) | sha256sum | cut -d' ' -f1
|
|
||||||
)
|
|
||||||
echo "Current base hash: $CURRENT_HASH"
|
|
||||||
|
|
||||||
# Try to get hash label from remote image config
|
|
||||||
# Pull the image config and extract labels
|
|
||||||
REMOTE_HASH=""
|
|
||||||
if docker pull ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest; then
|
|
||||||
REMOTE_HASH=$(docker inspect ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest --format '{{ index .Config.Labels "org.opencontainers.image.base.hash" }}' 2>/dev/null) || true
|
|
||||||
else
|
|
||||||
echo "WARN: failed to pull remote base image; forcing base rebuild."
|
|
||||||
echo "base_changed=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_HASH" ] || [ "$REMOTE_HASH" == "<no value>" ]; then
|
|
||||||
# No remote image or no hash label, need to rebuild
|
|
||||||
echo "No remote base image or hash label found, need rebuild"
|
|
||||||
echo "base_changed=true" >> $GITHUB_OUTPUT
|
|
||||||
elif [ "$CURRENT_HASH" != "$REMOTE_HASH" ]; then
|
|
||||||
echo "Hash mismatch: remote=$REMOTE_HASH, current=$CURRENT_HASH"
|
|
||||||
echo "base_changed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Hash matches, no rebuild needed"
|
|
||||||
echo "base_changed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-base:
|
|
||||||
needs: check-base-changes
|
|
||||||
if: needs.check-base-changes.outputs.base_changed == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Calculate base files hash
|
|
||||||
id: hash
|
|
||||||
run: |
|
|
||||||
PY_FINGERPRINT=$(python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import tomllib
|
|
||||||
|
|
||||||
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text("utf-8"))
|
|
||||||
project = data.get("project") or {}
|
|
||||||
build = data.get("build-system") or {}
|
|
||||||
|
|
||||||
fingerprint = {
|
|
||||||
"requires-python": project.get("requires-python"),
|
|
||||||
"dependencies": sorted(project.get("dependencies") or []),
|
|
||||||
"build-backend": build.get("build-backend"),
|
|
||||||
"build-requires": sorted(build.get("requires") or []),
|
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(fingerprint, sort_keys=True, separators=(",", ":")))
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
|
|
||||||
HASH=$(
|
|
||||||
(
|
|
||||||
cat Dockerfile.base
|
|
||||||
printf '%s\n' "$PY_FINGERPRINT"
|
|
||||||
cat frontend/package-lock.json
|
|
||||||
) | sha256sum | cut -d' ' -f1
|
|
||||||
)
|
|
||||||
echo "hash=$HASH" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Extract metadata for base image
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest
|
|
||||||
type=sha,prefix=
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.base.hash=${{ steps.hash.outputs.hash }}
|
|
||||||
|
|
||||||
- name: Build and push base image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.base
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha,scope=base
|
|
||||||
cache-to: type=gha,mode=max,scope=base
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|
||||||
download-hub:
|
download-hub:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -197,8 +33,7 @@ jobs:
|
|||||||
echo "Hub release tag: $TAG"
|
echo "Hub release tag: $TAG"
|
||||||
|
|
||||||
build-app:
|
build-app:
|
||||||
needs: [check-base-changes, build-base, download-hub]
|
needs: [download-hub]
|
||||||
if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.download-hub.result == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -241,30 +76,13 @@ jobs:
|
|||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
# 从 tag 提取版本号,如 v0.2.5 -> 0.2.5
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||||
if [ "$VERSION" = "$GITHUB_REF" ]; then
|
if [ "$VERSION" = "$GITHUB_REF" ]; then
|
||||||
# 不是 tag 触发,使用 git describe
|
|
||||||
VERSION=$(git describe --tags --always | sed 's/^v//')
|
VERSION=$(git describe --tags --always | sed 's/^v//')
|
||||||
fi
|
fi
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Extracted version: $VERSION"
|
echo "Extracted version: $VERSION"
|
||||||
|
|
||||||
- name: Update Dockerfile.app to use registry base image
|
|
||||||
run: |
|
|
||||||
sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
|
|
||||||
|
|
||||||
- name: Generate version file
|
|
||||||
run: |
|
|
||||||
# 生成 _version.py 文件
|
|
||||||
cat > src/_version.py << EOF
|
|
||||||
# Auto-generated by CI
|
|
||||||
__version__ = '${{ steps.version.outputs.version }}'
|
|
||||||
__version_tuple__ = tuple(int(x) for x in '${{ steps.version.outputs.version }}'.split('.') if x.isdigit())
|
|
||||||
version = __version__
|
|
||||||
version_tuple = __version_tuple__
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Resolve hub release for build args
|
- name: Resolve hub release for build args
|
||||||
run: |
|
run: |
|
||||||
echo "Hub release tag: ${{ needs.download-hub.outputs.hub_tag }}"
|
echo "Hub release tag: ${{ needs.download-hub.outputs.hub_tag }}"
|
||||||
@@ -276,7 +94,6 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.app
|
file: ./Dockerfile.app
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
no-cache-filters: builder
|
|
||||||
cache-from: type=gha,scope=app-amd64
|
cache-from: type=gha,scope=app-amd64
|
||||||
cache-to: type=gha,mode=min,scope=app-amd64
|
cache-to: type=gha,mode=min,scope=app-amd64
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -292,7 +109,6 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.app
|
file: ./Dockerfile.app
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
no-cache-filters: builder
|
|
||||||
cache-from: type=gha,scope=app-arm64
|
cache-from: type=gha,scope=app-arm64
|
||||||
cache-to: type=gha,mode=min,scope=app-arm64
|
cache-to: type=gha,mode=min,scope=app-arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -303,16 +119,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Create multi-arch manifest and push
|
- name: Create multi-arch manifest and push
|
||||||
run: |
|
run: |
|
||||||
# Extract digests
|
|
||||||
AMD64_DIGEST="${{ steps.build-amd64.outputs.digest }}"
|
AMD64_DIGEST="${{ steps.build-amd64.outputs.digest }}"
|
||||||
ARM64_DIGEST="${{ steps.build-arm64.outputs.digest }}"
|
ARM64_DIGEST="${{ steps.build-arm64.outputs.digest }}"
|
||||||
echo "amd64 digest: $AMD64_DIGEST"
|
echo "amd64 digest: $AMD64_DIGEST"
|
||||||
echo "arm64 digest: $ARM64_DIGEST"
|
echo "arm64 digest: $ARM64_DIGEST"
|
||||||
|
|
||||||
# For each tag, create multi-arch manifest on each registry
|
|
||||||
TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ')
|
TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ')
|
||||||
for FULL_TAG in $TAGS; do
|
for FULL_TAG in $TAGS; do
|
||||||
# Determine which registry this tag belongs to
|
|
||||||
if [[ "$FULL_TAG" == ghcr.io/* ]]; then
|
if [[ "$FULL_TAG" == ghcr.io/* ]]; then
|
||||||
REPO="${{ env.REGISTRY }}/${{ env.APP_IMAGE_NAME }}"
|
REPO="${{ env.REGISTRY }}/${{ env.APP_IMAGE_NAME }}"
|
||||||
elif [[ "$FULL_TAG" == docker.io/* ]]; then
|
elif [[ "$FULL_TAG" == docker.io/* ]]; then
|
||||||
|
|||||||
64
Cargo.lock
generated
64
Cargo.lock
generated
@@ -115,6 +115,8 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -305,6 +307,18 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-compression"
|
||||||
|
version = "0.4.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
|
||||||
|
dependencies = [
|
||||||
|
"compression-codecs",
|
||||||
|
"compression-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -711,6 +725,23 @@ dependencies = [
|
|||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compression-codecs"
|
||||||
|
version = "0.4.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
|
||||||
|
dependencies = [
|
||||||
|
"compression-core",
|
||||||
|
"flate2",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compression-core"
|
||||||
|
version = "0.4.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1485,6 +1516,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -2024,6 +2061,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3917,16 +3964,27 @@ version = "0.6.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
"iri-string",
|
"iri-string",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4073,6 +4131,12 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
|
|||||||
263
Dockerfile.app
263
Dockerfile.app
@@ -1,14 +1,17 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# 运行镜像:从 base 提取产物到精简运行时
|
# Aether 运行镜像:Rust gateway 直接服务 API + 前端静态文件
|
||||||
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
||||||
# 用于 GitHub Actions CI(官方源)
|
# 用于 GitHub Actions CI(官方源)
|
||||||
|
|
||||||
FROM aether-base:latest AS builder
|
# ==================== 前端构建 ====================
|
||||||
WORKDIR /app
|
FROM node:22-slim AS frontend-builder
|
||||||
# 复制前端源码并构建(CI 通过 no-cache-filters=builder 确保每次重建)
|
WORKDIR /app/frontend
|
||||||
COPY frontend/ ./frontend/
|
COPY frontend/package*.json ./
|
||||||
RUN cd frontend && npm run build
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ==================== Rust gateway 构建 ====================
|
||||||
FROM rust:1.86-slim AS gateway-builder
|
FROM rust:1.86-slim AS gateway-builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
@@ -26,244 +29,42 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
cp target/release/aether-gateway /tmp/aether-gateway
|
cp target/release/aether-gateway /tmp/aether-gateway
|
||||||
|
|
||||||
# ==================== 运行时镜像 ====================
|
# ==================== 运行时镜像 ====================
|
||||||
FROM python:3.13-slim
|
FROM debian:bookworm-slim
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 运行时依赖(无 gcc/nodejs/npm,使用 BuildKit 缓存加速)
|
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
apt-get update && apt-get install -y --no-install-recommends \
|
apt-get update && apt-get install -y --no-install-recommends \
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
libpq5 \
|
|
||||||
curl \
|
curl \
|
||||||
libjemalloc2
|
libjemalloc2 \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
|
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
|
||||||
[ -n "$jemalloc_path" ]; \
|
[ -n "$jemalloc_path" ]; \
|
||||||
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
|
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
|
||||||
# 从 base 镜像复制 Python 包
|
|
||||||
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
# 复制 gateway 二进制
|
||||||
# 只复制需要的 Python 可执行文件
|
|
||||||
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
|
||||||
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
|
||||||
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
|
||||||
COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
|
COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
|
||||||
# 从 builder 阶段复制前端构建产物
|
|
||||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
# 复制前端构建产物
|
||||||
RUN chmod -R 755 /usr/share/nginx/html
|
COPY --from=frontend-builder /app/frontend/dist /srv/frontend
|
||||||
# 复制后端代码
|
RUN chmod -R 755 /srv/frontend
|
||||||
COPY src/ ./src/
|
|
||||||
COPY alembic.ini ./
|
RUN mkdir -p /app/logs /app/data
|
||||||
COPY alembic/ ./alembic/
|
WORKDIR /app
|
||||||
COPY gunicorn_conf.py ./
|
|
||||||
# Nginx 配置模板
|
ENV LANG=C.UTF-8 \
|
||||||
# 策略:白名单后端路由 → 后端代理,其余全部 → 前端 SPA(index.html)
|
|
||||||
# 智能处理 IP:有外层代理头就透传,没有就用直连 IP
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'map $http_x_real_ip $real_ip {' \
|
|
||||||
' default $http_x_real_ip;' \
|
|
||||||
' "" $remote_addr;' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'map $http_x_forwarded_for $forwarded_for {' \
|
|
||||||
' default $http_x_forwarded_for;' \
|
|
||||||
' "" $remote_addr;' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'server {' \
|
|
||||||
' listen 80;' \
|
|
||||||
' server_name _;' \
|
|
||||||
' root /usr/share/nginx/html;' \
|
|
||||||
' index index.html;' \
|
|
||||||
' client_max_body_size 100M;' \
|
|
||||||
'' \
|
|
||||||
' # gzip 压缩配置(对 base64 图片等非流式响应有效)' \
|
|
||||||
' gzip on;' \
|
|
||||||
' gzip_min_length 256;' \
|
|
||||||
' gzip_comp_level 5;' \
|
|
||||||
' gzip_vary on;' \
|
|
||||||
' gzip_proxied any;' \
|
|
||||||
' gzip_types application/json text/plain text/css text/javascript application/javascript application/octet-stream;' \
|
|
||||||
' gzip_disable "msie6";' \
|
|
||||||
'' \
|
|
||||||
' # 静态资源:长期缓存' \
|
|
||||||
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
|
||||||
' expires 1y;' \
|
|
||||||
' add_header Cache-Control "public, no-transform";' \
|
|
||||||
' try_files $uri =404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 安全:阻止访问源码目录' \
|
|
||||||
' location ~ ^/(src|node_modules)/ {' \
|
|
||||||
' deny all;' \
|
|
||||||
' return 404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # WebSocket 隧道端点(gateway-owned tunnel 模式)' \
|
|
||||||
' location = /api/internal/proxy-tunnel {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' proxy_set_header Upgrade $http_upgrade;' \
|
|
||||||
' proxy_set_header Connection "upgrade";' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' proxy_read_timeout 86400s;' \
|
|
||||||
' proxy_send_timeout 86400s;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 后端 API 路由(白名单)→ 代理到后端' \
|
|
||||||
' location ~ ^/(api|v1|v1beta|upload|health)(/|$) {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' proxy_set_header Connection "";' \
|
|
||||||
' proxy_set_header Accept $http_accept;' \
|
|
||||||
' proxy_set_header Content-Type $content_type;' \
|
|
||||||
' proxy_set_header Authorization $http_authorization;' \
|
|
||||||
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' proxy_buffering off;' \
|
|
||||||
' proxy_cache off;' \
|
|
||||||
' proxy_request_buffering off;' \
|
|
||||||
' chunked_transfer_encoding on;' \
|
|
||||||
' gzip off;' \
|
|
||||||
' add_header X-Accel-Buffering no;' \
|
|
||||||
' proxy_connect_timeout 60s;' \
|
|
||||||
' proxy_send_timeout 3600s;' \
|
|
||||||
' proxy_read_timeout 3600s;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # API 文档路由 → 代理到后端' \
|
|
||||||
' location ~ ^/(docs|redoc|openapi\\.json)$ {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 所有其他路由 → 前端 SPA(先尝试静态文件,再回退到 index.html)' \
|
|
||||||
' location / {' \
|
|
||||||
' try_files $uri $uri/ /index.html;' \
|
|
||||||
' }' \
|
|
||||||
'}' > /etc/nginx/sites-available/default.template
|
|
||||||
# Supervisor 配置
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'[supervisord]' \
|
|
||||||
'nodaemon=true' \
|
|
||||||
'logfile=/var/log/supervisor/supervisord.log' \
|
|
||||||
'pidfile=/var/run/supervisord.pid' \
|
|
||||||
'' \
|
|
||||||
'[program:nginx]' \
|
|
||||||
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/%(ENV_PORT)s/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/var/log/nginx/access.log' \
|
|
||||||
'stderr_logfile=/var/log/nginx/error.log' \
|
|
||||||
'' \
|
|
||||||
'[program:gateway]' \
|
|
||||||
'command=/usr/local/bin/aether-gateway --bind 127.0.0.1:%(ENV_PORT)s --upstream http://127.0.0.1:%(ENV_LEGACY_APP_PORT)s' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=RUST_LOG=aether_gateway=info' \
|
|
||||||
'' \
|
|
||||||
'[program:app]' \
|
|
||||||
'command=/bin/bash -c "MAX_REQUESTS_JITTER=$((${MAX_REQUESTS:-50000}/20)); exec gunicorn src.main:app -c gunicorn_conf.py --preload -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:%(ENV_LEGACY_APP_PORT)s --max-requests ${MAX_REQUESTS:-50000} --max-requests-jitter $MAX_REQUESTS_JITTER --access-logfile - --error-logfile - --log-level info"' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true,LD_PRELOAD=/usr/local/lib/libjemalloc.so.2,MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000"' > /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
# 创建目录
|
|
||||||
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
|
||||||
# 入口脚本(启动前执行迁移)
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
# 环境变量
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONIOENCODING=utf-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
|
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
|
||||||
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
|
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
|
||||||
PORT=8084 \
|
RUST_LOG=aether_gateway=info \
|
||||||
LEGACY_APP_PORT=18084 \
|
AETHER_GATEWAY_BIND=0.0.0.0:80 \
|
||||||
GUNICORN_WORKERS=2 \
|
AETHER_GATEWAY_STATIC_DIR=/srv/frontend
|
||||||
MAX_REQUESTS=4000
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD curl -f http://localhost/health || exit 1
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
ENTRYPOINT ["/usr/local/bin/aether-gateway"]
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本)
|
# Aether 运行镜像:Rust gateway 直接服务 API + 前端静态文件(国内镜像源版本)
|
||||||
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
|
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
|
||||||
# 用于本地/国内服务器部署
|
|
||||||
|
|
||||||
FROM aether-base:latest AS builder
|
# ==================== 前端构建 ====================
|
||||||
|
FROM node:22-slim AS frontend-builder
|
||||||
WORKDIR /app
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package*.json ./
|
||||||
# 复制前端源码并构建
|
RUN npm config set registry https://registry.npmmirror.com && npm ci
|
||||||
COPY frontend/ ./frontend/
|
COPY frontend/ ./
|
||||||
RUN cd frontend && npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# ==================== Rust gateway 构建 ====================
|
||||||
FROM rust:1.86-slim AS gateway-builder
|
FROM rust:1.86-slim AS gateway-builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
@@ -29,258 +29,43 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
cp target/release/aether-gateway /tmp/aether-gateway
|
cp target/release/aether-gateway /tmp/aether-gateway
|
||||||
|
|
||||||
# ==================== 运行时镜像 ====================
|
# ==================== 运行时镜像 ====================
|
||||||
FROM python:3.13-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 运行时依赖(使用清华镜像源 + BuildKit 缓存加速)
|
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && apt-get install -y --no-install-recommends \
|
apt-get update && apt-get install -y --no-install-recommends \
|
||||||
nginx \
|
|
||||||
supervisor \
|
|
||||||
libpq5 \
|
|
||||||
curl \
|
curl \
|
||||||
libjemalloc2
|
libjemalloc2 \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
|
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
|
||||||
[ -n "$jemalloc_path" ]; \
|
[ -n "$jemalloc_path" ]; \
|
||||||
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
|
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
|
||||||
|
|
||||||
# 从 base 镜像复制 Python 包
|
# 复制 gateway 二进制
|
||||||
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
|
||||||
|
|
||||||
# 只复制需要的 Python 可执行文件
|
|
||||||
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
|
||||||
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
|
||||||
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
|
||||||
COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
|
COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
|
||||||
|
|
||||||
# 从 builder 阶段复制前端构建产物
|
# 复制前端构建产物
|
||||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
COPY --from=frontend-builder /app/frontend/dist /srv/frontend
|
||||||
RUN chmod -R 755 /usr/share/nginx/html
|
RUN chmod -R 755 /srv/frontend
|
||||||
|
|
||||||
# 复制后端代码
|
RUN mkdir -p /app/logs /app/data
|
||||||
COPY src/ ./src/
|
WORKDIR /app
|
||||||
COPY alembic.ini ./
|
|
||||||
COPY alembic/ ./alembic/
|
|
||||||
COPY gunicorn_conf.py ./
|
|
||||||
|
|
||||||
# Nginx 配置模板
|
ENV LANG=C.UTF-8 \
|
||||||
# 策略:白名单后端路由 → 后端代理,其余全部 → 前端 SPA(index.html)
|
|
||||||
# 智能处理 IP:有外层代理头就透传,没有就用直连 IP
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'map $http_x_real_ip $real_ip {' \
|
|
||||||
' default $http_x_real_ip;' \
|
|
||||||
' "" $remote_addr;' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'map $http_x_forwarded_for $forwarded_for {' \
|
|
||||||
' default $http_x_forwarded_for;' \
|
|
||||||
' "" $remote_addr;' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'server {' \
|
|
||||||
' listen 80;' \
|
|
||||||
' server_name _;' \
|
|
||||||
' root /usr/share/nginx/html;' \
|
|
||||||
' index index.html;' \
|
|
||||||
' client_max_body_size 100M;' \
|
|
||||||
'' \
|
|
||||||
' # gzip 压缩配置(对 base64 图片等非流式响应有效)' \
|
|
||||||
' gzip on;' \
|
|
||||||
' gzip_min_length 256;' \
|
|
||||||
' gzip_comp_level 5;' \
|
|
||||||
' gzip_vary on;' \
|
|
||||||
' gzip_proxied any;' \
|
|
||||||
' gzip_types application/json text/plain text/css text/javascript application/javascript application/octet-stream;' \
|
|
||||||
' gzip_disable "msie6";' \
|
|
||||||
'' \
|
|
||||||
' # 静态资源:长期缓存' \
|
|
||||||
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
|
|
||||||
' expires 1y;' \
|
|
||||||
' add_header Cache-Control "public, no-transform";' \
|
|
||||||
' try_files $uri =404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 安全:阻止访问源码目录' \
|
|
||||||
' location ~ ^/(src|node_modules)/ {' \
|
|
||||||
' deny all;' \
|
|
||||||
' return 404;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # WebSocket 隧道端点(gateway-owned tunnel 模式)' \
|
|
||||||
' location = /api/internal/proxy-tunnel {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' proxy_set_header Upgrade $http_upgrade;' \
|
|
||||||
' proxy_set_header Connection "upgrade";' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' proxy_read_timeout 86400s;' \
|
|
||||||
' proxy_send_timeout 86400s;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 后端 API 路由(白名单)→ 代理到后端' \
|
|
||||||
' location ~ ^/(api|v1|v1beta|upload|health)(/|$) {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' proxy_set_header Connection "";' \
|
|
||||||
' proxy_set_header Accept $http_accept;' \
|
|
||||||
' proxy_set_header Content-Type $content_type;' \
|
|
||||||
' proxy_set_header Authorization $http_authorization;' \
|
|
||||||
' proxy_set_header X-Api-Key $http_x_api_key;' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' proxy_buffering off;' \
|
|
||||||
' proxy_cache off;' \
|
|
||||||
' proxy_request_buffering off;' \
|
|
||||||
' chunked_transfer_encoding on;' \
|
|
||||||
' gzip off;' \
|
|
||||||
' add_header X-Accel-Buffering no;' \
|
|
||||||
' proxy_connect_timeout 60s;' \
|
|
||||||
' proxy_send_timeout 3600s;' \
|
|
||||||
' proxy_read_timeout 3600s;' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # API 文档路由 → 代理到后端' \
|
|
||||||
' location ~ ^/(docs|redoc|openapi\\.json)$ {' \
|
|
||||||
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
|
|
||||||
' proxy_http_version 1.1;' \
|
|
||||||
' proxy_set_header Host $host;' \
|
|
||||||
' proxy_set_header X-Real-IP $real_ip;' \
|
|
||||||
' proxy_set_header X-Forwarded-For $forwarded_for;' \
|
|
||||||
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
||||||
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
|
|
||||||
' proxy_hide_header CF-Connecting-IP;' \
|
|
||||||
' proxy_hide_header CF-IPCountry;' \
|
|
||||||
' proxy_hide_header CF-Ray;' \
|
|
||||||
' proxy_hide_header CF-Visitor;' \
|
|
||||||
' proxy_hide_header CDN-Loop;' \
|
|
||||||
' proxy_hide_header True-Client-IP;' \
|
|
||||||
' proxy_hide_header CF-Worker;' \
|
|
||||||
' proxy_hide_header CF-EW-Via;' \
|
|
||||||
' proxy_hide_header CF-Warp-Tag-ID;' \
|
|
||||||
' proxy_set_header CF-Connecting-IP "";' \
|
|
||||||
' proxy_set_header CF-IPCountry "";' \
|
|
||||||
' proxy_set_header CF-Ray "";' \
|
|
||||||
' proxy_set_header CF-Visitor "";' \
|
|
||||||
' proxy_set_header CDN-Loop "";' \
|
|
||||||
' proxy_set_header True-Client-IP "";' \
|
|
||||||
' proxy_set_header CF-Worker "";' \
|
|
||||||
' proxy_set_header CF-EW-Via "";' \
|
|
||||||
' proxy_set_header CF-Warp-Tag-ID "";' \
|
|
||||||
' }' \
|
|
||||||
'' \
|
|
||||||
' # 所有其他路由 → 前端 SPA(先尝试静态文件,再回退到 index.html)' \
|
|
||||||
' location / {' \
|
|
||||||
' try_files $uri $uri/ /index.html;' \
|
|
||||||
' }' \
|
|
||||||
'}' > /etc/nginx/sites-available/default.template
|
|
||||||
|
|
||||||
# Supervisor 配置
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'[supervisord]' \
|
|
||||||
'nodaemon=true' \
|
|
||||||
'logfile=/var/log/supervisor/supervisord.log' \
|
|
||||||
'pidfile=/var/run/supervisord.pid' \
|
|
||||||
'' \
|
|
||||||
'[program:nginx]' \
|
|
||||||
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/%(ENV_PORT)s/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/var/log/nginx/access.log' \
|
|
||||||
'stderr_logfile=/var/log/nginx/error.log' \
|
|
||||||
'' \
|
|
||||||
'[program:gateway]' \
|
|
||||||
'command=/usr/local/bin/aether-gateway --bind 127.0.0.1:%(ENV_PORT)s --upstream http://127.0.0.1:%(ENV_LEGACY_APP_PORT)s' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=RUST_LOG=aether_gateway=info' \
|
|
||||||
'' \
|
|
||||||
'[program:app]' \
|
|
||||||
'command=/bin/bash -c "MAX_REQUESTS_JITTER=$((${MAX_REQUESTS:-50000}/20)); exec gunicorn src.main:app -c gunicorn_conf.py --preload -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:%(ENV_LEGACY_APP_PORT)s --max-requests ${MAX_REQUESTS:-50000} --max-requests-jitter $MAX_REQUESTS_JITTER --access-logfile - --error-logfile - --log-level info"' \
|
|
||||||
'directory=/app' \
|
|
||||||
'autostart=true' \
|
|
||||||
'autorestart=true' \
|
|
||||||
'stdout_logfile=/dev/stdout' \
|
|
||||||
'stdout_logfile_maxbytes=0' \
|
|
||||||
'stderr_logfile=/dev/stderr' \
|
|
||||||
'stderr_logfile_maxbytes=0' \
|
|
||||||
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true,LD_PRELOAD=/usr/local/lib/libjemalloc.so.2,MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000"' > /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
|
|
||||||
# 创建目录
|
|
||||||
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
|
||||||
|
|
||||||
# 入口脚本(启动前执行迁移)
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONIOENCODING=utf-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
|
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
|
||||||
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
|
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
|
||||||
PORT=8084 \
|
RUST_LOG=aether_gateway=info \
|
||||||
LEGACY_APP_PORT=18084 \
|
AETHER_GATEWAY_BIND=0.0.0.0:80 \
|
||||||
GUNICORN_WORKERS=2 \
|
AETHER_GATEWAY_STATIC_DIR=/srv/frontend
|
||||||
MAX_REQUESTS=4000
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/aether-gateway"]
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
# 构建镜像:编译环境 + 预编译的依赖
|
|
||||||
# 用于 GitHub Actions CI 构建(不使用国内镜像源)
|
|
||||||
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest .
|
|
||||||
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
|
|
||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 构建工具(使用 BuildKit 缓存加速)
|
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
|
||||||
apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libpq-dev \
|
|
||||||
gcc \
|
|
||||||
nodejs \
|
|
||||||
npm
|
|
||||||
|
|
||||||
# Python 依赖(使用 BuildKit 缓存加速)
|
|
||||||
COPY pyproject.toml README.md ./
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
mkdir -p src && touch src/__init__.py && \
|
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install .
|
|
||||||
|
|
||||||
# 前端依赖(只安装,不构建,使用 BuildKit 缓存加速)
|
|
||||||
COPY frontend/package*.json ./frontend/
|
|
||||||
RUN --mount=type=cache,target=/root/.npm \
|
|
||||||
cd frontend && npm ci
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
# 构建镜像:编译环境 + 预编译的依赖(国内镜像源版本)
|
|
||||||
# 构建命令: docker build -f Dockerfile.base.local -t aether-base:latest .
|
|
||||||
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
|
|
||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 构建工具(使用清华镜像源 + BuildKit 缓存加速)
|
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
|
||||||
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
|
|
||||||
apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libpq-dev \
|
|
||||||
gcc \
|
|
||||||
nodejs \
|
|
||||||
npm
|
|
||||||
|
|
||||||
# pip 镜像源
|
|
||||||
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
|
|
||||||
# Python 依赖(使用 BuildKit 缓存加速)
|
|
||||||
COPY pyproject.toml README.md ./
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
mkdir -p src && touch src/__init__.py && \
|
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install .
|
|
||||||
|
|
||||||
# 前端依赖(只安装,不构建,使用淘宝镜像源 + BuildKit 缓存加速)
|
|
||||||
COPY frontend/package*.json ./frontend/
|
|
||||||
RUN --mount=type=cache,target=/root/.npm \
|
|
||||||
cd frontend && npm config set registry https://registry.npmmirror.com && npm ci
|
|
||||||
24
_deprecated_py_src/_version.py
Normal file
24
_deprecated_py_src/_version.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# file generated by vcs-versioning
|
||||||
|
# don't change, don't track in version control
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"__version_tuple__",
|
||||||
|
"version",
|
||||||
|
"version_tuple",
|
||||||
|
"__commit_id__",
|
||||||
|
"commit_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
version: str
|
||||||
|
__version__: str
|
||||||
|
__version_tuple__: tuple[int | str, ...]
|
||||||
|
version_tuple: tuple[int | str, ...]
|
||||||
|
commit_id: str | None
|
||||||
|
__commit_id__: str | None
|
||||||
|
|
||||||
|
__version__ = version = '0.6.4.dev6+gddf18fed9.d20260331'
|
||||||
|
__version_tuple__ = version_tuple = (0, 6, 4, 'dev6', 'gddf18fed9.d20260331')
|
||||||
|
|
||||||
|
__commit_id__ = commit_id = None
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user