build(deploy): 优化 uv 安装与部署并发保护

This commit is contained in:
yml2213
2026-10-23 09:59:32 +08:00
parent f074b75871
commit 331208742e
3 changed files with 78 additions and 10 deletions
+12 -8
View File
@@ -27,6 +27,7 @@ RUN --mount=type=cache,target=/root/.npm \
FROM python:3.12-slim AS runtime
ARG USE_CHINA_MIRRORS=true
ARG UV_VERSION=0.13.5
# 服务器默认使用阿里云镜像;本地构建可设 USE_CHINA_MIRRORS=false 直连官方源。
# apt 缓存挂载:包索引和 .deb 都留在 BuildKit 缓存里,不再每次重下。
@@ -50,24 +51,28 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
fonts-wqy-microhei
# 安装 uv:服务器走阿里云镜像,本地直连官方 PyPI。
# mirrors.cloud.aliyuncs.com 的证书与主机名不匹配,直接用 mirrors.aliyun.com,
# 避免每次构建先失败重试 10 秒;版本固定,基础层重建时行为保持一致。
RUN --mount=type=cache,target=/root/.cache/pip \
if [ "$USE_CHINA_MIRRORS" = "true" ]; then \
pip install -i https://mirrors.cloud.aliyuncs.com/pypi/simple/ uv || \
pip install -i https://mirrors.aliyun.com/pypi/simple/ uv || \
pip install uv; \
(pip install --retries 2 --timeout 30 \
-i https://mirrors.aliyun.com/pypi/simple/ "uv==${UV_VERSION}" || \
pip install --retries 2 --timeout 30 "uv==${UV_VERSION}"); \
else \
pip install uv; \
pip install --retries 2 --timeout 30 "uv==${UV_VERSION}"; \
fi
WORKDIR /app
# 缓存目录和 .venv 位于不同文件系统时 uv 会自动回退为复制;显式设置可避免构建告警。
ENV UV_LINK_MODE=copy
# 先复制依赖文件,利用 Docker 缓存层
COPY pyproject.toml uv.lock* README.md alembic.ini ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv venv /app/.venv && \
if [ "$USE_CHINA_MIRRORS" = "true" ]; then \
(UV_INDEX_URL=https://mirrors.cloud.aliyuncs.com/pypi/simple/ uv pip install -e . || \
UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ uv pip install -e . || \
(UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ uv pip install -e . || \
uv pip install -e .); \
else \
uv pip install -e .; \
@@ -117,8 +122,7 @@ COPY tests/ ./tests/
COPY conftest.py ./
RUN --mount=type=cache,target=/root/.cache/uv \
if [ "$USE_CHINA_MIRRORS" = "true" ]; then \
(UV_INDEX_URL=https://mirrors.cloud.aliyuncs.com/pypi/simple/ uv sync --frozen --group dev || \
UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ uv sync --frozen --group dev || \
(UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ uv sync --frozen --group dev || \
uv sync --frozen --group dev); \
else \
uv sync --frozen --group dev; \
+63 -2
View File
@@ -56,6 +56,56 @@ stop_legacy_containers() {
done
}
# 同一台机器只允许一个部署任务构建/重建,避免两个 deploy.sh 抢缓存和互相覆盖。
acquire_deploy_lock() {
local lock_file="${DEPLOY_LOCK_FILE:-/tmp/live-hub-py-deploy.lock}"
if ! command -v flock >/dev/null 2>&1; then
echo "⚠️ 未安装 flock,跳过部署并发锁"
return 0
fi
exec 9>"$lock_file"
if ! flock -n 9; then
echo "❌ 已有部署任务在运行(锁: $lock_file),请稍后再试"
exit 1
fi
echo " 已获取部署锁: $lock_file"
}
# 给镜像构建加超时,避免网络卡住时脚本无限挂起。
build_app_image() {
local build_timeout="${DEPLOY_BUILD_TIMEOUT:-1800}"
echo " 构建超时: ${build_timeout}s"
if command -v timeout >/dev/null 2>&1; then
timeout "$build_timeout" $COMPOSE build douyu-login || {
local code=$?
echo "❌ 镜像构建失败或超过 ${build_timeout}s(退出码 ${code})"
exit "$code"
}
else
$COMPOSE build douyu-login
fi
}
# 等待 Docker 自身 HEALTHCHECK 通过(同时覆盖 Web 和同容器 Worker)。
wait_for_container_healthy() {
local cid status
cid="$($COMPOSE ps -q douyu-login 2>/dev/null | head -n 1)"
if [ -z "$cid" ]; then
return 1
fi
for _ in $(seq 1 60); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid" 2>/dev/null || true)"
if [ "$status" = "healthy" ]; then
return 0
fi
if [ "$status" = "unhealthy" ]; then
return 1
fi
sleep 2
done
return 1
}
# ── 子命令 ──────────────────────────────────────────────
cmd_deploy() {
@@ -64,6 +114,8 @@ cmd_deploy() {
echo " 斗鱼批量登录 - Docker 部署"
echo "=============================="
acquire_deploy_lock
if ! command -v docker &> /dev/null; then
echo "❌ 未安装 Docker,请先安装:"
echo " curl -fsSL https://get.docker.com | sh"
@@ -93,10 +145,10 @@ cmd_deploy() {
mkdir -p data logs
# 构建并启动
echo "正在构建应用镜像(首次构建约3-5分钟)..."
echo "正在构建应用镜像(首次构建可能较久;有缓存时通常几十秒)..."
stop_legacy_containers
# 生产部署不构建含大型开发依赖的 test 镜像;测试镜像仅在本地按需构建。
$COMPOSE build douyu-login
build_app_image
echo ""
echo "正在启动服务..."
@@ -113,11 +165,20 @@ cmd_deploy() {
# 30 秒不够:mysql 一起重建时应用就绪约 35 秒,会误报「启动超时」。
for i in $(seq 1 40); do
if curl -s "http://localhost:${APP_PORT}/api/health" | grep -q "ok" 2>/dev/null; then
echo ""
echo "服务已响应,等待 Docker 健康检查..."
if ! wait_for_container_healthy; then
echo "❌ 服务已响应,但容器未进入 healthy 状态,查看日志:"
$COMPOSE ps
$COMPOSE logs --tail 50 douyu-login
exit 1
fi
echo ""
echo "=============================="
echo " ✅ 部署成功!"
echo "=============================="
echo " 访问地址: http://localhost:${APP_PORT}"
echo " 容器健康: healthy"
echo " 默认账号见环境变量 ADMIN_USERNAME/ADMIN_PASSWORD(未设置则为 admin/admin123)"
echo ""
echo " 查看应用日志: ./deploy.sh logs"
+3
View File
@@ -7,6 +7,8 @@ services:
args:
# 服务器默认走阿里云镜像;本地测试设为 false。
USE_CHINA_MIRRORS: ${USE_CHINA_MIRRORS:-true}
# 固定 uv 版本,避免基础层重建时解析到不同版本。
UV_VERSION: ${UV_VERSION:-0.13.5}
restart: unless-stopped
mem_limit: ${DOUYU_LOGIN_MEM_LIMIT:-2g}
cpus: ${DOUYU_LOGIN_CPUS:-2}
@@ -112,6 +114,7 @@ services:
target: test
args:
USE_CHINA_MIRRORS: ${USE_CHINA_MIRRORS:-true}
UV_VERSION: ${UV_VERSION:-0.13.5}
profiles: ["test"]
entrypoint: ["python", "-m", "pytest"]