为什么是 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:

  1. 谷歌账号开两步验证,然后生成应用专用密码(注意:要在浏览器里打开 myaccount.google.com/apppasswords,手机 Google App 里找不到这个入口)

  2. Halo 后台 → 设置 → 通知设置,填:

    • SMTP:smtp.gmail.com,端口 465,加密 SSL

    • 用户名:你的 Gmail 地址

    • 密码:刚才生成的 16 位应用专用密码(去掉中间空格)

  3. 点"测试邮箱",收到测试邮件即成功

七、每日自动备份

备份脚本 /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 渲染出来才算;备份脚本写了不算数,手动跑一遍、解包验内容才算。今晚一共踩了三个坑,每个都记下来了,下次再搭就顺了。