初始化
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 剧核工厂 Linux 一键部署脚本。
|
||||
# 负责安装 Nginx/PostgreSQL/Redis/FFmpeg、执行迁移、创建 systemd 服务,
|
||||
# 并按需使用本地 API/acme/acme.sh 申请或安装 HTTPS 证书。
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RESET='\033[0m'
|
||||
info(){ echo -e "${CYAN} -> $*${RESET}"; }
|
||||
ok(){ echo -e "${GREEN} OK $*${RESET}"; }
|
||||
warn(){ echo -e "${YELLOW} !! $*${RESET}"; }
|
||||
fail(){ echo -e "${RED} ERR $*${RESET}" >&2; exit 1; }
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
API_ROOT="$(realpath "${1:-$SCRIPT_DIR/..}")"
|
||||
PROJECT_ROOT="$(realpath "$API_ROOT/..")"
|
||||
CONFIG_FILE="$SCRIPT_DIR/.deploy.conf"
|
||||
SYSTEM_ENV_FILE="/etc/juyou_ai/juyou_ai.env"
|
||||
[ -f "$SCRIPT_DIR/prepare_server.sh" ] || fail "缺少服务器初始化脚本。"
|
||||
chmod +x "$API_ROOT/bin/jcf-api" 2>/dev/null || true
|
||||
[ -x "$API_ROOT/bin/jcf-api" ] || fail "缺少 Linux API 二进制 $API_ROOT/bin/jcf-api,请先执行 push.bat。"
|
||||
[ -f "$API_ROOT/static/web/index.html" ] || fail "缺少 WEB 构建产物。"
|
||||
[ -f "$API_ROOT/static/admin/index.html" ] || fail "缺少 ADMIN 构建产物。"
|
||||
|
||||
if [ -s "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
fi
|
||||
SERVICE_NAME="juyou_ai"
|
||||
APP_PORT="8123"
|
||||
DOMAIN_INPUT="${SAVED_DOMAIN_INPUT:-_}"
|
||||
USER_EMAIL="${SAVED_USER_EMAIL:-}"
|
||||
DOMAINS=()
|
||||
if [ "$DOMAIN_INPUT" != "_" ]; then
|
||||
for domain in $DOMAIN_INPUT; do
|
||||
[[ "$domain" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] || fail "域名格式无效:$domain"
|
||||
DOMAINS+=("$domain")
|
||||
done
|
||||
[ "${#DOMAINS[@]}" -gt 0 ] || fail "至少需要一个有效域名。"
|
||||
DOMAIN_INPUT="${DOMAINS[*]}"
|
||||
FIRST_DOMAIN="${DOMAINS[0]}"
|
||||
fi
|
||||
FIRST_DOMAIN="${FIRST_DOMAIN:-_}"
|
||||
command -v sudo >/dev/null 2>&1 || fail "系统缺少 sudo。"
|
||||
sudo env SAVED_DOMAIN_INPUT="$DOMAIN_INPUT" JCF_ENV_SOURCE="${JCF_ENV_SOURCE:-}" bash "$SCRIPT_DIR/prepare_server.sh" "$API_ROOT"
|
||||
[ -f "$SYSTEM_ENV_FILE" ] || fail "服务器初始化后仍缺少 $SYSTEM_ENV_FILE。"
|
||||
SOCKET_PATH="/run/$SERVICE_NAME/app.sock"
|
||||
CERT_DIR="/etc/nginx/ssl"
|
||||
NGINX_FILE="/etc/nginx/conf.d/$SERVICE_NAME.conf"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
LOG_DIR="/var/log/$SERVICE_NAME"
|
||||
LOG_FILE="$LOG_DIR/$SERVICE_NAME.log"
|
||||
LOGROTATE_FILE="/etc/logrotate.d/$SERVICE_NAME"
|
||||
|
||||
# 只读取迁移所需的数据库变量,不直接 source .env。
|
||||
# APP_NAME 等值可能包含空格,直接 source 会被 Bash 误解析。
|
||||
env_value() {
|
||||
local key="$1" value
|
||||
value="$(sudo sed -n -E "s/^${key}=//p" "$SYSTEM_ENV_FILE" | head -n 1)"
|
||||
value="${value#\"}"; value="${value%\"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
DATABASE_URL="$(env_value DATABASE_URL)"
|
||||
DATABASE_NAME="$(env_value DATABASE_NAME)"
|
||||
DATABASE_HOST="$(env_value DATABASE_HOST)"
|
||||
DATABASE_PORT="$(env_value DATABASE_PORT)"
|
||||
DATABASE_USER="$(env_value DATABASE_USER)"
|
||||
DATABASE_PASSWORD="$(env_value DATABASE_PASSWORD)"
|
||||
PSQL_ARGS=()
|
||||
if [ -n "${DATABASE_URL:-}" ]; then
|
||||
PSQL_TARGET="$DATABASE_URL"
|
||||
else
|
||||
PSQL_TARGET="dbname=${DATABASE_NAME:-juchuang_factory} host=${DATABASE_HOST:-127.0.0.1} port=${DATABASE_PORT:-5432} user=${DATABASE_USER:-postgres}"
|
||||
export PGPASSWORD="${DATABASE_PASSWORD:-}"
|
||||
fi
|
||||
|
||||
# 使用独立记录表避免重复执行同一个 SQL 迁移。
|
||||
psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -c 'CREATE TABLE IF NOT EXISTS jcf_schema_migrations (version varchar(128) PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP);' >/dev/null
|
||||
for migration in "$API_ROOT"/migrations/*.up.sql; do
|
||||
[ -f "$migration" ] || continue
|
||||
version="$(basename "$migration")"
|
||||
already="$(psql "$PSQL_TARGET" -At -v ON_ERROR_STOP=1 -c "SELECT 1 FROM jcf_schema_migrations WHERE version='${version//\'/\'\'}' LIMIT 1")"
|
||||
if [ "$already" = "1" ]; then
|
||||
info "跳过已执行迁移:$version"
|
||||
continue
|
||||
fi
|
||||
info "执行数据库迁移:$version"
|
||||
psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -f "$migration" >/dev/null
|
||||
psql "$PSQL_TARGET" -v ON_ERROR_STOP=1 -c "INSERT INTO jcf_schema_migrations(version) VALUES ('${version//\'/\'\'}')" >/dev/null
|
||||
done
|
||||
unset PGPASSWORD
|
||||
|
||||
sudo chown -R www-data:www-data "$API_ROOT"
|
||||
sudo rm -f "$API_ROOT/.env"
|
||||
sudo mkdir -p "$(dirname "$SOCKET_PATH")" "$CERT_DIR" /var/www/jcf-acme
|
||||
sudo install -d -m 0750 -o www-data -g www-data "$LOG_DIR"
|
||||
sudo touch "$LOG_FILE"
|
||||
sudo chown www-data:www-data "$LOG_FILE"
|
||||
sudo chmod 0640 "$LOG_FILE"
|
||||
|
||||
# 每天轮转 API 与 Worker 的统一日志,并在单个文件过大时提前轮转。
|
||||
sudo tee "$LOGROTATE_FILE" >/dev/null <<EOF
|
||||
$LOG_FILE {
|
||||
daily
|
||||
rotate 14
|
||||
maxsize 50M
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
|
||||
# 创建 API 与队列 Worker 同进程 systemd 服务。
|
||||
sudo tee "$SERVICE_FILE" >/dev/null <<EOF
|
||||
[Unit]
|
||||
Description=JuheFactory Go API and AI Worker
|
||||
After=network.target postgresql.service redis-server.service
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=$API_ROOT
|
||||
EnvironmentFile=$SYSTEM_ENV_FILE
|
||||
Environment=HOME=$API_ROOT
|
||||
CacheDirectory=juyou_ai
|
||||
ExecStart=$API_ROOT/bin/jcf-api
|
||||
StandardOutput=append:$LOG_FILE
|
||||
StandardError=append:$LOG_FILE
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME" --now
|
||||
|
||||
mkdir -p "$API_ROOT/static/web" "$API_ROOT/static/admin"
|
||||
EXISTING_CERT_BUNDLE="$CERT_DIR/${FIRST_DOMAIN}_bundle.crt"
|
||||
EXISTING_CERT_KEY="$CERT_DIR/${FIRST_DOMAIN}.key"
|
||||
if [ "$FIRST_DOMAIN" != "_" ] && [ -f "$EXISTING_CERT_BUNDLE" ] && [ -f "$EXISTING_CERT_KEY" ]; then
|
||||
info "检测到现有 SSL 证书,跳过 ACME 申请并复用证书"
|
||||
USE_SSL=true
|
||||
elif [ "$FIRST_DOMAIN" != "_" ] && [ -n "$USER_EMAIL" ]; then
|
||||
# 先用 HTTP 临时站点提供 ACME HTTP-01 验证目录。
|
||||
sudo tee "$NGINX_FILE" >/dev/null <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN_INPUT;
|
||||
location ^~ /.well-known/acme-challenge/ { root /var/www/jcf-acme; }
|
||||
location / { root $API_ROOT/static/web; try_files \$uri \$uri/ /index.html; }
|
||||
}
|
||||
EOF
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
ACME_BIN="$API_ROOT/acme/acme.sh"
|
||||
[ -f "$ACME_BIN" ] || fail "缺少本地 ACME 脚本,请先执行 API/deploy/update_acme.sh。"
|
||||
chmod +x "$ACME_BIN"
|
||||
export LE_CONFIG_HOME="/root/.acme.sh"
|
||||
bash "$ACME_BIN" --set-default-ca --server letsencrypt
|
||||
ACME_DOMAIN_ARGS=()
|
||||
for domain in "${DOMAINS[@]}"; do ACME_DOMAIN_ARGS+=( -d "$domain" ); done
|
||||
bash "$ACME_BIN" --issue --webroot /var/www/jcf-acme "${ACME_DOMAIN_ARGS[@]}" --accountemail "$USER_EMAIL"
|
||||
bash "$ACME_BIN" --install-cert -d "$FIRST_DOMAIN" \
|
||||
--key-file "$CERT_DIR/${FIRST_DOMAIN}.key" \
|
||||
--fullchain-file "$CERT_DIR/${FIRST_DOMAIN}_bundle.crt" \
|
||||
--reloadcmd "systemctl reload nginx"
|
||||
USE_SSL=true
|
||||
else
|
||||
USE_SSL=false
|
||||
[ "$FIRST_DOMAIN" = "_" ] || warn "未填写证书邮箱,部署为 HTTP。"
|
||||
fi
|
||||
|
||||
if [ "$USE_SSL" = true ]; then
|
||||
PROTOCOL=https
|
||||
sudo tee "$NGINX_FILE" >/dev/null <<EOF
|
||||
server { listen 80; server_name $DOMAIN_INPUT; return 301 https://\$host\$request_uri; }
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name $DOMAIN_INPUT;
|
||||
ssl_certificate $CERT_DIR/${FIRST_DOMAIN}_bundle.crt;
|
||||
ssl_certificate_key $CERT_DIR/${FIRST_DOMAIN}.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
client_max_body_size 2g;
|
||||
location /api/ { proxy_pass http://127.0.0.1:$APP_PORT; 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 300; proxy_buffering off; }
|
||||
location ~ ^/(health|docs|redoc|openapi.json)(/.*)?\$ { proxy_pass http://127.0.0.1:$APP_PORT; proxy_set_header Host \$host; }
|
||||
location /admin/ { alias $API_ROOT/static/admin/; try_files \$uri \$uri/ /admin/index.html; }
|
||||
location / { root $API_ROOT/static/web; try_files \$uri \$uri/ /index.html; }
|
||||
}
|
||||
EOF
|
||||
else
|
||||
PROTOCOL=http
|
||||
sudo tee "$NGINX_FILE" >/dev/null <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN_INPUT;
|
||||
client_max_body_size 2g;
|
||||
location /api/ { proxy_pass http://127.0.0.1:$APP_PORT; 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 300; proxy_buffering off; }
|
||||
location ~ ^/(health|docs|redoc|openapi.json)(/.*)?\$ { proxy_pass http://127.0.0.1:$APP_PORT; proxy_set_header Host \$host; }
|
||||
location /admin/ { alias $API_ROOT/static/admin/; try_files \$uri \$uri/ /admin/index.html; }
|
||||
location / { root $API_ROOT/static/web; try_files \$uri \$uri/ /index.html; }
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx || sudo systemctl restart nginx
|
||||
sudo systemctl restart "$SERVICE_NAME"
|
||||
if [ -n "${JCF_ENV_SOURCE:-}" ]; then
|
||||
rm -f -- "$JCF_ENV_SOURCE"
|
||||
fi
|
||||
ok "部署完成:${PROTOCOL}://${FIRST_DOMAIN},服务名:$SERVICE_NAME"
|
||||
Reference in New Issue
Block a user