Home
avatar

静静

Astro博客部署在服务器上并设置自动hook

将 Astro 博客部署到云服务器上有完全的控制权。以下是详细步骤:

1. 服务器环境准备

登录服务器并更新系统

用的是Ubuntu-24.04.1-x64

ssh username@your-server-ip
sudo apt update && sudo apt upgrade -y

安装必要软件

# 安装 Node.js(推荐用 nvm 避免权限问题)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# 重新加载 bash
source ~/.bashrc

# 安装 Node.js LTS
nvm install --lts
nvm use --lts

# 验证安装
node --version
npm --version

image-20251211173329481

2. 安装 Nginx(Web服务器)

sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx

3. 安装pnpm

npm install -g pnpm

4. 在服务器上创建 Astro 项目

cd /var/www
pnpm create astro@latest --template uxiaohan/vhAstro-Theme astro-blog
cd astro-blog
npm install
npm run build

image-20251211174946037

image-20251211175116016

5. 配置 Nginx

# 创建 Nginx 配置文件
sudo vim /etc/nginx/sites-available/astro-blog

基本配置示例

server {
    listen 80;
    listen [::]:80;
    
    # 你的域名或服务器IP
    server_name your-domain.com www.your-domain.com;
    # 如果只有 IP,用:server_name your-server-ip;
    
    root /var/www/astro-blog/dist/;
    index index.html;
    
    # 启用 gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    location / {
        try_files $uri $uri/ $uri.html =404;
    }
    
    # 缓存静态资源
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # 防止访问 .astro 等隐藏文件
    location ~ /\. {
        deny all;
    }
}

启用网站配置

# 创建符号链接
sudo ln -s /etc/nginx/sites-available/astro-blog /etc/nginx/sites-enabled/

# 测试配置
sudo nginx -t

# 重启 Nginx
sudo systemctl restart nginx

image-20251211182620483

到这里就创建了默认的blog主题的,如果用了主题的就是主题的主页面,然后把md文章丢src下面对应的目录就行。

迁移Hexo文章到Astro

在原本的Hexo文件夹下运行下面的代码,需要安装依赖

pip install python-frontmatter pyyaml  -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple

下面这个脚本实现替换文件头的功能,可能个别文章还是需要手动替换,不过这个已经很好了。

import os
import re
import datetime
import frontmatter
import yaml

