为什么是 Halo
一直想有个自己的小站,写写文章、放放个人主页。看了一圈建站方案,最后选了 Halo:国产开源、中文后台、Docker 一键部署,对新手友好,本体免费。服务器是手头一台 Debian 系的 VPS。
最终架构
访客 → 博客域名 → Cloudflare Tunnel → VPS 本机 8090 → Halo → MySQL 8.0
1Panel V2:服务器管理面板(管容器、文件、备份)
Halo 2:博客本体,跑在 Docker 里
MySQL 8.0:生产数据库,替代 Halo 默认的 H2
Cloudflare Tunnel:内网穿透,博客端口不对公网开放
自动备份:每天凌晨备份数据库 + 附件,保留 7 天
一、安装 1Panel
先确认端口没被占用:
ss -tlnp
然后跑官方安装脚本。注意 V2 安装程序默认端口是 40120,我改成了 28888(跟另一台机器统一)。装完记好面板地址、安全入口和账号密码。
二、Docker 部署 Halo
docker run -d --name halo \
-p 8090:8090 \
-v /opt/halo2:/root/.halo2 \
--restart unless-stopped \
halohub/halo:2 \
--halo.external-url=https://你的博客域名/
跑起来后看日志,出现 Started Application 就是成功了。此时 / 会返回 404(还没装主题),/console 返回 302(等初始化),都是正常的。
浏览器打开 http://你的服务器IP:8090/console 完成初始化:设站点标题、管理员账号密码。初始化完成后记得关掉 8090 的防火墙放行,后面全走隧道。
三、Cloudflare Tunnel 内网穿透
不想把博客端口暴露在公网,就用 Cloudflare Tunnel。思路:VPS 上跑一个 cloudflared 客户端,连到 Cloudflare,公网流量经隧道进来。
docker run -d --name cloudflared --network host --restart always \
cloudflare/cloudflare-docker \
tunnel --no-autoupdate run --token 你的隧道token
然后在 Cloudflare 后台给隧道加一条 public hostname:你的博客域名 → http://localhost:8090。全程 HTTPS,Cloudflare 自动配证书。
四、踩坑实录
坑 1:复用隧道导致 502
一开始图省事,把博客的 hostname 加到了已有的隧道上,而那个隧道同时连着两台服务器的客户端。Cloudflare 会随机分流,一半请求落到另一台没有博客服务的机器上 → 502。
教训:一个隧道只服务一台机器。老老实实新建独立隧道,问题解决。
坑 2:H2 数据库导致首页 500
上线后首页正文区报 500,日志是 H2 读取正文 CLOB 超时。docker restart halo 后恢复。这也坚定了换 MySQL 的决心——Halo 官方本来就不建议 H2 跑生产。
坑 3:换库后主题"已存在"
切到 MySQL 是空库,但 /opt/halo2 数据卷沿用了旧文件,里面残留着 H2 时代的主题目录,新库里却没有记录,装主题时报"已存在"。删掉残留的旧主题目录后重装即可。
五、从 H2 切换到 MySQL
先建一个 Docker 网络让两个容器互通:
docker network create halo-net
起 MySQL 8.0(不映射端口到公网,只在内部网络用):
docker run -d --name halodb --network halo-net \
-v /opt/halodb:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=你的root密码 \
-e MYSQL_DATABASE=halo -e MYSQL_USER=halo -e MYSQL_PASSWORD=你的密码 \
--restart unless-stopped \
mysql:8.0
重建 Halo 接入 MySQL(参数按官方文档):
docker run -d --name halo --network halo-net \
-p 127.0.0.1:8090:8090 \
-v /opt/halo2:/root/.halo2 \
--restart unless-stopped \
halohub/halo:2 \
--spring.r2dbc.url=r2dbc:pool:mysql://halodb:3306/halo \
--spring.r2dbc.username=halo \
--spring.r2dbc.password=你的密码 \
--spring.sql.init.platform=mysql \
--halo.external-url=https://你的博客域名/
注意这次 8090 只绑了 127.0.0.1,连本机其他网卡都不监听,彻底不对外。之后重新走一遍初始化即可。
六、配置邮件发送(Gmail SMTP)
Halo 默认发不出邮件,改邮箱、收评论通知验证码都得先配 SMTP。我用的是 Gmail:
谷歌账号开两步验证,然后生成应用专用密码(注意:要在浏览器里打开 myaccount.google.com/apppasswords,手机 Google App 里找不到这个入口)
Halo 后台 → 设置 → 通知设置,填:
SMTP:
smtp.gmail.com,端口465,加密 SSL用户名:你的 Gmail 地址
密码:刚才生成的 16 位应用专用密码(去掉中间空格)
点"测试邮箱",收到测试邮件即成功
七、每日自动备份
备份脚本 /opt/backup/backup-halo.sh,每天凌晨 3 点跑,留 7 天:
#!/bin/bash
D=$(date +%F)
B=/opt/backup
docker exec halodb sh -c 'exec mysqldump --no-tablespaces -uhalo -p"$MYSQL_PASSWORD" halo' > $B/halo-db-$D.sql
tar -czf $B/halo-data-$D.tar.gz -C /opt halo2 2>/dev/null
find $B -name 'halo-*' -mtime +7 -delete
密码从容器内的环境变量取,不落盘明文。定时任务:
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/backup/backup-halo.sh") | crontab -
一个小经验:终端粘贴多行脚本容易粘连(我的 FinalShell 就中招过),可以先把脚本转成 base64 单行再粘贴写入,稳得多。
写在最后
整个折腾下来,最大的感受是:链路验收要走到最后。隧道通了不算通,首页 200 渲染出来才算;备份脚本写了不算数,手动跑一遍、解包验内容才算。今晚一共踩了三个坑,每个都记下来了,下次再搭就顺了。
评论