工作流

目前我的工作流是:本地hexo d推送部署到github上,然后通过github action部署到github pages 和vercel上。同时通过脚本将public文件夹下的静态文件同步推送到我的ECS服务器上。

也就是说,可以通过三个域名访问到我的博客,一个是https://lloyd-kai.github.io/ 源站点,一个是https://blog.lloydkai.top/ 部署到vercel上的(我已解析为自己购买的域名),一个是https://lloydkai.cn/ 部署到阿里云的ECS上的(方便国内的用户访问)。

部署时参考的教程

部署到vercel上

零成本免备案部署博客——vercel与zeabur搭建hexo主题博客 , 第2期:Vercel部署并绑定自定义域名+安装Butterfly主题 。这两个视频互相补充,主要参考第二个视频即可部署成功

部署到gihub的pages上

请看hexo博客系列(二)博客创建指南 ,严格来说我不是将hexo博客项目中所有的内容推送到github的,而是用hexo-deployer-git插件在本地生成public下的html文件,再推送到github pages上的。所以你会看到其他的的教程中它是先将整个hexo项目先推送到github,然后再将分支推送到github pages上,而我只上传必要的文件到github pages上,所有的文件都在我的本地电脑里面。主要的参考教程:Fomalhaut的博客,核心就在于是否下载了hexo-deployer-git插件以及配置ssh密钥等

官方hexo-deployer-git教程

部署到阿里云 ECS 上

主要参考的教程:链接

以下是根据我自己的实际使用情况修改,我使用的是阿里云 ECS,系统为 Ubuntu。

1. 连接阿里云服务器

没有上传 SSH 密钥之前,可以先用下面的命令登录,再输入密码:

1
ssh -l root 你的ECS公网IP -p 22

如果后面已经配置好了 SSH 密钥,也可以直接使用:

1
ssh root@你的ECS公网IP

2. 设置阿里云防火墙与安全组

这里最容易出问题的是 SSH 的来源 IP 白名单。我的电脑当前出口公网 IP 会变化,所以如果你开了代理或切换了网络,安全组里的放行地址也要一起更新。

如果你发现把 SSH 安全组临时放开为 0.0.0.0/0 就能连上,那基本可以确定问题出在“来源 IP 不匹配”,而不是 ECS 本身坏了。

3. 配置 Nginx 启用 HTTPS

Nginx 这里我只保留静态站点的基本配置,目录直接指向 /var/www/html/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;

root /var/www/html;
index index.html;

location / {
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name 你的域名 www.你的域名;
ssl_certificate /etc/nginx/ssl/你的域名.pem;
ssl_certificate_key /etc/nginx/ssl/你的域名.key;
root /var/www/html;
index index.html;

location / {
}
}

我这里不再使用 GitHub Actions 来同步 ECS,而是改成 Hexo 的 deployAfter 钩子执行脚本上传。

4. 自定义 ECS 部署脚本