def convert_hexo_to_astro(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 检查文件是否已经有 Astro 格式的前言
        if 'id:' in content and 'recommend:' in content:
            print(f"跳过已转换的文件: {file_path}")
            return
        
        # 解析原 frontmatter
        post = frontmatter.loads(content)
        
        # 生成英文 ID(基于标题)
        title = post.get('title', '未命名文章')
        # 移除特殊字符,保留中文字符
        id_str = re.sub(r'[^\w\u4e00-\u9fff\s-]', '', str(title))
        id_str = re.sub(r'\s+', '-', id_str)  # 空格转横线
        id_str = id_str.lower().strip('-')[:50]  # 转为小写并截断
        
        # 如果 ID 为空,使用文件名
        if not id_str:
            filename = os.path.basename(file_path).replace('.md', '')
            id_str = re.sub(r'[^\w\s-]', '', filename)
            id_str = re.sub(r'\s+', '-', id_str).lower()[:50]
        
        # 转换日期格式
        if 'date' in post:
            if isinstance(post['date'], datetime.datetime):
                post['date'] = post['date'].isoformat()
            elif isinstance(post['date'], str):
                # 尝试多种日期格式
                date_formats = [
                    '%Y-%m-%d %H:%M:%S',
                    '%Y-%m-%d %H:%M',
                    '%Y-%m-%d',
                    '%Y/%m/%d %H:%M:%S',
                    '%Y/%m/%d'
                ]
                for fmt in date_formats:
                    try:
                        dt = datetime.datetime.strptime(post['date'], fmt)
                        post['date'] = dt.isoformat()
                        break
                    except ValueError:
                        continue
                else:
                    # 如果所有格式都失败,使用当前时间
                    post['date'] = datetime.datetime.now().isoformat()
        
        # 转换分类格式
        if 'categories' in post:
            if isinstance(post['categories'], list):
                # 如果是列表,转换为逗号分隔的字符串
                post['categories'] = ",".join([str(c) for c in post['categories']])
            elif not isinstance(post['categories'], str):
                # 如果不是字符串,转换为字符串
                post['categories'] = str(post['categories'])
        else:
            # 如果没有分类,设置为默认值
            post['categories'] = "未分类"
        
        # 确保标签是列表格式
        if 'tags' in post:
            if isinstance(post['tags'], str):
                # 如果是字符串,尝试按逗号分割
                post['tags'] = [tag.strip() for tag in post['tags'].split(',')]
            elif not isinstance(post['tags'], list):
                # 如果不是列表,转换为列表
                post['tags'] = [str(post['tags'])]
        else:
            # 如果没有标签,设置为空列表
            post['tags'] = []
        
        # 添加必填字段(如果不存在)
        if 'id' not in post:
            post['id'] = id_str
        
        if 'recommend' not in post:
            post['recommend'] = False
        
        if 'top' not in post:
            post['top'] = False
        
        if 'hide' not in post:
            post['hide'] = False
        
        # 确保封面图字段存在
        if 'cover' not in post:
            post['cover'] = ""
        
        # 保存新格式
        new_content = frontmatter.dumps(post)
        
        # 写入文件
        with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
            f.write(new_content)
        
        print(f"成功转换: {file_path}")
        
    except yaml.YAMLError as e:
        print(f"YAML 解析错误 {file_path}: {e}")
        # 尝试手动修复格式
        manual_fix_yaml(file_path)
    except Exception as e:
        print(f"处理文件 {file_path} 时出错: {e}")

def manual_fix_yaml(file_path):
    """手动修复 YAML 格式错误"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 提取前言部分
        parts = content.split('---', 2)
        if len(parts) < 3:
            print(f"无法解析文件格式: {file_path}")
            return
        
        frontmatter_text = parts[1]
        body = parts[2]
        
        # 简单的 YAML 修复
        # 1. 确保键值对格式正确
        lines = frontmatter_text.split('\n')
        fixed_lines = []
        
        for line in lines:
            line = line.strip()
            if not line:
                continue
                
            # 处理键值对
            if ':' in line:
                key, value = line.split(':', 1)
                key = key.strip()
                value = value.strip()
                
                # 确保值有引号(如果包含特殊字符)
                if value and not (value.startswith('"') and value.endswith('"')):
                    if any(char in value for char in '[]{}:,'):
                        value = f'"{value}"'
                
                fixed_lines.append(f"{key}: {value}")
            else:
                # 处理列表项
                if line.startswith('-'):
                    fixed_lines.append(f"  {line}")
                else:
                    fixed_lines.append(line)
        
        # 重新构建前言
        fixed_frontmatter = '\n'.join(fixed_lines)
        new_content = f"---\n{fixed_frontmatter}\n---\n{body}"
        
        with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
            f.write(new_content)
        
        print(f"已尝试手动修复: {file_path}")
        
        # 重新尝试转换
        convert_hexo_to_astro(file_path)
        
    except Exception as e:
        print(f"手动修复失败 {file_path}: {e}")

def backup_files(directory):
    """备份原始文件"""
    import shutil
    import time
    
    backup_dir = f"{directory}_backup_{int(time.time())}"
    shutil.copytree(directory, backup_dir)
    print(f"已创建备份: {backup_dir}")
    return backup_dir

# 主程序
if __name__ == "__main__":
    blog_dir = './'
    
    # 检查目录是否存在
    if not os.path.exists(blog_dir):
        print(f"目录不存在: {blog_dir}")
        exit(1)
    
    # 备份原始文件
    backup_path = backup_files(blog_dir)
    print(f"原始文件已备份到: {backup_path}")
    
    # 遍历目录转换所有文件
    converted_count = 0
    error_count = 0
    
    for root, dirs, files in os.walk(blog_dir):
        for file in files:
            if file.endswith('.md'):
                file_path = os.path.join(root, file)
                try:
                    convert_hexo_to_astro(file_path)
                    converted_count += 1
                except Exception as e:
                    print(f"处理文件 {file_path} 时发生严重错误: {e}")
                    error_count += 1
    
    print(f"\n转换完成!")
    print(f"成功转换: {converted_count} 个文件")
    print(f"转换失败: {error_count} 个文件")
    print(f"原始文件备份在: {backup_path}")

然后运行一下下面的命令,删除某些不规范的代码块语法。

# C → c
find src/content/blog/ \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
    sed -i 's/```C$/```c/g' "$file"
done

# mysql → sql
find src/content/blog/ \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
    sed -i 's/```mysql$/```sql/g' "$file"
done

# url → plaintext
find src/content/blog/ \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
    sed -i 's/```url$/```plaintext/g' "$file"
done

marmaid支持

添加 Mermaid 支持。导入了 mermaid,添加到 integrations 数组中。

//------其他部分

import mermaid from 'astro-mermaid';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
//------其他部分
    integrations: [
        swup({
            theme: false,
            animationClass: "vh-animation-",
            containers: [".main-inner>.main-inner-content", '.vh-header>.main'],
            smoothScrolling: true,
            progress: true,
            cache: true,
            preload: true,
            accessibility: true,
            updateHead: true,
            updateBodyClass: false,
            globalInstance: true
        }),
        
        // 添加 Mermaid 支持
        mermaid({
            theme: 'default',  // 或 'forest', 'dark', 'neutral' 等
        }),
        
        //------其他部分
        Compress({ Image: false, Action: { Passed: async () => true } }),
        sitemap({
            // 处理末尾带 / 的 url
            serialize: (item) => ({ ...item, url: item.url.endsWith('/') ? 

6. 使用 Let’s Encrypt 配置 HTTPS(可选但推荐)

# 安装 Certbot
sudo apt install certbot python3-certbot-nginx -y

# 获取证书不要邮箱
sudo certbot --nginx -d your-domain.com -d www.your-domain.com --register-unsafely-without-email --agree-tos

# 设置自动续期
sudo certbot renew --dry-run

7. 配置防火墙和设置专用用户

# 如果使用 UFW
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable
# 打开hook的端口
sudo ufw allow 9000/tcp
sudo ufw status

第1步:创建专用用户

# 创建系统用户 blogdeploy(不创建家目录,不允许登录)
sudo adduser --system --group --no-create-home blogdeploy

# 验证用户创建成功
id blogdeploy
# 应该显示:uid=xxx(blogdeploy) gid=xxx(blogdeploy) groups=xxx(blogdeploy)

第2步:设置目录权限

# 将项目目录所有权给 blogdeploy
sudo chown -R blogdeploy:blogdeploy /var/www/astro-blog

# 或者如果 pnpm 装在其他地方
which pnpm  # 先查看 pnpm 位置
# 给 blogdeploy 用户访问 pnpm 的权限
sudo chown -R blogdeploy:blogdeploy /root/.local/share/pnpm


# dist 目录特殊处理:让 www-data(Nginx用户)有读取权限
sudo chown -R blogdeploy:www-data /var/www/astro-blog/dist
sudo chmod -R 755 /var/www/astro-blog/dist

第3步:配置 blogdeploy 用户的 Git

# 直接在项目里配置(只对这个仓库生效)
cd /var/www/astro-blog

sudo -u blogdeploy git config user.name "Blog Deploy"
sudo -u blogdeploy git config user.email "deploy@ruajingjing.top"
sudo -u blogdeploy git config --add safe.directory /var/www/astro-blog

# 验证
sudo -u blogdeploy git config --list

# 切换到 blogdeploy 用户(临时)
sudo -u blogdeploy bash

# 测试能否访问仓库
cd /var/www/astro-blog
git status
git pull origin astro-blog

# 退出 blogdeploy 用户
exit

第4步:确保 blogdeploy 能使用 pnpm

echo "安装 nvm..."
# 通过 sudo -u 执行安装脚本
sudo -u blogdeploy bash << 'EOF'
# 下载并安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash

# 设置 nvm 环境变量
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# 安装 Node.js LTS
echo "安装 Node.js LTS 版本..."
nvm install --lts
nvm use --lts

# 验证安装
echo "Node.js 版本:"
node --version
echo "npm 版本:"
npm --version
EOF

# 4. 重新安装 pnpm(使用正确的方法)
echo "重新安装 pnpm..."
sudo -u blogdeploy bash << 'EOF'
# 加载 nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# 确保使用正确的 Node.js 版本
nvm use --lts

# 卸载可能存在的问题 pnpm 安装
npm uninstall -g pnpm 2>/dev/null || true

# 清除 npm 缓存
npm cache clean --force

# 使用正确方法安装 pnpm
npm install -g pnpm --prefix=/home/blogdeploy/.npm-packages

# 创建 pnpm 专用配置
mkdir -p ~/.pnpm
cat > ~/.pnpm/config.json << 'PNPM_CONFIG'
{
  "store-dir": "/home/blogdeploy/.pnpm-store",
  "global-dir": "/home/blogdeploy/.npm-packages/lib/node_modules"
}
PNPM_CONFIG

# 设置 PATH(不修改 .bashrc,使用环境变量)
export PATH="/home/blogdeploy/.npm-packages/bin:$PATH"

# 验证安装
echo "pnpm 版本:"
pnpm --version 2>/dev/null || echo "pnpm 未正确安装"

echo "pnpm 存储路径:"
pnpm store path 2>/dev/null || echo "无法获取存储路径"
EOF

# 5. 创建安全的部署环境配置
echo "创建部署环境配置..."
sudo -u blogdeploy bash << 'EOF'
# 创建部署脚本使用的环境文件
cat > ~/.blogdeploy.env << 'DEPLOY_ENV'
export NVM_DIR="/home/blogdeploy/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
export PATH="/home/blogdeploy/.npm-packages/bin:$PATH"
DEPLOY_ENV

echo "已创建部署环境配置文件"
EOF

echo "=== 环境验证 ==="

sudo -u blogdeploy bash << 'EOF'
echo "1. 用户: \$USER"
echo "2. 家目录: \$HOME"
echo ""

echo "3. Node.js 版本:"
node --version 2>/dev/null || echo "未安装"
echo ""

echo "4. npm 版本:"
npm --version 2>/dev/null || echo "未安装"
echo ""

echo "5. pnpm 版本:"
pnpm --version 2>/dev/null || echo "未安装"
echo ""

echo "6. pnpm 存储路径:"
pnpm store path 2>/dev/null || echo "无法获取"
echo ""

echo "7. 当前 PATH:"
echo \$PATH
EOF

第5步:测试完整部署流程

# 以 blogdeploy 用户身份测试部署
sudo -u blogdeploy bash -c "cd /var/www/astro-blog && git pull origin astro-blog && pnpm install && pnpm build"

cd /var/www/astro-blog
sudo -u blogdeploy git remote set-url origin https://gitee.com/xxx/my-blog.git
sudo -u blogdeploy git pull origin astro-blog

# 检查是否成功
ls -la /var/www/astro-blog/dist

第6步:验证权限

# 检查关键目录权限
ls -la /var/www/astro-blog
# 应该显示 blogdeploy blogdeploy

ls -la /var/www/astro-blog/dist
# 应该显示 blogdeploy www-data 或 www-data www-data

# 检查 Nginx 能否读取
sudo -u www-data cat /var/www/astro-blog/dist/index.html
# 应该能正常显示内容

常见问题排查

如果遇到权限错误

# 重新设置权限
sudo chown -R blogdeploy:blogdeploy /var/www/astro-blog
sudo chown -R blogdeploy:www-data /var/www/astro-blog/dist
sudo chmod -R 755 /var/www/astro-blog

如果 Git 报错

# 设置 Git 安全目录
sudo -u blogdeploy git config --global --add safe.directory /var/www/astro-blog

如果 pnpm 找不到

# 检查 pnpm 路径
which pnpm

# 如果在 /root/.local/share/pnpm,需要创建链接或重装

8. 切换git分支迁移

1. 初始化并关联远程仓库

cd /var/www/astro-blog

# 如果还没初始化 git
git init

# 添加 Gitee 远程仓库
git remote add origin https://xxxx.git

# 查看远程仓库
git remote -v

2. 创建新分支(推荐叫 astro)

# 创建并切换到新分支
git checkout -b astro

3. 添加 .gitignore

创建 .gitignore 文件,避免提交不必要的文件:

nano .gitignore

粘贴以下内容:

# 依赖
node_modules/
.pnpm-store/

# 构建产物
dist/
.astro/

# 环境变量
.env
.env.local
.env.production

# 日志
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*

# 操作系统
.DS_Store
Thumbs.db

# 编辑器
.vscode/
.idea/
*.swp
*.swo
*~

# 临时文件
*.log
.cache/

4. 提交代码

# 添加所有文件
git add .

# 查看状态
git status

# 提交
git commit -m "feat: 迁移到 Astro 博客框架

- 使用 vhAstro-Theme 主题
- 配置网站信息和备案
- 添加 TOC 目录功能
- 迁移所有文章内容"

5. 推送到 Gitee

# 首次推送(会创建远程分支)
git push -u origin astro

# 如果需要输入用户名密码,使用 Gitee 账号

6. 如果推送失败(需要先拉取)

# 拉取远程仓库
git pull origin master --allow-unrelated-histories

# 然后再推送
git push -u origin astro

后续更新流程

以后更新博客:

# 添加修改
git add .

# 提交
git commit -m "update: 更新内容描述"

# 推送
git push

如果想保留 Hexo 分支

# 查看所有分支
git branch -a

# master 分支保留 Hexo 内容
# astro 分支是新的 Astro 博客

配置 Git 用户信息(如果还没配置)

git config --global user.name "你的名字"
git config --global user.email "your-email@example.com"

常见问题

如果提示认证失败

Gitee 可能需要使用个人访问令牌

  1. 访问:https://gitee.com/profile/personal_access_tokens
  2. 生成新令牌
  3. 使用令牌作为密码

或者配置 SSH:

# 生成 SSH 密钥
ssh-keygen -t rsa -C "your-email@example.com"

# 查看公钥
cat ~/.ssh/id_rsa.pub

# 复制公钥,添加到 Gitee SSH 设置

然后改用 SSH 地址:

git remote set-url origin git@gitee.com:xxx.git

验证步骤

执行每步后检查:

# 1. 查看当前分支
git branch

# 2. 查看远程仓库
git remote -v

# 3. 查看提交历史
git log --oneline

# 4. 查看状态
git status

9. 使用 Git 自动化部署(推荐)

完美!✅ 现在继续配置 Webhook。

第1步:安装 webhook 工具

sudo apt update
sudo apt install webhook -y

# 验证安装
webhook --version

第2步:生成强密钥

# 生成一个强密钥用于 Webhook 验证
openssl rand -hex 32

# 记下这个密钥,例如:
# a8f5e2c9d3b7f1e4a6c8d9e2f3a1b5c7d4e6f8a1b2c3d4e5f6a7b8c9d0e1f2a3

把生成的密钥保存好,待会要用。

第3步:创建部署脚本

# 创建脚本目录
sudo mkdir -p /var/www/deploy-scripts

# 创建部署脚本
sudo vim /var/www/deploy-scripts/deploy-blog.sh

粘贴以下内容:

#!/bin/bash

# 日志文件
LOG_FILE="/var/log/astro-blog-deploy.log"
REPO_DIR="/var/www/astro-blog"

# 记录开始
echo "========================================" >> $LOG_FILE
echo "部署开始: $(date)" >> $LOG_FILE
echo "触发者: Gitee Webhook" >> $LOG_FILE

# 切换到项目目录
cd $REPO_DIR || {
    echo "ERROR: 无法进入项目目录" >> $LOG_FILE
    exit 1
}

# 验证 Git 仓库完整性
echo "验证仓库完整性..." >> $LOG_FILE
if ! git fsck --no-progress >> $LOG_FILE 2>&1; then
    echo "ERROR: Git 仓库损坏,终止部署" >> $LOG_FILE
    exit 1
fi

# 拉取代码
echo "拉取代码..." >> $LOG_FILE
git fetch origin astro-blog >> $LOG_FILE 2>&1
git reset --hard origin/astro-blog >> $LOG_FILE 2>&1

# 清理旧依赖
echo "清理依赖..." >> $LOG_FILE
rm -rf node_modules >> $LOG_FILE 2>&1

# 安装依赖
echo "安装依赖..." >> $LOG_FILE
pnpm install --frozen-lockfile >> $LOG_FILE 2>&1

# 审计依赖漏洞
echo "审计依赖..." >> $LOG_FILE
pnpm audit --audit-level=high >> $LOG_FILE 2>&1 || {
    echo "WARNING: 发现高危漏洞" >> $LOG_FILE
}

# 构建
echo "构建项目..." >> $LOG_FILE
pnpm build >> $LOG_FILE 2>&1

# 验证构建产物
if [ ! -d "dist" ]; then
    echo "ERROR: 构建失败,dist目录不存在" >> $LOG_FILE
    exit 1
fi

# 设置正确的权限
# chown -R blogdeploy:www-data dist/
# chmod -R 755 dist/

echo "部署成功: $(date)" >> $LOG_FILE
echo "========================================" >> $LOG_FILE

保存后设置权限:

# 给脚本执行权限
sudo chmod +x /var/www/deploy-scripts/deploy-blog.sh

# 设置所有者为 blogdeploy
sudo chown blogdeploy:blogdeploy /var/www/deploy-scripts/deploy-blog.sh

# 创建日志文件
sudo touch /var/log/astro-blog-deploy.log
sudo chown blogdeploy:blogdeploy /var/log/astro-blog-deploy.log

第4步:测试部署脚本

# 以 blogdeploy 用户身份测试脚本
sudo -u blogdeploy /var/www/deploy-scripts/deploy-blog.sh

# 查看日志
tail -30 /var/log/astro-blog-deploy.log

如果成功,会看到 “部署成功” 的消息。

如果解决构建错误

(Astro 想访问 /nonexistent/.config,但 blogdeploy 没有家目录)。

# 给 blogdeploy 创建配置目录
sudo mkdir -p /home/blogdeploy/.config
sudo chown -R blogdeploy:blogdeploy /home/blogdeploy
sudo usermod -d /home/blogdeploy blogdeploy

# 或者禁用 Astro 遥测
cd /var/www/astro-blog
sudo -u blogdeploy npx astro telemetry disable

# 再次测试部署
sudo -u blogdeploy /var/www/deploy-scripts/deploy-blog.sh

测试成功后继续!

blogdeploy 用户安装独立的 pnpm:

bash

# 1. 为 blogdeploy 创建 home 目录(如果还没有)
sudo mkdir -p /home/blogdeploy
sudo chown blogdeploy:blogdeploy /home/blogdeploy

# 2. 切换到 blogdeploy 用户并安装 pnpm
sudo -u blogdeploy bash << 'EOF'
cd ~
curl -fsSL https://get.pnpm.io/install.sh | sh -
source ~/.bashrc
pnpm --version
EOF

# 3. 验证 pnpm 可用
sudo -u blogdeploy bash -c 'export PNPM_HOME="/home/blogdeploy/.local/share/pnpm"; export PATH="$PNPM_HOME:$PATH"; pnpm --version'

# 4. 更新部署脚本(使用上面方案2的脚本内容)

# 5. 修复 Git 警告
sudo chown -R blogdeploy:blogdeploy /home/blogdeploy/.config

# 6. 测试脚本
sudo -u blogdeploy /var/www/deploy-scripts/deploy-blog-fast.sh

# 7. 查看日志
tail -30 /var/log/astro-blog-deploy.log

# 8. 重启 webhook 服务
sudo systemctl restart webhook

执行完这些步骤后,再推送一次代码测试。这次应该就能成功构建了!🚀

优化脚本(跳过依赖安装)

创建一个快速部署脚本:

sudo vim /var/www/deploy-scripts/deploy-blog-fast.sh
#!/bin/bash

# 设置完整的环境变量
export HOME=/home/blogdeploy
export NVM_DIR="$HOME/.nvm"
export PATH="$HOME/.npm-global/bin:$PATH"

# 加载 nvm 环境
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# 日志文件
LOG_FILE="/var/log/astro-blog-deploy.log"
REPO_DIR="/var/www/astro-blog"

# 记录开始
{
echo "========================================"
echo "快速部署开始: $(date)"
echo "用户: $(whoami)"
echo "HOME: $HOME"
echo "PATH: $PATH"
echo "NVM_DIR: $NVM_DIR"
echo "Node版本: $(node --version 2>&1)"
echo "npm版本: $(npm --version 2>&1)"
echo "PNPM位置: $(which pnpm 2>&1)"
echo "PNPM版本: $(pnpm --version 2>&1)"
} >> $LOG_FILE 2>&1

# 切换到项目目录
cd $REPO_DIR || {
    echo "ERROR: 无法进入项目目录 $REPO_DIR" >> $LOG_FILE
    exit 1
}

# 拉取代码
echo "拉取代码..." >> $LOG_FILE
git fetch origin astro-blog >> $LOG_FILE 2>&1

# 检查是否有更新
LOCAL=$(git rev-parse HEAD 2>/dev/null)
REMOTE=$(git rev-parse origin/astro-blog 2>/dev/null)

if [ "$LOCAL" = "$REMOTE" ] && [ -n "$LOCAL" ]; then
    echo "没有更新,跳过部署" >> $LOG_FILE
    exit 0
fi

git reset --hard origin/astro-blog >> $LOG_FILE 2>&1

# 清理旧的构建文件
echo "清理旧构建..." >> $LOG_FILE
rm -rf dist >> $LOG_FILE 2>&1

echo "构建项目..." >> $LOG_FILE
pnpm run build 2>&1 | tee -a $LOG_FILE

# 验证构建产物
if [ ! -d "dist" ]; then
    echo "ERROR: 构建失败,dist 目录未生成" >> $LOG_FILE
    echo "尝试使用完整构建命令..." >> $LOG_FILE
    # 尝试使用 npx
    npx astro build 2>&1 | tee -a $LOG_FILE
fi

# 再次验证
if [ ! -d "dist" ]; then
    echo "ERROR: 构建失败" >> $LOG_FILE
    exit 1
fi

echo "构建成功!" >> $LOG_FILE
echo "构建文件:" >> $LOG_FILE
ls -la dist/ 2>&1 | head -10 >> $LOG_FILE

echo "快速部署成功: $(date)" >> $LOG_FILE
echo "========================================" >> $LOG_FILE

设置权限:

sudo chmod +x /var/www/deploy-scripts/deploy-blog-fast.sh
sudo chown blogdeploy:blogdeploy /var/www/deploy-scripts/deploy-blog-fast.sh

第5步:配置 Webhook

这里用的gitee的hook,每个代码仓库的hook规则可能不一样,需要翻看文档

# 创建 webhook 配置文件
sudo vim /etc/webhook.conf

粘贴(记得把密钥改成你自己的):

[
  {
    "id": "deploy-blog",
    "execute-command": "/var/www/deploy-scripts/deploy-blog-fast.sh",
    "command-working-directory": "/var/www/astro-blog",
    "response-message": "正在部署博客,请稍候...",
    "trigger-rule": {
      "and": [
        {
          "match": {
            "type": "value",
            "value": "你的密码",
            "parameter": {
              "source": "header",
              "name": "X-Gitee-Token"
            }
          }
        },
        {
          "match": {
            "type": "value",
            "value": "refs/heads/astro-blog",
            "parameter": {
              "source": "payload",
              "name": "ref"
            }
          }
        }
      ]
    }
  }
]

第6步:创建 Webhook systemd 服务

sudo vim /etc/systemd/system/webhook.service

使用全局安装就不需要指定变量了

[Unit]
Description=Webhook Service for Blog Auto Deploy
After=network.target

[Service]
Type=simple
User=blogdeploy
Group=blogdeploy
Environment="HOME=/home/blogdeploy"
ExecStart=/usr/bin/webhook -hooks /etc/webhook.conf -verbose -port 9000
Restart=always
RestartSec=10

# 安全限制
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/astro-blog /var/log /home/blogdeploy

[Install]
WantedBy=multi-user.target

第7步:启动 Webhook 服务

# 重载 systemd
sudo systemctl daemon-reload

# 启动服务
sudo systemctl start webhook

# 设置开机自启
sudo systemctl enable webhook

# 查看状态
sudo systemctl status webhook

# 查看日志
sudo journalctl -u webhook -f

应该看到类似:

● webhook.service - Webhook Service for Blog Auto Deploy
   Loaded: loaded
   Active: active (running)

继续下一步!🚀

修改 Webhook 配置使用快速脚本

sudo vim /etc/webhook.conf

"execute-command" 改为:

{
  "id": "deploy-blog",
  "execute-command": "/var/www/deploy-scripts/deploy-blog-fast.sh",
  ...
}

重启 Webhook 服务

sudo systemctl restart webhook
sudo systemctl status webhook

现在有两个脚本

  1. 快速部署(只更新文章时用):deploy-blog-fast.sh - 只拉代码 + 构建
  2. 完整部署(更新依赖时用):deploy-blog.sh - 重装依赖 + 构建

Webhook 用快速的,手动完整部署时:

sudo -u blogdeploy /var/www/deploy-scripts/deploy-blog.sh

测试快速脚本

sudo -u blogdeploy /var/www/deploy-scripts/deploy-blog-fast.sh
tail -20 /var/log/astro-blog-deploy.log

应该几十秒就完成了!继续配置 Gitee Webhook 吧?🚀

关于 Git 警告

顺便修复 Git 的权限警告:

# 创建配置目录
sudo mkdir -p /home/blogdeploy/.config/git

# 创建空文件
sudo touch /home/blogdeploy/.config/git/ignore
sudo touch /home/blogdeploy/.config/git/attributes

# 设置权限
sudo chown -R blogdeploy:blogdeploy /home/blogdeploy/.config
sudo chmod -R 755 /home/blogdeploy/.config
# 确保目录存在并有正确权限
sudo mkdir -p /home/blogdeploy/.config/git
sudo touch /home/blogdeploy/.config/git/ignore
sudo touch /home/blogdeploy/.config/git/attributes
sudo chown -R blogdeploy:blogdeploy /home/blogdeploy
sudo chmod -R 755 /home/blogdeploy

配置 Gitee Webhook

在服务器安全组开放端口

登录你的服务器控制台(阿里云/腾讯云等):

  1. 找到”安全组规则”
  2. 添加入站规则:
    • 端口:9000
    • 协议:TCP
    • 来源:0.0.0.0/0(或只允许 Gitee IP)

第1步:访问 Gitee Webhook 设置

打开浏览器,访问:

https://gitee.com/bugatti100Peagle/my-blog/hooks

第2步:添加 WebHook

点击页面上的 “添加 WebHook”“管理 WebHook”“添加”

第3步:填写配置

填写以下信息:

http://你的服务器IP:9000/hooks/deploy-blog

例如:http://123.45.67.89:9000/hooks/deploy-blog

密码令牌

粘贴你之前生成的密钥(openssl rand -hex 32 那个)

如果忘了,重新生成一个:

openssl rand -hex 32

就是/etc/webhook.conf 里的 secret

事件选择
  • 勾选 Push
  • ⬜ 其他不勾选
其他选项
  • SSL 验证:关闭(因为用的是 HTTP)
  • 激活 WebHook:✅ 勾选

第4步:保存并测试

点击 “添加”“确定”

然后点击刚创建的 Webhook 右侧的 “测试” 按钮

验证是否成功

方法1:查看 Gitee 界面

测试后,Gitee 会显示:

  • 成功:状态码 200,响应 “正在部署博客,请稍候…”
  • 失败:显示错误信息

方法2:查看服务器日志

# 查看 webhook 日志
sudo journalctl -u webhook -f

# 查看部署日志
tail -f /var/log/astro-blog-deploy.log

如果成功,你会看到类似:

[webhook] 2025/12/13 xx:xx:xx [deploy-blog] 200 | deploy-blog got matched
[webhook] 2025/12/13 xx:xx:xx [deploy-blog] executing /var/www/deploy-scripts/deploy-blog-fast.sh

第5步:真实测试

在本地修改一篇文章,然后:

git add .
git commit -m "test: 测试自动部署"
git push origin astro-blog

然后:

  1. 观察服务器日志:tail -f /var/log/astro-blog-deploy.log
  2. 应该会自动开始部署
  3. 完成后访问你的网站验证更新

常见问题

如果 Gitee 提示连接失败

# 确认 webhook 服务正在运行
sudo systemctl status webhook

# 确认端口开放
sudo netstat -tlnp | grep 9000

# 测试本地访问
curl http://localhost:9000/hooks/deploy-blog

如果 Gitee 提示 403 或验证失败

检查密钥是否一致:

# 查看配置文件中的密钥
sudo cat /etc/webhook.conf | grep secret

确保和 Gitee 填的一样。

修复 dist 目录权限

# 1. 确保 blogdeploy 对整个项目目录有写权限
sudo chown -R blogdeploy:blogdeploy /var/www/astro-blog

# 2. 特别是 dist 目录
sudo rm -rf /var/www/astro-blog/dist
sudo mkdir -p /var/www/astro-blog/dist
sudo chown -R blogdeploy:blogdeploy /var/www/astro-blog/dist

# 3. 确保 node_modules 也有正确权限
sudo chown -R blogdeploy:blogdeploy /var/www/astro-blog/node_modules

# 4. 测试构建
sudo -u blogdeploy bash /var/www/deploy-scripts/deploy-blog-fast.sh

# 5. 查看日志
tail -50 /var/log/astro-blog-deploy.log

安全增强(可选)

如果还想更安全,可以加上 IP 白名单:

{
  "trigger-rule": {
    "and": [
      {
        "match": {
          "type": "value",
          "value": "你的密码",
          "parameter": {
            "source": "header",
            "name": "X-Gitee-Token"
          }
        }
      },
      {
        "match": {
          "type": "value",
          "value": "refs/heads/astro-blog",
          "parameter": {
            "source": "payload",
            "name": "ref"
          }
        }
      },
      {
        "match": {
          "type": "ip-whitelist",
          "ip-range": "180.97.0.0/16,116.211.0.0/16"
        }
      }
    ]
  }
}

10. 监控和维护

查看 Nginx 日志

# 实时查看访问日志
sudo tail -f /var/log/nginx/access.log

# 查看错误日志
sudo tail -f /var/log/nginx/error.log

创建服务状态检查脚本

#!/bin/bash
# check-status.sh

echo "=== 系统状态检查 ==="
echo "Nginx 状态:"
sudo systemctl status nginx --no-pager

echo -e "\n磁盘使用:"
df -h

echo -e "\n内存使用:"
free -h

echo -e "\n最近访问日志:"
sudo tail -20 /var/log/nginx/access.log

12. 常用命令总结

# 查看 Nginx 状态
sudo systemctl status nginx

# 重启 Nginx
sudo systemctl restart nginx

# 重载配置
sudo systemctl reload nginx

# 查看 Nginx 配置语法
sudo nginx -t

# 查看网站访问
sudo tail -f /var/log/nginx/access.log

13. 备份脚本

#!/bin/bash
# backup.sh

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/home/username/backups"
SOURCE_DIR="/var/www/myblog"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 备份网站文件
tar -czf $BACKUP_DIR/myblog_$DATE.tar.gz $SOURCE_DIR

# 备份数据库(如果有)
# mysqldump -u username -p dbname > $BACKUP_DIR/db_$DATE.sql

# 保留最近7天备份
find $BACKUP_DIR -name "myblog_*.tar.gz" -mtime +7 -delete

echo "备份完成: $BACKUP_DIR/myblog_$DATE.tar.gz"

14. 问题排查

常见问题解决:

  1. 403 Forbidden 错误
# 检查文件权限
sudo chown -R www-data:www-data /var/www/myblog
sudo chmod -R 755 /var/www/myblog
  1. 502 Bad Gateway 错误
# 检查端口占用
sudo netstat -tulpn | grep :80
sudo systemctl restart nginx
  1. 内存不足
# 增加 swap
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

部署流程总结:

  1. ✅ 服务器:Ubuntu 24.04 LTS
  2. ✅ Web服务器:Nginx
  3. ✅ 运行环境:Node.js (nvm)
  4. ✅ 项目目录:/var/www/myblog
  5. ✅ SSL证书:Let’s Encrypt
  6. ✅ 自动化:Git hooks
  7. ✅ 监控:日志和 systemd
服务器 博客搭建 Astro