diff --git a/Makefile b/Makefile index ed926367..b266f3eb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset +.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset validate-release-config DEV_DIR := .dev DEV_SERVER_PID := $(DEV_DIR)/server.pid @@ -142,3 +142,6 @@ db-reset: ## 重置数据库 docker compose down -v --remove-orphans docker compose up -d --wait --remove-orphans postgres cd server && ./mvnw flyway:migrate -pl skillhub-app + +validate-release-config: ## 校验发布环境变量文件(默认 .env.release) + ./scripts/validate-release-config.sh .env.release diff --git a/README.md b/README.md index 85200f70..ef98d955 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Recommended image tags: Start the runtime: ```bash +make validate-release-config docker compose --env-file .env.release -f compose.release.yml up -d ``` @@ -152,6 +153,7 @@ Recommended production baseline: - keep PostgreSQL / Redis bound to `127.0.0.1` - use external S3 / OSS via `SKILLHUB_STORAGE_S3_*` - rotate or disable the bootstrap admin after initial setup +- run `make validate-release-config` before `docker compose up -d` If the GHCR package remains private, run `docker login ghcr.io` before `docker compose up -d`. diff --git a/docs/09-deployment.md b/docs/09-deployment.md index 1d0fe04a..8cb8110d 100644 --- a/docs/09-deployment.md +++ b/docs/09-deployment.md @@ -96,6 +96,7 @@ make dev-all-reset ```bash cp .env.release.example .env.release +make validate-release-config docker compose --env-file .env.release -f compose.release.yml up -d ``` @@ -114,6 +115,9 @@ docker compose --env-file .env.release -f compose.release.yml up -d - `.env.release.example` - 运行时变量模板 - 包含镜像名、镜像版本、端口、数据库凭证、外部 OSS、站点公网地址和首登管理员参数 +- `scripts/validate-release-config.sh` + - 在启动前校验 `.env.release` + - 可提前拦截占位值、URL 格式错误、缺失的 OSS 凭据、危险的明文默认值 ### 5.3 镜像标签约定 @@ -168,7 +172,32 @@ docker compose --env-file .env.release -f compose.release.yml up -d - 前端反代和运行时 API 地址通过 `SKILLHUB_API_UPSTREAM` / `SKILLHUB_WEB_API_BASE_URL` 注入 - 如果要开放真实登录,再补充 `OAUTH2_GITHUB_CLIENT_ID` / `OAUTH2_GITHUB_CLIENT_SECRET` -## 8 可观测性 +## 8 裸金属上线清单 + +推荐顺序: + +1. 准备服务器基础环境 + - 安装 Docker Engine 与 Docker Compose Plugin + - 配置公网 HTTPS 入口,确保最终访问域名已经确定 + - 打开 `80` / `443`,避免直接暴露 `5432` / `6379` +2. 填写 `.env.release` + - `SKILLHUB_PUBLIC_BASE_URL` 填最终 HTTPS 域名,且不要带尾部 `/` + - `SKILLHUB_STORAGE_PROVIDER=s3` + - 按云厂商 OSS / S3 兼容参数填写 `SKILLHUB_STORAGE_S3_*` + - 设置非默认的 `POSTGRES_PASSWORD` 与 `BOOTSTRAP_ADMIN_PASSWORD` +3. 启动前校验 + - 运行 `make validate-release-config` + - 确认没有 `replace-me`、`change-this-*`、`ChangeMe!2026` 之类的占位值 +4. 首次启动 + - 运行 `docker compose --env-file .env.release -f compose.release.yml up -d` + - 检查 `docker compose --env-file .env.release -f compose.release.yml ps` + - 检查 `curl -i http://127.0.0.1:8080/actuator/health` +5. 首登收尾 + - 使用 `BOOTSTRAP_ADMIN_USERNAME` / `BOOTSTRAP_ADMIN_PASSWORD` 登录 + - 立即修改管理员密码 + - 如果后续完全走 OAuth,可将 `BOOTSTRAP_ADMIN_ENABLED=false` + +## 9 可观测性 | 维度 | 方案 | |------|------| @@ -176,7 +205,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d | 日志 | 容器 stdout / stderr | | 指标 | Spring Boot Actuator,后续可接 Prometheus | -## 9 数据迁移 +## 10 数据迁移 Flyway 仍是唯一 schema 变更入口: diff --git a/scripts/validate-release-config.sh b/scripts/validate-release-config.sh new file mode 100755 index 00000000..faa944a4 --- /dev/null +++ b/scripts/validate-release-config.sh @@ -0,0 +1,180 @@ +#!/bin/sh +set -eu + +ENV_FILE="${1:-.env.release}" + +if [ ! -f "$ENV_FILE" ]; then + echo "ERROR: env file not found: $ENV_FILE" >&2 + exit 1 +fi + +while IFS= read -r raw_line || [ -n "$raw_line" ]; do + line=$(printf '%s' "$raw_line" | tr -d '\r') + case "$line" in + ""|\#*) continue ;; + esac + export "$line" +done < "$ENV_FILE" + +errors=0 +warnings=0 + +error() { + errors=$((errors + 1)) + echo "ERROR: $*" >&2 +} + +warn() { + warnings=$((warnings + 1)) + echo "WARN: $*" >&2 +} + +require_non_empty() { + var_name="$1" + eval "var_value=\${$var_name:-}" + if [ -z "$var_value" ]; then + error "$var_name is required" + fi +} + +reject_values() { + var_name="$1" + shift + eval "var_value=\${$var_name:-}" + if [ -z "$var_value" ]; then + return 0 + fi + for bad in "$@"; do + if [ "$var_value" = "$bad" ]; then + error "$var_name still uses placeholder/default value: $bad" + return 0 + fi + done +} + +validate_url() { + var_name="$1" + eval "var_value=\${$var_name:-}" + if [ -z "$var_value" ]; then + return 0 + fi + case "$var_value" in + http://*|https://*) ;; + *) error "$var_name must start with http:// or https://" ;; + esac +} + +validate_no_trailing_slash() { + var_name="$1" + eval "var_value=\${$var_name:-}" + case "$var_value" in + */) error "$var_name must not have a trailing slash" ;; + esac +} + +validate_boolean() { + var_name="$1" + eval "var_value=\${$var_name:-}" + case "$var_value" in + ""|true|false) ;; + *) error "$var_name must be true or false" ;; + esac +} + +validate_port() { + var_name="$1" + eval "var_value=\${$var_name:-}" + if [ -z "$var_value" ]; then + return 0 + fi + case "$var_value" in + *[!0-9]*|"") error "$var_name must be numeric" ;; + *) + if [ "$var_value" -lt 1 ] || [ "$var_value" -gt 65535 ]; then + error "$var_name must be between 1 and 65535" + fi + ;; + esac +} + +require_non_empty SKILLHUB_PUBLIC_BASE_URL +validate_url SKILLHUB_PUBLIC_BASE_URL +validate_no_trailing_slash SKILLHUB_PUBLIC_BASE_URL + +reject_values POSTGRES_PASSWORD "change-this-postgres-password" "skillhub_demo" "skillhub_dev" +reject_values BOOTSTRAP_ADMIN_PASSWORD "replace-this-admin-password" "ChangeMe!2026" "Admin@2026" +reject_values SKILLHUB_STORAGE_S3_ACCESS_KEY "replace-me" +reject_values SKILLHUB_STORAGE_S3_SECRET_KEY "replace-me" + +validate_boolean SESSION_COOKIE_SECURE +validate_boolean BOOTSTRAP_ADMIN_ENABLED +validate_boolean SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE +validate_boolean SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET + +validate_port POSTGRES_PORT +validate_port REDIS_PORT +validate_port API_PORT +validate_port WEB_PORT + +require_non_empty POSTGRES_DB +require_non_empty POSTGRES_USER +require_non_empty POSTGRES_PASSWORD + +storage_provider="${SKILLHUB_STORAGE_PROVIDER:-}" +case "$storage_provider" in + s3) + require_non_empty SKILLHUB_STORAGE_S3_ENDPOINT + require_non_empty SKILLHUB_STORAGE_S3_BUCKET + require_non_empty SKILLHUB_STORAGE_S3_ACCESS_KEY + require_non_empty SKILLHUB_STORAGE_S3_SECRET_KEY + require_non_empty SKILLHUB_STORAGE_S3_REGION + validate_url SKILLHUB_STORAGE_S3_ENDPOINT + validate_url SKILLHUB_STORAGE_S3_PUBLIC_ENDPOINT + ;; + local) + warn "SKILLHUB_STORAGE_PROVIDER=local is only suitable for non-production or temporary validation" + ;; + "") + error "SKILLHUB_STORAGE_PROVIDER is required" + ;; + *) + error "SKILLHUB_STORAGE_PROVIDER must be either local or s3" + ;; +esac + +if [ -n "${SKILLHUB_WEB_API_BASE_URL:-}" ]; then + validate_url SKILLHUB_WEB_API_BASE_URL + validate_no_trailing_slash SKILLHUB_WEB_API_BASE_URL +fi + +if [ -n "${DEVICE_AUTH_VERIFICATION_URI:-}" ]; then + validate_url DEVICE_AUTH_VERIFICATION_URI +fi + +if [ "${SESSION_COOKIE_SECURE:-true}" != "true" ]; then + warn "SESSION_COOKIE_SECURE is not true; only acceptable behind plain HTTP during temporary local verification" +fi + +if [ "${POSTGRES_BIND_ADDRESS:-127.0.0.1}" != "127.0.0.1" ]; then + warn "POSTGRES_BIND_ADDRESS is not 127.0.0.1; confirm database exposure is intended" +fi + +if [ "${REDIS_BIND_ADDRESS:-127.0.0.1}" != "127.0.0.1" ]; then + warn "REDIS_BIND_ADDRESS is not 127.0.0.1; confirm Redis exposure is intended" +fi + +oauth_id="${OAUTH2_GITHUB_CLIENT_ID:-}" +oauth_secret="${OAUTH2_GITHUB_CLIENT_SECRET:-}" +if [ -n "$oauth_id" ] && [ -z "$oauth_secret" ]; then + error "OAUTH2_GITHUB_CLIENT_SECRET is required when OAUTH2_GITHUB_CLIENT_ID is set" +fi +if [ -n "$oauth_secret" ] && [ -z "$oauth_id" ]; then + error "OAUTH2_GITHUB_CLIENT_ID is required when OAUTH2_GITHUB_CLIENT_SECRET is set" +fi + +if [ "$errors" -gt 0 ]; then + echo "Release config validation failed: $errors error(s), $warnings warning(s)." >&2 + exit 1 +fi + +echo "Release config validation passed with $warnings warning(s)."