From fa65a401a59298b9e175018fa27eacdeace7c266 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 11 Sep 2026 08:43:07 +0800 Subject: [PATCH 1/2] fix(web): bake all nginx configs into image, select via APP_ENV at runtime Problem: web Dockerfile used ARG NGINX_CONF to bake a single nginx config at build time. CI always built from develop branch with nginx-staging.conf, so the production image also contained the staging nginx config pointing to xiaoxia-api-staging:8000, causing 502 in production after restarts. Fix: - Bake all 3 nginx configs (default/nginx.conf, nginx-staging.conf, nginx-production.conf) into the web image - Add nginx-entrypoint.sh that symlinks the correct config based on APP_ENV (staging/production) at container startup - Remove NGINX_CONF build arg from CI pipeline (3 occurrences) - Update deploy scripts to pass APP_ENV to web container - Update compose.yml to use APP_ENV instead of NGINX_ENV This ensures a single image artifact works for both staging and production, with the correct nginx upstream selected at runtime via environment variable. --- .gitea/workflows/ci-pipeline.yml | 3 --- infra/docker/compose.yml | 6 +++--- infra/docker/deploy-production-registry.sh | 21 ++++++--------------- infra/docker/deploy-staging-registry.sh | 12 ++---------- infra/docker/deploy-staging.sh | 1 - infra/docker/nginx-entrypoint.sh | 18 ++++++++++++++++++ infra/docker/web-artifact.Dockerfile | 10 +++++++--- infra/docker/web.Dockerfile | 10 +++++++--- 8 files changed, 43 insertions(+), 38 deletions(-) create mode 100755 infra/docker/nginx-entrypoint.sh diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index 8b997ff13..df2528baa 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -828,7 +828,6 @@ jobs: EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\"" if [ "${{ matrix.service }}" = "web" ]; then - EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf" fi # Worker 与 API/Web 统一走持久 builder(ci-builder-persist),共享宿主机层缓存 @@ -1027,7 +1026,6 @@ jobs: EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\"" if [ "${{ matrix.service }}" = "web" ]; then - EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf" fi NO_CACHE_FLAG="" @@ -1568,7 +1566,6 @@ jobs: EXTRA_BUILD_ARGS="APP_VERSION=\"${TAG_NAME}\"" if [ "${{ matrix.service }}" = "web" ]; then - EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-production.conf" fi # Docker build 带重试:失败自动重试2次,第2次重试加--no-cache diff --git a/infra/docker/compose.yml b/infra/docker/compose.yml index 46bfc0e74..466fa0eb9 100755 --- a/infra/docker/compose.yml +++ b/infra/docker/compose.yml @@ -163,7 +163,7 @@ services: # context: ../.. # dockerfile: ${WEB_DOCKERFILE:-infra/docker/web.Dockerfile} # args: - # NGINX_CONF: ${WEB_NGINX_CONF:-infra/docker/nginx.conf} + # (NGINX_CONF no longer needed - all configs baked into image) container_name: xiaoxia-web-${ENV:-staging} restart: unless-stopped @@ -178,12 +178,12 @@ services: - xiaoxia-net # ========================================= - # Nginx 配置运行时覆盖 + # Nginx 配置运行时覆盖(双保险:entrypoint 也按 APP_ENV 选择配置) # 确保容器使用正确环境的 nginx 配置,即使镜像构建时使用了默认配置 # 注意: 只覆盖 /etc/nginx/conf.d/default.conf,不挂载 /usr/share/nginx/html # ========================================= environment: - - NGINX_ENV=${ENV:-staging} + - APP_ENV=${ENV:-staging} volumes: - ./nginx-${ENV:-staging}.conf:/etc/nginx/conf.d/default.conf:ro diff --git a/infra/docker/deploy-production-registry.sh b/infra/docker/deploy-production-registry.sh index c0a78f05f..d9ff2dffc 100755 --- a/infra/docker/deploy-production-registry.sh +++ b/infra/docker/deploy-production-registry.sh @@ -14,11 +14,11 @@ REGISTRY_TOKEN="${REGISTRY_TOKEN:-}" ENV_FILE="${ENV_FILE:-/var/lib/xiaoxia-saas-production/.env}" GENERATED_DIR="${GENERATED_DIR:-/var/lib/xiaoxia-saas-production/generated}" LEGACY_ASSETS_DIR="${LEGACY_ASSETS_DIR:-/var/lib/xiaoxia-saas-production/legacy-assets}" +REPO_DIR="${REPO_DIR:-/var/lib/xiaoxia-saas-production/repo}" if [ -z "$IMAGE_TAG" ]; then echo "ERROR: IMAGE_TAG is required" exit 1 -fi test -f "$ENV_FILE" mkdir -p "$GENERATED_DIR" @@ -31,7 +31,6 @@ if [ -n "$REGISTRY_TOKEN" ]; then printf %s "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin 2>/dev/null || { echo "WARN: docker login failed, will try to pull anyway" } -fi # ---- Pull 三镜像 ---- REGISTRY_API="${REGISTRY}/xiaoxia-saas-api:${IMAGE_TAG}" @@ -66,17 +65,14 @@ if docker inspect xiaoxia-web-production >/dev/null 2>&1; then if [ -d "$_tmpdir" ] && [ "$(ls -A "$_tmpdir" 2>/dev/null)" ]; then cp -an "$_tmpdir"/. "$LEGACY_ASSETS_DIR"/ 2>/dev/null || true echo "Legacy assets backed up: $(ls "$_tmpdir" | wc -l) files" - fi rm -rf "$_tmpdir" else echo "No existing web container, skipping legacy assets backup" -fi # 清理超过 7 天的旧 assets 文件(避免无限增长) if [ -d "$LEGACY_ASSETS_DIR" ]; then find "$LEGACY_ASSETS_DIR" -type f -mtime +7 -delete 2>/dev/null || true echo "Legacy assets cleanup done (retain 7 days)" -fi # ---- 确保基础设施容器在运行 ---- echo "Checking infrastructure containers..." @@ -84,12 +80,10 @@ for c in xiaoxia-postgres-production xiaoxia-redis-production; do if ! docker inspect "$c" >/dev/null 2>&1; then echo "ERROR: Required container not found: $c" exit 1 - fi state=$(docker inspect -f '{{.State.Status}}' "$c") if [ "$state" != "running" ]; then echo "ERROR: Container not running: $c ($state)" exit 1 - fi done # ---- 确保生产网络存在 ---- @@ -108,7 +102,6 @@ echo "Migrations completed." echo "Stopping old containers..." docker rm -f xiaoxia-api-production 2>/dev/null || true docker rm -f xiaoxia-worker-production 2>/dev/null || true -docker rm -f xiaoxia-web-production 2>/dev/null || true # ---- 日志配置(所有容器共用) ---- LOG_OPTS="--log-driver json-file --log-opt max-size=50m --log-opt max-file=3" @@ -166,15 +159,16 @@ docker run -d \ # ---- 启动 Web ---- # Legacy assets 挂载到 /usr/share/nginx/html/assets-legacy/assets/ # nginx 配置中 assets location 有 fallback 逻辑 -LEGACY_VOLUME="" +WEB_VOLUMES="" if [ -d "$LEGACY_ASSETS_DIR" ] && [ "$(ls -A "$LEGACY_ASSETS_DIR" 2>/dev/null)" ]; then - LEGACY_VOLUME="-v ${LEGACY_ASSETS_DIR}:/usr/share/nginx/html/assets-legacy/assets:ro" + WEB_VOLUMES="-v ${LEGACY_ASSETS_DIR}:/usr/share/nginx/html/assets-legacy/assets:ro" echo "Web container: legacy assets mounted (fallback)" else echo "Web container: no legacy assets to mount" fi echo "Starting Web container..." +docker rm -f xiaoxia-web-production 2>/dev/null || true docker run -d \ --name xiaoxia-web-production \ --network xiaoxia-net-production \ @@ -182,7 +176,8 @@ docker run -d \ --restart unless-stopped \ --cpus 0.5 \ --memory 512m \ - $LEGACY_VOLUME \ + -e APP_ENV=production \ + $WEB_VOLUMES \ --health-cmd "wget --spider -q http://127.0.0.1:80" \ --health-interval 30s \ --health-timeout 5s \ @@ -197,7 +192,6 @@ while [ "$i" -lt 40 ]; do if curl -sf --max-time 5 http://127.0.0.1:8001/health >/dev/null 2>&1; then echo "API is healthy!" break - fi i=$((i + 1)) echo " Waiting... ($i/40)" sleep 3 @@ -207,7 +201,6 @@ if [ "$i" -ge 40 ]; then echo "ERROR: API did not become healthy within 120s" docker logs --tail 50 xiaoxia-api-production exit 1 -fi # ---- 等待 Web 健康 ---- echo "Waiting for Web to become healthy..." @@ -216,7 +209,6 @@ while [ "$i" -lt 15 ]; do if curl -sf --max-time 5 http://127.0.0.1:3002/ >/dev/null 2>&1; then echo "Web is healthy!" break - fi i=$((i + 1)) echo " Waiting... ($i/15)" sleep 2 @@ -226,7 +218,6 @@ if [ "$i" -ge 15 ]; then echo "ERROR: Web did not become healthy within 30s" docker logs --tail 30 xiaoxia-web-production exit 1 -fi # ---- 清理旧镜像 ---- echo "Cleaning up old images..." diff --git a/infra/docker/deploy-staging-registry.sh b/infra/docker/deploy-staging-registry.sh index c5441d9f9..8db4a7956 100755 --- a/infra/docker/deploy-staging-registry.sh +++ b/infra/docker/deploy-staging-registry.sh @@ -124,23 +124,15 @@ docker run -d \ "$LOCAL_WORKER" # ---- 启动 Web ---- -# Web 镜像默认打包 production nginx.conf,staging 需要挂载 staging 配置 -NGINX_CONF="${NGINX_CONF:-${COMPOSE_DIR}/nginx-staging.conf}" -if [ ! -f "$NGINX_CONF" ]; then - echo "WARN: nginx config not found at $NGINX_CONF, using image default" - NGINX_VOLUME="" -else - NGINX_VOLUME="-v ${NGINX_CONF}:/etc/nginx/conf.d/default.conf:ro" -fi - echo "Starting Web container..." +docker rm -f xiaoxia-web-staging 2>/dev/null || true docker run -d \ --name xiaoxia-web-staging \ --network xiaoxia-net-staging \ -p 127.0.0.1:3001:80 \ --restart unless-stopped \ --label com.centurylinklabs.watchtower.enable=true \ - $NGINX_VOLUME \ + -e APP_ENV=staging \ --health-cmd "wget --spider -q http://127.0.0.1:80" \ --health-interval 30s \ --health-timeout 5s \ diff --git a/infra/docker/deploy-staging.sh b/infra/docker/deploy-staging.sh index 39039cc45..f160dc7e6 100755 --- a/infra/docker/deploy-staging.sh +++ b/infra/docker/deploy-staging.sh @@ -53,7 +53,6 @@ export API_IMAGE="${API_IMAGE:-${REGISTRY}/xiaoxia-saas-api:dev}" export WORKER_IMAGE="${WORKER_IMAGE:-${REGISTRY}/xiaoxia-saas-worker:dev}" # Use staging-specific nginx config (proxy_pass → xiaoxia-api-staging:8000) -export WEB_NGINX_CONF=infra/docker/nginx-staging.conf if [ "${REBUILD_BACKEND:-0}" = "1" ] || [ "${BUILD_WEB:-0}" = "1" ]; then if [ "${ALLOW_STAGING_BUILDS:-false}" != "true" ]; then diff --git a/infra/docker/nginx-entrypoint.sh b/infra/docker/nginx-entrypoint.sh new file mode 100755 index 000000000..88c25d45c --- /dev/null +++ b/infra/docker/nginx-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Select nginx config based on APP_ENV (staging/production). +# Both configs are baked into the image at well-known paths. +# nginx reads config only at startup, so symlink before exec. +set -e + +NGINX_CONF_DIR="/etc/nginx/conf.d" + +case "${APP_ENV:-production}" in + staging) + ln -sf /etc/nginx/nginx-staging.conf "$NGINX_CONF_DIR/default.conf" + ;; + *) + ln -sf /etc/nginx/nginx-production.conf "$NGINX_CONF_DIR/default.conf" + ;; +esac + +exec nginx -g "daemon off;" diff --git a/infra/docker/web-artifact.Dockerfile b/infra/docker/web-artifact.Dockerfile index 330199957..0a4138dfd 100644 --- a/infra/docker/web-artifact.Dockerfile +++ b/infra/docker/web-artifact.Dockerfile @@ -1,7 +1,11 @@ FROM git.xiaoxiajianji.com/xiaoxia/base/nginx:alpine AS runner -ARG NGINX_CONF=infra/docker/nginx.conf WORKDIR /usr/share/nginx/html COPY apps/web/dist ./ -COPY ${NGINX_CONF} /etc/nginx/conf.d/default.conf +# 将所有 nginx 配置烤入镜像,entrypoint 按 APP_ENV 选择 +COPY infra/docker/nginx.conf /etc/nginx/nginx-production.conf +COPY infra/docker/nginx-staging.conf /etc/nginx/nginx-staging.conf +COPY infra/docker/nginx-production.conf /etc/nginx/nginx-production.conf +COPY infra/docker/nginx-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/infra/docker/web.Dockerfile b/infra/docker/web.Dockerfile index 10cd2f15d..0c6728ead 100755 --- a/infra/docker/web.Dockerfile +++ b/infra/docker/web.Dockerfile @@ -28,9 +28,13 @@ RUN --mount=type=cache,target=/app/apps/web/.tscache,sharing=locked \ # Production stage with nginx FROM git.xiaoxiajianji.com/xiaoxia/base/nginx:alpine AS runner -ARG NGINX_CONF=infra/docker/nginx.conf WORKDIR /usr/share/nginx/html COPY --from=builder /app/apps/web/dist ./ -COPY ${NGINX_CONF} /etc/nginx/conf.d/default.conf +# 将所有 nginx 配置烤入镜像,entrypoint 按 APP_ENV 选择 +COPY infra/docker/nginx.conf /etc/nginx/nginx-production.conf +COPY infra/docker/nginx-staging.conf /etc/nginx/nginx-staging.conf +COPY infra/docker/nginx-production.conf /etc/nginx/nginx-production.conf +COPY infra/docker/nginx-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +ENTRYPOINT ["/docker-entrypoint.sh"] -- 2.54.0 From 100538a704fc487d5cd9fed8c1773db8e9077b82 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 11 Sep 2026 08:51:56 +0800 Subject: [PATCH 2/2] chore: remove empty if blocks after removing NGINX_CONF --- .gitea/workflows/ci-pipeline.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index df2528baa..dbae83666 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -827,8 +827,6 @@ jobs: CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:develop" EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\"" - if [ "${{ matrix.service }}" = "web" ]; then - fi # Worker 与 API/Web 统一走持久 builder(ci-builder-persist),共享宿主机层缓存 NO_CACHE_FLAG="" @@ -1025,8 +1023,6 @@ jobs: CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}" EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\"" - if [ "${{ matrix.service }}" = "web" ]; then - fi NO_CACHE_FLAG="" for i in 1 2 3; do @@ -1565,8 +1561,6 @@ jobs: CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:main" EXTRA_BUILD_ARGS="APP_VERSION=\"${TAG_NAME}\"" - if [ "${{ matrix.service }}" = "web" ]; then - fi # Docker build 带重试:失败自动重试2次,第2次重试加--no-cache NO_CACHE_FLAG="" -- 2.54.0