我的实现思路是:

  1. hexo d 完成 Git 部署后,再自动执行脚本上传 public/*
  2. 脚本先获取当前电脑的出口公网 IP,并在失败时提示去检查安全组来源地址
  3. 上传后自动修复目录与文件权限,避免 Nginx 读取静态资源异常
  4. Nginx 直接托管 /var/www/html/ 的静态文件

这样就不需要 GitHub Actions 参与 ECS 同步了,但 hexo d 会多执行一步上传,所以整体会稍慢一些。

首先,在 _config.yml 文件中添加配置:

1
2
3
4
5
6
7
# 本地执行 `hexo d` 后,自动通过脚本上传到阿里云 ECS
ecs_deploy:
enable: true
host: 你的ECS公网IP
user: root
target: /var/www/html/
# 如果你想显式记录当前出口公网 IP,对照安全组时更直观,可以再补充一个来源 IP 字段

然后在 Hexo 项目根目录下新建脚本 scripts/deploy-ecs.js

这里我是使用https://cip.cc/ 网站查询本机真正的IP,因为我的IP是不定的,所以需要查找确认,如果你嫌麻烦可以直接在服务器的安全组里面的SSH中填写IP为0.0.0.0/0 也就是任何IP都可连接。

这个脚本的核心逻辑是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
'use strict';

const { spawnSync } = require('child_process');
const https = require('https');

const PUBLIC_IP_CHECK_URL = 'https://cip.cc/';

function getPublicIp() {
return new Promise((resolve) => {
const request = https.get(PUBLIC_IP_CHECK_URL, (response) => {
if (response.statusCode !== 200) {
response.resume();
resolve(null);
return;
}

let body = '';
response.setEncoding('utf8');
response.on('data', (chunk) => {
body += chunk;
});
response.on('end', () => {
const match = body.match(/(?:查询\s*IP|IP)\s*[::]\s*([0-9]{1,3}(?:\.[0-9]{1,3}){3})/i);
resolve(match ? match[1] : null);
});
});

request.setTimeout(5000, () => {
request.destroy();
resolve(null);
});

request.on('error', () => resolve(null));
});
}

function runOrThrow(command, errorMessage) {
const result = spawnSync(command, {
shell: true,
stdio: 'inherit',
cwd: process.cwd()
});

if (result.status !== 0) {
throw new Error(`${errorMessage} (exit code ${result.status})`);
}
}

function logConnectionHints(host, port) {
hexo.log.warn(`[ecs-deploy] 连接失败:${host}:${port}`);
hexo.log.warn('[ecs-deploy] 请打开 https://cip.cc/ 查询当前电脑出口公网 IP');
hexo.log.warn('[ecs-deploy] 请先确认 ECS 的公网 IP 是否发生变化');
hexo.log.warn('[ecs-deploy] 请确认安全组和服务器防火墙已经放行 SSH 端口');
hexo.log.warn('[ecs-deploy] 如果 SSH 端口不是 22,请在 _config.yml 中更新 ecs_deploy.port');
}

hexo.on('deployAfter', async function () {
const cfg = hexo.config.ecs_deploy || {};
if (cfg.enable === false) {
hexo.log.info('[ecs-deploy] disabled by config');
return;
}

const host = cfg.host;
const user = cfg.user || 'root';
const port = cfg.port || 22;
const target = cfg.target || '/var/www/html/';
const sourceIp = cfg.source_ip || cfg.whitelist_ip || cfg.allowed_source_ip || '';

if (!host) {
hexo.log.warn('[ecs-deploy] skip: missing ecs_deploy.host in _config.yml');
return;
}

const currentPublicIp = await getPublicIp();

const source = 'public/*';
const destination = `${user}@${host}:${target}`;
const scpCommand = `scp -P ${port} -r ${source} ${destination}`;
const remoteFixPermsCommand = `ssh -p ${port} ${user}@${host} "find ${target} -type d -exec chmod 755 {} \\; ; find ${target} -type f -exec chmod 644 {} \\;"`;

if (currentPublicIp) {
hexo.log.info(`[ecs-deploy] 正在通过 scp 上传:来源IP ${currentPublicIp} -> 目标服务器 ${destination}`);
} else {
hexo.log.info(`[ecs-deploy] 正在通过 scp 上传:来源IP 未获取到 -> 目标服务器 ${destination}`);
}

try {
runOrThrow(scpCommand, '[ecs-deploy] scp 上传失败');
} catch (error) {
if (currentPublicIp && sourceIp) {
hexo.log.warn(`[ecs-deploy] 当前电脑出口公网 IP: ${currentPublicIp}`);
if (currentPublicIp !== sourceIp.replace(/\/\d+$/, '')) {
hexo.log.warn('[ecs-deploy] 两者不一致,请到阿里云安全组中修改允许访问 SSH 的来源 IP');
}
} else if (currentPublicIp) {
hexo.log.warn(`[ecs-deploy] 当前电脑出口公网 IP: ${currentPublicIp}`);
} else if (sourceIp) {
hexo.log.warn(`[ecs-deploy] 安全组允许的来源 IP: ${sourceIp}`);
hexo.log.warn('[ecs-deploy] 当前出口公网 IP 获取失败,请手动确认本机/VPN 的外网 IP 是否与安全组一致');
}
logConnectionHints(host, port);
throw error;
}

hexo.log.info('[ecs-deploy] fixing file permissions for nginx');
try {
runOrThrow(remoteFixPermsCommand, '[ecs-deploy] 远端权限修复失败');
} catch (error) {
hexo.log.warn('[ecs-deploy] SSH 已连接,但远端权限修复失败');
hexo.log.warn('[ecs-deploy] 请检查远端目标路径是否存在、目录权限是否正确');
throw error;
}

hexo.log.info('[ecs-deploy] upload finished');
});

为什么要做权限修复:

  • 某些环境下,scp 上传后目录权限可能变得过于严格
  • 如果 Nginx 无法读取 cssjsfont 等资源目录,就会出现样式缺失或者页面和本地预览不一致的问题

以后只需要正常在终端里输入下面的命令即可:

1
hexo cl;hexo g;hexo d

它会先完成 Hexo 的常规部署,再把生成的 public 目录同步到 ECS 上。

自动续签SSL证书

参考链接 ,使用了Let’s Encrypt自动续签,建议使用阿里云,每年免费额度有20个证书,有效时间为三个月,基本够用了。

SSL证书其实就是安全性的问题,如果没有签的话在访问网站的时候,浏览器就会显示

您与此网站之间建立的连接不安全,请勿在此网站上输入任何敏感信息(例如密码或信用卡信息),因为攻击者可能会盗取这些信息。 了解详情

自动续签时间是60天一次,一年大概就6个。如果需要就自己修改时间。

如何备案

官方文档

总体来说流程有五个——填表申请、阿里云初审、短信验证、管理审核、ICP出结果和公安备案,ICP出结果之后就要马上将备案信息添加到首页之下,包括公安的备案(出结果后30天内),如果不添加就会有罚款。

官方表述

ICP备案成功后,您需要在网站首页底部悬挂ICP备案号并生成链接指向工信部网站(https://beian.miit.gov.cn/),否则被相关部门核查出来,将会面临罚款。

参考资料

  1. 阿里云备案官方文档
  2. 链接