#!/usr/bin/env bash set -euo pipefail # PocketBase 版本 PB_VERSION="0.22.0" PB_DIR="/opt/pocketbase" PB_DATA_DIR="${PB_DIR}/pb_data" PB_MIGRATIONS_DIR="${PB_DIR}/pb_migrations" PB_PUBLIC_DIR="${PB_DIR}/pb_public" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*"; } # 检查是否为 root check_root() { if [[ $EUID -ne 0 ]]; then log_error "请使用 root 权限运行 (sudo ./install.sh)" exit 1 fi } # 检测系统架构 detect_arch() { local arch=$(uname -m) case $arch in x86_64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7l) echo "armv7" ;; *) log_error "不支持的架构: $arch"; exit 1 ;; esac } # 安装依赖 install_deps() { log_info "安装系统依赖..." apt update && apt upgrade -y apt install -y curl wget unzip nginx certbot python3-certbot-nginx ufw fail2ban log_success "系统依赖安装完成" } # 下载并安装 PocketBase install_pocketbase() { local arch=$(detect_arch) local pb_url="https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${arch}.zip" local tmp_dir=$(mktemp -d) log_info "下载 PocketBase v${PB_VERSION} (${arch})..." cd "$tmp_dir" wget -q --show-progress "$pb_url" -O pocketbase.zip unzip -q pocketbase.zip log_info "安装 PocketBase 到 ${PB_DIR}..." mkdir -p "$PB_DIR" mv pocketbase "$PB_DIR/" chmod +x "$PB_DIR/pocketbase" # 创建数据目录 mkdir -p "$PB_DATA_DIR" "$PB_MIGRATIONS_DIR" "$PB_PUBLIC_DIR" # 复制迁移文件 if [[ -d "$(dirname "$0")/../apps/pb_migrations" ]]; then cp -r "$(dirname "$0")/../apps/pb_migrations"/* "$PB_MIGRATIONS_DIR/" log_success "迁移文件已复制" fi # 清理 cd / rm -rf "$tmp_dir" log_success "PocketBase 安装完成" } # 生成加密密钥 generate_encryption_key() { openssl rand -base64 32 | tr -d '\n' } # 创建 systemd 服务 create_systemd_service() { local encryption_key=$(generate_encryption_key) log_info "创建 systemd 服务..." cat > /etc/systemd/system/pocketbase.service < /etc/nginx/sites-available/yunsheng-digital < HTTPS 重定向 server { listen 80; listen [::]:80; server_name ${domain} www.${domain}; # ACME 挑战路径 (用于 SSL 证书验证) location /.well-known/acme-challenge/ { root /var/www/html; try_files \$uri =404; } # 其他请求重定向到 HTTPS location / { return 301 https://\$host\$request_uri; } } # HTTPS 主站点 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name ${domain} www.${domain}; # SSL 证书 (由 Certbot 自动填充) # ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem; # SSL 安全配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' wss: https:; frame-ancestors 'self';" always; # 根目录 root ${PB_PUBLIC_DIR}; index index.html; # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # 静态文件缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; try_files \$uri =404; } # 前端路由 (SPA fallback) location / { try_files \$uri \$uri/ /index.html; } # PocketBase API 代理 location /api/ { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; 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_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Forwarded-Host \$host; proxy_set_header X-Forwarded-Port \$server_port; # 超时设置 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; # 缓冲 proxy_buffering on; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # 文件上传大小限制 client_max_body_size 100M; } # Realtime WebSocket location /api/realtime { proxy_pass http://127.0.0.1:8090; 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_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # WebSocket 超时 proxy_read_timeout 86400; proxy_send_timeout 86400; } # PocketBase Admin UI location /_/ { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; 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_set_header X-Forwarded-Proto \$scheme; # 限制 Admin UI 访问 (可选:仅允许特定 IP) # allow 1.2.3.4; # deny all; } # 禁止访问隐藏文件 location ~ /\. { deny all; access_log off; log_not_found off; } # 禁止访问备份文件 location ~* \.(bak|backup|sql|log|env|ini)$ { deny all; access_log off; log_not_found off; } } EOF # 启用站点 ln -sf /etc/nginx/sites-available/yunsheng-digital /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default # 测试配置 nginx -t log_success "Nginx 配置完成" } # 申请 SSL 证书 setup_ssl() { local domain="$1" local email="$2" if [[ -z "$domain" || -z "$email" ]]; then log_warn "未提供域名或邮箱,跳过 SSL 证书申请" log_info "稍后可手动运行: certbot --nginx -d ${domain}" return fi log_info "申请 Let's Encrypt SSL 证书..." # 先启动 nginx (HTTP 模式) systemctl restart nginx # 申请证书 certbot --nginx -d "${domain}" -d "www.${domain}" \ --non-interactive \ --agree-tos \ --email "${email}" \ --redirect \ --hsts \ --staple-ocsp \ --must-staple # 设置自动续期 systemctl enable certbot.timer log_success "SSL 证书申请完成" } # 配置防火墙 configure_firewall() { log_info "配置 UFW 防火墙..." ufw --force enable ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 80/tcp ufw allow 443/tcp # 仅允许本地访问 PocketBase 直接端口 ufw allow from 127.0.0.1 to any port 8090 log_success "防火墙配置完成" } # 配置 Fail2Ban configure_fail2ban() { log_info "配置 Fail2Ban..." cat > /etc/fail2ban/jail.d/yunsheng-digital.conf < /usr/local/bin/yunsheng-backup <<'EOF' #!/usr/bin/env bash set -euo pipefail BACKUP_DIR="/backup/yunsheng-digital" DATE=$(date +%Y%m%d_%H%M%S) PB_DATA_DIR="/opt/pocketbase/pb_data" mkdir -p "$BACKUP_DIR" log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; } log_success() { echo -e "\033[0;32m[SUCCESS]\033[0m $*"; } log_info "开始备份..." # 备份 PocketBase 数据 (使用内置 dump 命令) /opt/pocketbase/pocketbase dump \ --dir=/opt/pocketbase/pb_data \ --output="${BACKUP_DIR}/pocketbase_${DATE}.zip" # 备份 Nginx 配置 cp -r /etc/nginx/sites-available/yunsheng-digital "${BACKUP_DIR}/nginx_${DATE}.conf" # 备份 SSL 证书 cp -r /etc/letsencrypt "${BACKUP_DIR}/letsencrypt_${DATE}" # 清理 30 天前的备份 find "$BACKUP_DIR" -type f -mtime +30 -delete log_success "备份完成: ${BACKUP_DIR}/pocketbase_${DATE}.zip" EOF chmod +x /usr/local/bin/yunsheng-backup # 更新脚本 cat > /usr/local/bin/yunsheng-update <<'EOF' #!/usr/bin/env bash set -euo pipefail PROJECT_DIR="/opt/yunsheng-digital" PB_DIR="/opt/pocketbase" PB_PUBLIC_DIR="${PB_DIR}/pb_public" log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; } log_success() { echo -e "\033[0;32m[SUCCESS]\033[0m $*"; } log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*"; } cd "$PROJECT_DIR" log_info "拉取最新代码..." git pull origin master log_info "安装前端依赖..." cd apps/web pnpm install --frozen-lockfile log_info "构建前端..." pnpm build log_info "部署构建产物..." cp -r dist/* "$PB_PUBLIC_DIR/" log_info "重载 Nginx..." systemctl reload nginx log_success "更新完成!" EOF chmod +x /usr/local/bin/yunsheng-update # 状态检查脚本 cat > /usr/local/bin/yunsheng-status <<'EOF' #!/usr/bin/env bash echo "=== 云升数码 服务状态 ===" echo echo "--- PocketBase ---" systemctl status pocketbase --no-pager -l echo echo "--- Nginx ---" systemctl status nginx --no-pager -l echo echo "--- 磁盘使用 ---" df -h /opt/pocketbase echo echo "--- 内存使用 ---" free -h echo echo "--- 网络连接 ---" ss -tlnp | grep -E '(80|443|8090)' EOF chmod +x /usr/local/bin/yunsheng-status log_success "维护脚本创建完成" } # 设置定时任务 setup_cron() { log_info "设置定时任务..." # 每天凌晨 3 点备份 (crontab -l 2>/dev/null | grep -v yunsheng-backup; echo "0 3 * * * /usr/local/bin/yunsheng-backup >> /var/log/yunsheng-backup.log 2>&1") | crontab - # 每周一凌晨 4 点清理日志 (crontab -l 2>/dev/null | grep -v "journalctl"; echo "0 4 * * 1 journalctl --vacuum-time=30d >> /var/log/yunsheng-cleanup.log 2>&1") | crontab - log_success "定时任务设置完成" } # 主函数 main() { echo "==========================================" echo " 云升数码 - 一键部署脚本 v1.0" echo "==========================================" echo check_root # 获取参数 DOMAIN="${1:-}" EMAIL="${2:-}" if [[ -z "$DOMAIN" ]]; then read -rp "请输入域名 (例: yunsheng.digital): " DOMAIN fi if [[ -z "$EMAIL" ]]; then read -rp "请输入邮箱 (用于 SSL 证书): " EMAIL fi log_info "开始部署..." log_info "域名: ${DOMAIN}" log_info "邮箱: ${EMAIL}" install_deps install_pocketbase create_systemd_service configure_nginx "$DOMAIN" setup_ssl "$DOMAIN" "$EMAIL" configure_firewall configure_fail2ban create_maintenance_scripts setup_cron # 启动服务 log_info "启动服务..." systemctl start pocketbase sleep 3 systemctl restart nginx echo echo "==========================================" log_success "部署完成!" echo "==========================================" echo echo "📋 重要信息:" echo " - 网站地址: https://${DOMAIN}" echo " - 后台管理: https://${DOMAIN}/_/" echo " - 管理员账号: admin@yunsheng.digital" echo " - 管理员密码: YunSheng@2024!Admin" echo " - PocketBase 数据目录: ${PB_DATA_DIR}" echo " - 前端构建目录: ${PB_PUBLIC_DIR}" echo echo "🔐 安全提醒:" echo " 1. 请立即登录后台修改默认管理员密码" echo " 2. 加密密钥已保存在 systemd 服务文件中" echo " 3. 建议配置 SSH 密钥登录,禁用密码登录" echo echo "🛠️ 常用命令:" echo " - 查看状态: yunsheng-status" echo " - 备份数据: yunsheng-backup" echo " - 更新代码: yunsheng-update" echo " - 查看日志: journalctl -u pocketbase -f" echo } main "$@"