diff --git a/Dockerfile b/Dockerfile index 7511f3a..2e6b2de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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; \ diff --git a/deploy.sh b/deploy.sh index fe947c1..79efac0 100755 --- a/deploy.sh +++ b/deploy.sh @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index 55a0580..b6ade33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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"]