refactor(deploy): 去除内置 nginx,各服务暴露宿主机端口,末尾输出 nginx 配置参考

私有化部署场景下用户通常有自己的 nginx,内置 nginx 容器会占用 80/443
与宿主机冲突,且域名/HTTPS 交互配置对用户是噪音。

- docker-compose.yml: nginx 容器改为 profile=nginx-bundled(默认不启动)
  各业务容器增加 ports 暴露宿主机端口(9001/8086/8082/8083/8084/8085/8080/8081)
- deploy.sh: 去除 域名/HTTPS/certbot 交互,改为询问一个外部访问地址(用于 SDK 配置)
  健康检查和 import API 直接打 127.0.0.1:9001,不再依赖 nginx 容器
  末尾输出端口表和可直接复制的 nginx location 配置参考

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-05-20 15:30:55 +08:00
父节点 60aeb61433
当前提交 2a250e79a0
共有 2 个文件被更改,包括 91 次插入246 次删除

查看文件

@ -17,6 +17,8 @@ services:
tenant-service:
image: ${REGISTRY}/tenant-service:${IMAGE_TAG}
profiles: ["base"]
ports:
- "9001:9001"
env_file:
- ./config/xuqm.env # 业务配置:运行模式、域名、功能开关
- ./config/secrets.env # 敏感配置密码、Token
@ -40,6 +42,8 @@ services:
file-service:
image: ${REGISTRY}/file-service:${IMAGE_TAG}
profiles: ["base"]
ports:
- "8086:8086"
env_file:
- ./config/xuqm.env
- ./config/secrets.env
@ -63,6 +67,8 @@ services:
tenant-web:
image: ${REGISTRY}/tenant-web:${IMAGE_TAG}
profiles: ["base"]
ports:
- "8080:80"
restart: unless-stopped
# ---------------------------------------------------------------------------
@ -72,19 +78,21 @@ services:
ops-web:
image: ${REGISTRY}/ops-web:${IMAGE_TAG}
profiles: ["base"]
ports:
- "8081:80"
restart: unless-stopped
# ---------------------------------------------------------------------------
# Nginx 反向代理(必须
# 统一入口:端口 80HTTP和 443HTTPS
# 路由所有请求到各后端容器
# Nginx 反向代理(可选,profile: nginx-bundled
# 默认不启动 — 用户通常用宿主机自己的 nginx 代理到各服务端口。
# 需要内置 nginx 时COMPOSE_PROFILES=...,nginx-bundled
# ---------------------------------------------------------------------------
nginx:
image: nginx:1.27-alpine
profiles: ["base"]
profiles: ["nginx-bundled"]
ports:
- "80:80" # HTTP
- "443:443" # HTTPS需要配置证书,见 docs/runbook.md
- "80:80"
- "443:443"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/nginx/conf.d:/etc/nginx/conf.d:ro
@ -105,6 +113,8 @@ services:
im-service:
image: ${REGISTRY}/im-service:${IMAGE_TAG}
profiles: ["im"]
ports:
- "8082:8082"
env_file:
- ./config/xuqm.env
- ./config/secrets.env
@ -128,6 +138,8 @@ services:
push-service:
image: ${REGISTRY}/push-service:${IMAGE_TAG}
profiles: ["push"]
ports:
- "8083:8083"
env_file:
- ./config/xuqm.env
- ./config/secrets.env
@ -146,6 +158,8 @@ services:
update-service:
image: ${REGISTRY}/update-service:${IMAGE_TAG}
profiles: ["update"]
ports:
- "8084:8084"
env_file:
- ./config/xuqm.env
- ./config/secrets.env
@ -167,6 +181,8 @@ services:
license-service:
image: ${REGISTRY}/license-service:${IMAGE_TAG}
profiles: ["license"]
ports:
- "8085:8085"
env_file:
- ./config/xuqm.env
- ./config/secrets.env

查看文件

