一键部署测试
This commit is contained in:
+23
-3
@@ -29,13 +29,33 @@ STORAGE_SECRET_ACCESS_KEY
|
||||
|
||||
如果正式对外运营,还需要配置真实短信和实名服务,避免继续使用测试配置。
|
||||
|
||||
## 2. 启动服务
|
||||
## 2. 一键部署
|
||||
|
||||
```bash
|
||||
./scripts/deploy-prod.sh
|
||||
```
|
||||
|
||||
脚本会自动完成:
|
||||
|
||||
- 检查 `backend/.env` 是否仍指向 `127.0.0.1`、`localhost` 或占位值。
|
||||
- 构建并启动生产容器。
|
||||
- 等待 MySQL、Redis、MinIO 就绪。
|
||||
- 按顺序执行尚未应用的数据库迁移。
|
||||
- 重启后端并检查 `http://127.0.0.1:7890/api/health`。
|
||||
|
||||
如需部署完成后直接跟随查看后端日志:
|
||||
|
||||
```bash
|
||||
./scripts/deploy-prod.sh --logs
|
||||
```
|
||||
|
||||
## 3. 手动启动服务
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
## 3. 首次初始化数据库
|
||||
## 4. 手动初始化数据库
|
||||
|
||||
将命令中的 `change-hfb-password` 替换为 `backend/.env` 里的 `MYSQL_PASSWORD`。
|
||||
|
||||
@@ -46,7 +66,7 @@ for file in backend/migrations/*.sql; do
|
||||
done
|
||||
```
|
||||
|
||||
## 4. 验证
|
||||
## 5. 验证
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:7890/health
|
||||
|
||||
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.prod.yml"
|
||||
BACKEND_ENV="${ROOT_DIR}/backend/.env"
|
||||
MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations"
|
||||
HEALTH_URL="${PROD_HEALTH_URL:-http://127.0.0.1:7890/api/health}"
|
||||
PUBLIC_URL="${PROD_PUBLIC_URL:-http://hfb.221329.cc.cd:7890}"
|
||||
READY_TIMEOUT="${PROD_READY_TIMEOUT:-120}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
printf "${BLUE}[deploy]${NC} %s\n" "$*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
printf "${GREEN}[deploy]${NC} %s\n" "$*"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
printf "${YELLOW}[deploy]${NC} %s\n" "$*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
printf "${RED}[deploy]${NC} %s\n" "$*" >&2
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令:$1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "${BACKEND_ENV}"
|
||||
}
|
||||
|
||||
require_env() {
|
||||
local key="$1"
|
||||
local value
|
||||
value="$(env_value "${key}")"
|
||||
if [[ -z "${value}" ]]; then
|
||||
log_error "backend/.env 缺少或未填写 ${key}"
|
||||
exit 1
|
||||
fi
|
||||
printf "%s" "${value}"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
用法: ./scripts/deploy-prod.sh [选项]
|
||||
|
||||
选项:
|
||||
--no-build 不重新构建镜像,仅启动已有镜像
|
||||
--no-migrate 不执行数据库迁移
|
||||
--logs 部署完成后跟随查看后端日志
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
环境变量:
|
||||
PROD_HEALTH_URL=http://127.0.0.1:7890/api/health
|
||||
部署后健康检查地址
|
||||
PROD_PUBLIC_URL=http://hfb.221329.cc.cd:7890
|
||||
输出给用户看的访问地址
|
||||
PROD_READY_TIMEOUT=120
|
||||
等待服务就绪秒数
|
||||
|
||||
示例:
|
||||
./scripts/deploy-prod.sh
|
||||
./scripts/deploy-prod.sh --no-build
|
||||
./scripts/deploy-prod.sh --logs
|
||||
EOF
|
||||
}
|
||||
|
||||
NO_BUILD=0
|
||||
NO_MIGRATE=0
|
||||
FOLLOW_LOGS=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-build)
|
||||
NO_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--no-migrate)
|
||||
NO_MIGRATE=1
|
||||
shift
|
||||
;;
|
||||
--logs)
|
||||
FOLLOW_LOGS=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "未知选项:$1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
validate_env() {
|
||||
if [[ ! -f "${BACKEND_ENV}" ]]; then
|
||||
log_error "未找到 backend/.env,请先执行:cp backend/.env.prod.example backend/.env 并填写生产配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local app_env mysql_dsn redis_addr storage_endpoint
|
||||
app_env="$(require_env APP_ENV)"
|
||||
mysql_dsn="$(require_env MYSQL_DSN)"
|
||||
redis_addr="$(require_env REDIS_ADDR)"
|
||||
storage_endpoint="$(require_env STORAGE_ENDPOINT)"
|
||||
require_env MYSQL_DATABASE >/dev/null
|
||||
require_env MYSQL_USER >/dev/null
|
||||
require_env MYSQL_PASSWORD >/dev/null
|
||||
require_env JWT_SECRET >/dev/null
|
||||
require_env MINIO_ROOT_USER >/dev/null
|
||||
require_env MINIO_ROOT_PASSWORD >/dev/null
|
||||
require_env STORAGE_ACCESS_KEY_ID >/dev/null
|
||||
require_env STORAGE_SECRET_ACCESS_KEY >/dev/null
|
||||
|
||||
if [[ "${app_env}" != "production" ]]; then
|
||||
log_warn "APP_ENV 当前是 ${app_env},生产部署建议改为 production"
|
||||
fi
|
||||
|
||||
if [[ "${mysql_dsn}" == *"127.0.0.1"* || "${mysql_dsn}" == *"localhost"* ]]; then
|
||||
log_error "MYSQL_DSN 仍指向本机,请改为 tcp(mysql:3306)"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${redis_addr}" == "127.0.0.1:"* || "${redis_addr}" == "localhost:"* ]]; then
|
||||
log_error "REDIS_ADDR 仍指向本机,请改为 redis:6379"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${storage_endpoint}" == *"127.0.0.1"* || "${storage_endpoint}" == *"localhost"* ]]; then
|
||||
log_error "STORAGE_ENDPOINT 仍指向本机,请改为 http://minio:9000"
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eq '=(change-|保持你的|你的)' "${BACKEND_ENV}"; then
|
||||
log_error "backend/.env 里还有占位值,请先替换后再部署"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "${COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
wait_service_healthy() {
|
||||
local service="$1"
|
||||
local label="$2"
|
||||
local attempt
|
||||
|
||||
log "等待 ${label} 就绪..."
|
||||
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
||||
local container_id status
|
||||
container_id="$(compose ps -q "${service}" 2>/dev/null || true)"
|
||||
if [[ -n "${container_id}" ]]; then
|
||||
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
|
||||
if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
|
||||
log_success "${label} 已就绪"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log_error "${label} 启动超时,请执行:docker compose -f deploy/docker-compose.prod.yml logs ${service}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
mysql_exec() {
|
||||
local password
|
||||
password="$(require_env MYSQL_PASSWORD)"
|
||||
compose exec -T -e MYSQL_PWD="${password}" mysql mysql -u"$(require_env MYSQL_USER)" "$@"
|
||||
}
|
||||
|
||||
mysql_scalar() {
|
||||
mysql_exec -N -s "$(require_env MYSQL_DATABASE)" -e "$1"
|
||||
}
|
||||
|
||||
ensure_migration_table() {
|
||||
mysql_exec "$(require_env MYSQL_DATABASE)" <<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version VARCHAR(255) PRIMARY KEY,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
SQL
|
||||
}
|
||||
|
||||
bootstrap_existing_schema_history() {
|
||||
local migration_count users_exists system_configs_exists
|
||||
migration_count="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations;")"
|
||||
users_exists="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'users';")"
|
||||
system_configs_exists="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'system_configs';")"
|
||||
|
||||
if [[ "${migration_count}" == "0" && "${users_exists}" != "0" && "${system_configs_exists}" != "0" ]]; then
|
||||
log_warn "检测到已有表结构但没有迁移记录,将当前 migration 文件标记为已应用"
|
||||
local file version
|
||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
||||
version="$(basename "${file}" .sql)"
|
||||
mysql_exec "$(require_env MYSQL_DATABASE)" -e \
|
||||
"INSERT IGNORE INTO schema_migrations (version) VALUES ('${version}');"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
if [[ "${NO_MIGRATE}" == "1" ]]; then
|
||||
log_warn "已跳过数据库迁移"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "执行数据库迁移..."
|
||||
ensure_migration_table
|
||||
bootstrap_existing_schema_history
|
||||
|
||||
local file version applied
|
||||
shopt -s nullglob
|
||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
||||
version="$(basename "${file}" .sql)"
|
||||
applied="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations WHERE version = '${version}';")"
|
||||
if [[ "${applied}" != "0" ]]; then
|
||||
log "跳过已应用迁移:${version}"
|
||||
continue
|
||||
fi
|
||||
|
||||
log "导入迁移:${version}"
|
||||
mysql_exec "$(require_env MYSQL_DATABASE)" < "${file}"
|
||||
mysql_exec "$(require_env MYSQL_DATABASE)" -e \
|
||||
"INSERT INTO schema_migrations (version) VALUES ('${version}');"
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
log_success "数据库迁移完成"
|
||||
}
|
||||
|
||||
wait_http_ready() {
|
||||
local attempt
|
||||
log "等待接口健康检查通过..."
|
||||
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
||||
if curl -fsS --max-time 3 "${HEALTH_URL}" >/dev/null 2>&1; then
|
||||
log_success "健康检查通过:${HEALTH_URL}"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log_error "健康检查超时:${HEALTH_URL}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "${ROOT_DIR}"
|
||||
need_cmd docker
|
||||
need_cmd curl
|
||||
validate_env
|
||||
|
||||
log "检查 Docker Compose 配置..."
|
||||
compose config >/dev/null
|
||||
|
||||
if [[ "${NO_BUILD}" == "1" ]]; then
|
||||
log "启动生产服务..."
|
||||
compose up -d
|
||||
else
|
||||
log "构建并启动生产服务..."
|
||||
compose up -d --build
|
||||
fi
|
||||
|
||||
wait_service_healthy mysql "MySQL"
|
||||
wait_service_healthy redis "Redis"
|
||||
wait_service_healthy minio "MinIO"
|
||||
run_migrations
|
||||
|
||||
log "重启后端以加载最新数据库状态..."
|
||||
compose up -d --no-deps --force-recreate backend
|
||||
wait_service_healthy backend "后端"
|
||||
wait_http_ready
|
||||
|
||||
log_success "部署完成:${PUBLIC_URL}"
|
||||
log "常用命令:docker compose -f deploy/docker-compose.prod.yml logs -f backend"
|
||||
|
||||
if [[ "${FOLLOW_LOGS}" == "1" ]]; then
|
||||
compose logs -f backend
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user