|
|
#!/usr/bin/env bash
|
|
|
# 宝塔宿主机 MySQL + Docker 应用 — 192.168.2.14:30327
|
|
|
# 用法(/home/project/yubao):
|
|
|
# bash deploy/install-bt-host-mysql.sh
|
|
|
# 若尚无 .env,会自动从 deploy/.env.bt-host-mysql.example 复制
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.bt-host-mysql.yml"
|
|
|
ENV_FILE="${ROOT_DIR}/.env"
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
GREEN='\033[0;32m'
|
|
|
YELLOW='\033[1;33m'
|
|
|
NC='\033[0m'
|
|
|
|
|
|
log() { echo -e "${GREEN}[deploy-bt]${NC} $*"; }
|
|
|
warn() { echo -e "${YELLOW}[deploy-bt]${NC} $*"; }
|
|
|
err() { echo -e "${RED}[deploy-bt]${NC} $*" >&2; }
|
|
|
|
|
|
read_env() {
|
|
|
local key="$1"
|
|
|
local line
|
|
|
line="$(grep -E "^[[:space:]]*${key}=" "$ENV_FILE" 2>/dev/null | tail -1 || true)"
|
|
|
if [[ -z "$line" ]]; then
|
|
|
echo ""
|
|
|
return
|
|
|
fi
|
|
|
line="${line#*=}"
|
|
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
|
line="${line%"${line##*[![:space:]]}"}"
|
|
|
if [[ "$line" == \"*\" && "$line" == *\" ]]; then
|
|
|
line="${line:1:${#line}-2}"
|
|
|
elif [[ "$line" == \'*\' && "$line" == *\' ]]; then
|
|
|
line="${line:1:${#line}-2}"
|
|
|
fi
|
|
|
echo "$line"
|
|
|
}
|
|
|
|
|
|
validate_env() {
|
|
|
local example="${ROOT_DIR}/deploy/.env.bt-host-mysql.example"
|
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
|
if [[ -f "$example" ]]; then
|
|
|
log "未找到 .env,从 deploy/.env.bt-host-mysql.example 自动复制"
|
|
|
cp "$example" "$ENV_FILE"
|
|
|
else
|
|
|
err "未找到 .env 且缺少 ${example}"
|
|
|
exit 1
|
|
|
fi
|
|
|
fi
|
|
|
|
|
|
DATABASE_URL="$(read_env DATABASE_URL)"
|
|
|
SESSION_SECRET="$(read_env SESSION_SECRET)"
|
|
|
APP_ADMIN_PASS="$(read_env APP_ADMIN_PASS)"
|
|
|
APP_OPS_PASS="$(read_env APP_OPS_PASS)"
|
|
|
APP_PORT="$(read_env APP_PORT)"
|
|
|
PUBLIC_HOST="$(read_env PUBLIC_HOST)"
|
|
|
MYSQL_USER="$(read_env MYSQL_USER)"
|
|
|
MYSQL_PASSWORD="$(read_env MYSQL_PASSWORD)"
|
|
|
|
|
|
local missing=0
|
|
|
for key in DATABASE_URL SESSION_SECRET APP_ADMIN_PASS APP_OPS_PASS MYSQL_USER MYSQL_PASSWORD; do
|
|
|
local val="${!key:-}"
|
|
|
if [[ -z "$val" ]] || [[ "$val" == CHANGE_ME* ]]; then
|
|
|
err "请在 .env 中填写: $key"
|
|
|
missing=1
|
|
|
fi
|
|
|
done
|
|
|
|
|
|
if [[ "$APP_ADMIN_PASS" == "$APP_OPS_PASS" ]]; then
|
|
|
err "APP_ADMIN_PASS 与 APP_OPS_PASS 不能相同"
|
|
|
missing=1
|
|
|
fi
|
|
|
|
|
|
for weak in admin123 ops123 password 123456; do
|
|
|
if [[ "$APP_ADMIN_PASS" == "$weak" || "$APP_OPS_PASS" == "$weak" ]]; then
|
|
|
err "生产环境禁止使用弱口令: $weak"
|
|
|
missing=1
|
|
|
fi
|
|
|
done
|
|
|
|
|
|
if [[ "$SESSION_SECRET" == *"change-me"* ]] || [[ ${#SESSION_SECRET} -lt 32 ]]; then
|
|
|
err "SESSION_SECRET 须至少 32 字符且非开发默认值"
|
|
|
missing=1
|
|
|
fi
|
|
|
|
|
|
if [[ $missing -ne 0 ]]; then
|
|
|
exit 1
|
|
|
fi
|
|
|
}
|
|
|
|
|
|
preflight_mysql() {
|
|
|
local db_user db_pass db_name
|
|
|
db_user="$(read_env MYSQL_USER)"
|
|
|
db_pass="$(read_env MYSQL_PASSWORD)"
|
|
|
db_name="$(read_env MYSQL_DATABASE)"
|
|
|
if [[ -z "$db_pass" ]]; then
|
|
|
return 0
|
|
|
fi
|
|
|
log "测试宝塔 MySQL 连接..."
|
|
|
if docker run --rm --add-host=host.docker.internal:host-gateway mysql:8 \
|
|
|
mysql -h host.docker.internal -u "${db_user}" -p"${db_pass}" -e "SELECT 1" "${db_name}" >/dev/null 2>&1; then
|
|
|
log "MySQL 连接正常"
|
|
|
return 0
|
|
|
fi
|
|
|
err "MySQL 认证失败:请核对 .env 中 MYSQL_PASSWORD / DATABASE_URL 是否与宝塔一致"
|
|
|
err "常见原因:"
|
|
|
err " 1) 宝塔用户未授权 Docker 网段(172.%)"
|
|
|
err " 2) 重新上传代码覆盖了 .env"
|
|
|
err "修复示例:"
|
|
|
err " MYSQL_PASSWORD=宝塔里 ${db_user} 用户的真实密码"
|
|
|
err " DATABASE_URL=mysql://${db_user}:真实密码@host.docker.internal:3306/${db_name}"
|
|
|
exit 1
|
|
|
}
|
|
|
|
|
|
deploy_docker() {
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
|
err "未安装 Docker"
|
|
|
exit 1
|
|
|
fi
|
|
|
if ! docker compose version >/dev/null 2>&1; then
|
|
|
err "需要 Docker Compose V2"
|
|
|
exit 1
|
|
|
fi
|
|
|
|
|
|
mkdir -p "${ROOT_DIR}/data"
|
|
|
|
|
|
cd "$ROOT_DIR"
|
|
|
local compose=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE")
|
|
|
|
|
|
log "构建并启动(无容器 MySQL,连接宝塔 MySQL)..."
|
|
|
export DOCKER_BUILDKIT=1
|
|
|
export BUILDKIT_MAX_PARALLELISM=1
|
|
|
"${compose[@]}" build web
|
|
|
"${compose[@]}" up -d
|
|
|
|
|
|
log "同步数据库结构(prisma db push)..."
|
|
|
"${compose[@]}" run --rm --no-deps web pnpm exec prisma db push --skip-generate
|
|
|
|
|
|
log "同步默认账号(admin / ops)..."
|
|
|
"${compose[@]}" run --rm --no-deps web pnpm db:seed
|
|
|
|
|
|
log "容器状态:"
|
|
|
"${compose[@]}" ps
|
|
|
}
|
|
|
|
|
|
smoke_check() {
|
|
|
local port="${APP_PORT:-30327}"
|
|
|
local url="http://127.0.0.1:${port}/api/health"
|
|
|
log "健康检查: ${url}"
|
|
|
local body
|
|
|
body="$(curl -sf "$url" 2>/dev/null || true)"
|
|
|
if [[ "$body" == \{* ]]; then
|
|
|
log "Web 服务正常"
|
|
|
else
|
|
|
warn "Web 暂未响应,请查看:docker compose -f deploy/docker-compose.bt-host-mysql.yml logs -f web"
|
|
|
fi
|
|
|
}
|
|
|
|
|
|
print_summary() {
|
|
|
local port="${APP_PORT:-30327}"
|
|
|
local host="${PUBLIC_HOST:-<服务器内网IP>}"
|
|
|
echo ""
|
|
|
log "========== 部署完成(宝塔 MySQL 模式)=========="
|
|
|
echo " 内网访问: http://${host}:${port}"
|
|
|
echo " 登录页: http://${host}:${port}/login"
|
|
|
echo " 邮件列表: http://${host}:${port}/mails"
|
|
|
echo " 设置页: http://${host}:${port}/settings"
|
|
|
echo ""
|
|
|
echo " 常用命令:"
|
|
|
echo " docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml logs -f worker"
|
|
|
echo " docker compose --env-file .env -f deploy/docker-compose.bt-host-mysql.yml restart"
|
|
|
echo ""
|
|
|
warn "首次登录后请立即修改 admin/ops 密码(.env 中 APP_ADMIN_PASS / APP_OPS_PASS)"
|
|
|
warn "CC / IMAP 推荐在 Admin「设置」页配置,不必改代码"
|
|
|
}
|
|
|
|
|
|
main() {
|
|
|
validate_env
|
|
|
preflight_mysql
|
|
|
deploy_docker
|
|
|
smoke_check
|
|
|
print_summary
|
|
|
}
|
|
|
|
|
|
main "$@"
|