@ -40,14 +40,10 @@ REGISTRY_USER="xuqinmin12"
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-xuqinmin1022}"
IMAGE_TAG="latest"
# 部署主机(用于健康检查 HTTP 请求)
# 可以是 IP 地址,无需域名,局域网内即可完整使用所有服务
# 部署主机(用于健康检查 HTTP 请求,直接打服务端口)
DEPLOY_HOST="${DEPLOY_HOST:-localhost}"
CONSOLE_BASE="http://${DEPLOY_HOST}"
_HTTP_SCHEME="http"
_WS_SCHEME="ws"
DEPLOY_DOMAIN=""
USE_HTTPS=false
# MySQLmanaged 模式,由 Docker 容器托管)
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-XuqmRoot@2026}"
@ -66,7 +62,7 @@ PUBLIC_PLATFORM_URL="https://dev.xuqinmin.com"
# ---------------------------------------------------------------------------
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STEP=0
TOTAL_STEPS=8
TOTAL_STEPS=7
# ---------------------------------------------------------------------------
# 工具函数
@ -94,8 +90,6 @@ printf '\n\033[1;35m════════════════════
printf '\033[1;35m XuqmGroup 私有化一键部署脚本(全量部署)\033[0m\n'
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
printf ' 部署目录: %s\n' "$ROOT_DIR"
printf ' 目标主机: %s\n' "$DEPLOY_HOST"
printf ' 访问地址: %s\n' "$CONSOLE_BASE"
printf ' 镜像仓库: %s\n' "$REGISTRY"
printf ' 服务集: base + im + push + update + license\n\n'
@ -186,38 +180,28 @@ else
fi
# ---------------------------------------------------------------------------
# 0c. 域名 / HTTPS 配置(交互式
# 0c. 外部访问地址(用于 SDK 配置,填写用户自己 nginx 代理后的地址
# ---------------------------------------------------------------------------
printf '\n\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf '\033[1;33m 访问域名配置(可选)\033[0m\n'
printf '\033[1;33m 外部访问地址\033[0m\n'
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf ' 默认使用 localhost适合本机或局域网访问\n'
printf ' 如有公网域名,可在此配置并自动申请 HTTPS 证书\n\n'
printf ' 填写您的 nginx 对外暴露的地址,SDK 客户端将使用此地址。\n'
printf ' http://192.168.1.100 或 https://xuqm.example.com\n\n'
read -rp " 是否使用自定义域名?[y/N]: " _domain_yn
if [[ "${_domain_yn:-N}" =~ ^[Yy]$ ]]; then
while [ -z "$DEPLOY_DOMAIN" ]; do
read -rp " 请输入域名(如 xuqm.example.com,不含 http://: " DEPLOY_DOMAIN
if ! printf '%s' "$DEPLOY_DOMAIN" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$'; then
warn "域名格式不正确,请重新输入(例: xuqm.example.com"
DEPLOY_DOMAIN=""
_EXT_BASE=""
while [ -z "$_EXT_BASE" ]; do
read -rp " 外部访问地址: " _EXT_BASE
if ! printf '%s' "$_EXT_BASE" | grep -qE '^https?://'; then
warn "请以 http:// 或 https:// 开头"
_EXT_BASE=""
fi
done
DEPLOY_HOST="$DEPLOY_DOMAIN"
read -rp " 是否配置 HTTPS需域名已解析到本机,将自动申请 Let's Encrypt 证书)?[y/N]: " _https_yn
if [[ "${_https_yn:-N}" =~ ^[Yy]$ ]]; then
USE_HTTPS=true
_HTTP_SCHEME="https"
CONSOLE_BASE="${_EXT_BASE%/}"
if printf '%s' "$CONSOLE_BASE" | grep -q '^https://'; then
_WS_SCHEME="wss"
CONSOLE_BASE="https://${DEPLOY_DOMAIN}"
else
CONSOLE_BASE="http://${DEPLOY_DOMAIN}"
fi
ok "域名: ${DEPLOY_DOMAIN},协议: ${_HTTP_SCHEME}"
else
ok "使用默认地址: ${CONSOLE_BASE}"
fi
DEPLOY_HOST="$(printf '%s' "$CONSOLE_BASE" | sed 's|https\?://||')"
ok "外部地址: ${CONSOLE_BASE}"
# ---------------------------------------------------------------------------
# Step 1 — 预检
@ -264,7 +248,7 @@ REGISTRY_USER=${REGISTRY_USER}
REGISTRY_PASSWORD=${REGISTRY_PASSWORD}
IMAGE_TAG=${IMAGE_TAG}
# 启用全量服务(base + 基础设施 + im + push + update + license
# 启用全量服务(nginx 容器默认不启动,用户自行配置宿主机 nginx
COMPOSE_PROFILES=base,infra-mysql,infra-redis,im,push,update,license
# MySQLmanaged 模式,Docker 容器托管)
@ -374,43 +358,11 @@ EOF
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
ok "config/tenant/bootstrap.env 已写入 (chmod 600)"
# config/nginx/conf.d/xuqm.conf
mkdir -p "$ROOT_DIR/config/nginx/conf.d"
NGINX_CONF_PATH="$ROOT_DIR/config/nginx/conf.d/xuqm.conf"
if [ ! -f "$NGINX_CONF_PATH" ]; then
fail "nginx 配置文件不存在: $NGINX_CONF_PATH(请确保在仓库根目录运行本脚本)"
fi
# nginx 容器默认不启动,用户使用宿主机自己的 nginx 代理到各服务端口)
if $USE_HTTPS; then
# ── HTTPS 模式:安装 certbot → 申请证书 → 生成 SSL nginx 配置 → compose override ──
# 安装 certbot若未安装
if ! command -v certbot >/dev/null 2>&1; then
info "安装 certbot ..."
if [ "$(command -v apt-get)" ]; then
apt-get install -y -qq certbot 2>/dev/null || fail "certbot 安装失败,请手动执行: apt install certbot"
elif [ "$(command -v yum)" ]; then
yum install -y -q certbot 2>/dev/null || fail "certbot 安装失败,请手动执行: yum install certbot"
else
fail "无法自动安装 certbot,请手动安装后重新运行"
fi
fi
ok "certbot $(certbot --version 2>&1 | head -1)"
# 申请 Let's Encrypt 证书standalone,此时 nginx 尚未启动)
CERT_DIR="/etc/letsencrypt/live/${DEPLOY_DOMAIN}"
if [ -f "${CERT_DIR}/fullchain.pem" ]; then
ok "证书已存在,跳过申请: ${CERT_DIR}"
else
info "申请 Let's Encrypt 证书(域名: ${DEPLOY_DOMAIN},需要端口 80 可达)..."
certbot certonly --standalone -d "$DEPLOY_DOMAIN" \
--agree-tos --register-unsafely-without-email --non-interactive \
|| fail "证书申请失败,请确认域名 ${DEPLOY_DOMAIN} 已解析到本机且端口 80 可访问"
ok "证书已申请: ${CERT_DIR}"
fi
# 生成包含 SSL 的 nginx 配置(覆盖仓库静态文件)
cat > "$NGINX_CONF_PATH" << NGINX_CONF
if false; then
# 以下为内置 nginx 配置示例,仅在 COMPOSE_PROFILES 包含 nginx-bundled 时使用
cat > /dev/null << NGINX_CONF
# =============================================================================
# XuqmGroup 私有化部署 — Nginx HTTPS 配置(由 deploy.sh 自动生成)
# 域名: ${DEPLOY_DOMAIN}
@ -543,156 +495,8 @@ server {
}
}
NGINX_CONF
ok "nginx HTTPS 配置已生成(${DEPLOY_DOMAIN}"
# docker-compose override挂载 /etc/letsencrypt 到 nginx 容器
cat > "$ROOT_DIR/docker-compose.override.yml" << 'OVERRIDE_YAML'
# 由 deploy.sh 自动生成 — HTTPS 模式需要挂载 Let's Encrypt 证书
services:
nginx:
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
OVERRIDE_YAML
ok "docker-compose.override.yml 已生成SSL 证书挂载)"
else
ok "config/nginx/conf.d/xuqm.conf 就绪HTTP 模式,使用仓库内文件)"
fi
# (以下 heredoc 为静态配置的备份注释,不执行)
: <<'NGINX_CONF'
# =============================================================================
# XuqmGroup 私有化部署 — Nginx 路由配置
#
# 服务端口映射:
# tenant-service 9001 /api/*(核心 API、/actuator/*
# file-service 8086 /file/*(文件上传下载)
# im-service 8082 /api/im/*IM HTTP、/ws/im/*WebSocket
# update-service 8084 /api/v1/updates/*、/api/v1/rn/*
# push-service 8083 厂商回调(内部端口)
# license-service 8085 内部调用
# ops-web 80 /ops/*(运营后台)
# tenant-web 80 /*(控制台,兜底路由)
# =============================================================================
server {
listen 80;
server_name _;
# 强制 UTF-8 编码,防止中文乱码
charset utf-8;
# 最大上传文件大小(文件服务单独设置 500m
client_max_body_size 100m;
# ----------- 健康检查 -----------
location /health {
return 200 "ok\n";
add_header Content-Type text/plain;
}
# ----------- 版本管理服务update-service:8084-----------
# 注意:必须在通用 /api/ 之前声明,否则会被错误路由到 tenant-service
location /api/v1/updates/ {
proxy_pass http://update-service:8084/api/v1/updates/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
# RN 热更新包下载和列表
location /api/v1/rn/ {
proxy_pass http://update-service:8084/api/v1/rn/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 120s;
}
# ----------- IM 服务im-service:8082-----------
# 注意:必须在通用 /api/ 之前声明
location /api/im/ {
proxy_pass http://im-service:8082/api/im/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
# IM WebSocket 长连接(客户端消息收发)
location /ws/im/ {
proxy_pass http://im-service:8082/ws/im/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s; # WebSocket 保持 1 小时
}
# ----------- License 服务license-service:8085-----------
# 注意:必须在通用 /api/ 之前声明
location /api/license/ {
proxy_pass http://license-service:8085/api/license/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
# ----------- 核心 APItenant-service:9001-----------
# 注意tenant-service 运行在 9001 端口(不是 8080
location /api/ {
proxy_pass http://tenant-service:9001/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
# Spring Boot Actuator 健康检查(内部监控用)
location /actuator/ {
proxy_pass http://tenant-service:9001/actuator/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ----------- 文件服务file-service:8086-----------
location /file/ {
proxy_pass http://file-service:8086/file/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# ----------- 文档站tenant-web 内置,VitePress base=/docs/-----------
location /docs/ {
proxy_pass http://tenant-web:80/docs/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ----------- 运营后台ops-web:80-----------
location /ops {
proxy_pass http://ops-web:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ----------- 控制台前端tenant-web:80-----------
# 兜底路由,必须放最后
location / {
proxy_pass http://tenant-web:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
NGINX_CONF
# config/vendors/push.env — 推送服务厂商凭据(初始为关闭状态,按需开启)
mkdir -p "$ROOT_DIR/config/vendors"
if [ ! -f "$ROOT_DIR/config/vendors/push.env" ]; then
@ -850,11 +654,11 @@ docker compose \
--profile im --profile push --profile update --profile license \
up -d
# 等待 tenant-service 健康(最多 120 秒
# 等待 tenant-service 健康(直接打 9001 端口,不经过 nginx
printf ' 等待 tenant-service 启动'
for i in $(seq 1 40); do
code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
"http://${DEPLOY_HOST}/actuator/health" 2>/dev/null || echo 000)"
"http://127.0.0.1:9001/actuator/health" 2>/dev/null || echo 000)"
if [ "$code" = "200" ]; then
printf '\n'
ok "tenant-service 健康 (HTTP 200)"
@ -1037,7 +841,7 @@ print(json.dumps(resp['data']))
-H "Content-Type: application/json" \
--data-binary @- \
-w "\n__HTTP_STATUS__:%{http_code}" \
"http://127.0.0.1/api/private/deployment/migrate/import")
"http://127.0.0.1:9001/api/private/deployment/migrate/import")
_IMPORT_STATUS=$(printf '%s' "$_IMPORT_RESP" | grep -o '__HTTP_STATUS__:[0-9]*' | cut -d: -f2)
_IMPORT_BODY=$(printf '%s' "$_IMPORT_RESP" | sed 's/__HTTP_STATUS__:[0-9]*//')
if [ "${_IMPORT_STATUS}" != "200" ]; then
@ -1106,9 +910,6 @@ fi
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
printf '\033[1;35m XuqmGroup 私有化部署完成\033[0m\n'
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
printf '\n \033[1m访问地址\033[0m %s\n' "${CONSOLE_BASE}"
printf ' \033[1m运营后台\033[0m %s/ops\n' "${CONSOLE_BASE}"
printf ' \033[1m文档站 \033[0m %s/docs/\n' "${CONSOLE_BASE}"
printf '\n \033[1m租户信息\033[0m\n'
printf ' 邮箱: %s\n' "${DEPLOY_TENANT_EMAIL}"
printf ' 用户名: %s\n' "${DEPLOY_TENANT_USERNAME}"
@ -1117,15 +918,43 @@ if [ "$DEPLOY_MODE" = "new" ]; then
else
printf ' 密码: 同生产平台密码(原样迁移,未重置)\n'
fi
printf '\n \033[1m初始化方式\033[0m%s\n' "${DEPLOY_MODE}"
printf '\n \033[1m服务状态\033[0m\n'
printf ' 控制台 %s\n' "${CONSOLE_BASE}"
printf ' IM 服务 %s/api/im/\n' "${CONSOLE_BASE}"
printf ' 版本管理 %s/api/v1/updates/\n' "${CONSOLE_BASE}"
printf ' 文件服务 %s/file/\n' "${CONSOLE_BASE}"
printf ' License 服务 已启动(内部调用)\n'
printf ' 推送服务 已启动(厂商凭据见 config/vendors/push.env\n'
printf '\n \033[1m容器端口请在您的 nginx 中配置代理):\033[0m\n'
printf ' 控制台前端 127.0.0.1:8080 → 代理 /\n'
printf ' 运营后台 127.0.0.1:8081 → 代理 /ops\n'
printf ' 核心 API 127.0.0.1:9001 → 代理 /api/ /actuator/\n'
printf ' 文件服务 127.0.0.1:8086 → 代理 /file/ /api/file/\n'
printf ' IM 服务 127.0.0.1:8082 → 代理 /api/im/ /ws/im\n'
printf ' 版本管理 127.0.0.1:8084 → 代理 /api/v1/updates/ /api/v1/rn/\n'
printf ' License 服务 127.0.0.1:8085 → 代理 /api/license/\n'
printf ' 推送服务 127.0.0.1:8083 (厂商回调,按需代理)\n'
printf '\n \033[1mNginx 配置参考(复制到您的 nginx server 块):\033[0m\n'
printf '\033[0;37m'
cat <<'NGINX_REF'
charset utf-8;
client_max_body_size 100m;
location /api/v1/updates/ { proxy_pass http://127.0.0.1:8084/api/v1/updates/; }
location /api/v1/rn/ { proxy_pass http://127.0.0.1:8084/api/v1/rn/; }
location /api/im/ { proxy_pass http://127.0.0.1:8082/api/im/; }
location /ws/im {
proxy_pass http://127.0.0.1:8082/ws/im;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
location /api/license/ { proxy_pass http://127.0.0.1:8085/api/license/; }
location /file/ {
proxy_pass http://127.0.0.1:8086/file/;
client_max_body_size 500m;
proxy_read_timeout 300s;
}
location /api/ { proxy_pass http://127.0.0.1:9001/api/; }
location /actuator/ { proxy_pass http://127.0.0.1:9001/actuator/; }
location /ops { proxy_pass http://127.0.0.1:8081; }
location / { proxy_pass http://127.0.0.1:8080; }
NGINX_REF
printf '\033[0m'
printf '\n \033[1m部署目录\033[0m %s\n' "$ROOT_DIR"
printf ' \033[1m验证结果\033[0m %s/.deploy-state/last-verify.json\n' "$ROOT_DIR"
printf ' \033[1m审计日志\033[0m %s/logs/audit.log\n' "$ROOT_DIR"
printf '\n\033[1;32m 部署成功!\033[0m\n\n'
printf '\n\033[1;32m 部署成功!配置好 nginx 后即可访问:%s\033[0m\n\n' "${CONSOLE_BASE}"