将 Halo 博客从内置 H2 数据库迁移到独立 MySQL 8.0 容器,以及后续维护运行

1. YAML 语法与解析错误

  • 问题现象:go-yaml load error ... did not find expected key。

  • 根本原因:编辑 docker-compose.yml 时混用了制表符(Tab)与空格,或键值对缩进层级不一致、冒号后漏加空格。

  • 解决方式:规范 YAML 缩进,全量替换为统一 2 空格缩进的配置。

2. 容器生命周期竞争(Race Condition)

  • 问题现象:Halo 启动直接抛出 Connection refused: halo-mysql/172.20.0.2:3306。

  • 根本原因:MySQL 8.0 首次挂载空卷时,需要 20~40 秒在后台初始化系统表与数据目录,此时 3306 端口未对外开放监听。Compose 默认的 depends_on 仅等待容器创建即拉起 Halo,Halo 抢先连接未就绪的端口而崩溃退出。

  • 解决方式:在 halo-mysql 服务中配置 healthcheck(mysqladmin ping),并将 Halo 的依赖条件设定为 condition: service_healthy,确保 MySQL 真正可用后再启动博客。

3. MySQL 容器权限与 NUMA 警告刷屏

  • 问题现象:MySQL 启动日志中大量刷出 mbind: Operation not permitted。

  • 根本原因:Docker 默认的 seccomp 安全沙箱拦截了 MySQL 尝试调用底层内存策略(set_mempolicy / mbind)的系统调用。

  • 解决方式:在配置中加入 security_opt: [seccomp:unconfined],放行系统调用,避免初始化停滞。

4. 数据库迁移方言冲突(最核心深层根因)

  • 问题现象:MySQL 连接成功后,Halo 依然报错崩溃:io.r2dbc.spi.R2dbcBadGrammarException: [1064] You have an error in your SQL syntax ... near '"migrations"'。

  • 根本原因:

    1. 旧的工作目录挂载(./data)残留了之前的 H2 运行配置与状态。

    2. 启动命令行(command:)传参优先级问题,导致底层 r2dbc-migrate 组件依然锁定了 dialect=H2,生成了 H2 双引号语法并向 MySQL 执行,直接引发 1064 语法错误。

  • 解决方式:

    • 将旧的 data 目录改名隔离为纯净目录。

    • 弃用 command 传参,改用标准环境变量 SPRING_SQL_INIT_PLATFORM=mysql 与 SPRING_R2DBC_* 显式声明方言与连接平台。

5. 云服务器内存击穿(OOM)导致 SSH 失联

  • 问题现象:执行重启与数据导入过程中,服务器终端彻底断开且无法重新连入。

  • 根本原因:在 2GB 内存的云主机上,MySQL 8.0 与 Java 21 (Spring Boot) 在数据恢复/启动时产生瞬时内存峰值,物理内存耗尽触发内核保护,导致 SSH 守护进程无响应或假死。

  • 解决方式:

    • 通过云控制台完成实例重启恢复连通。

    • 为系统创建并挂载 2GB Swap 虚拟内存(/swapfile),提供了充足的突发内存缓冲空间。

6. 网关 502 与大文件反代拦截

  • 问题现象:

    • 浏览器提示 502 Bad Gateway (openresty)。

    • 后台上传备份文件连续失败。

  • 根本原因:

    • 502 是因为后端 Halo 容器在崩溃退出或还在初始化的空档期,反代网关找不到上游服务。

    • 上传失败是因为反代网关(OpenResty/Nginx)默认限制了客户端请求体大小(client_max_body_size 默认 1M~10M)。

  • 解决方式:绕过前端 HTTP 上传,直接将备份包移入挂载的 /opt/halo/data/backups/ 目录,在控制台直接触发本地恢复。

梳理下来,这次排查涵盖了容器编排、数据库引擎初始化、框架方言适配、Linux 内存管理以及网络反代的完整排障闭环。目前整套架构运行在健康检查与持久化卷之上,整体已经处于非常健壮的状态。

docker-compose.yml文件的配置:

Halo 启动得比 MySQL 快。depends_on 只会等待 MySQL 的容器「启动」,并不会等 MySQL 真正「就绪」(尤其第一次启动时 MySQL 8.0 要初始化数据目录,需要几十秒)。于是 Halo 第一次启动时连不上 MySQL,直接崩溃:

2026-10-05T20:49:31 ... Application run failed

Failed to obtain R2DBC Connection → Connection refused

然后因为 restart: unless-stopped,它重启一次,这次 MySQL 已经好了,于是第二次就正常启动了(日志 788 行:StartedApplication in 30.275 seconds、777 行:Netty started on port 8090)。

502 就是这段崩溃窗口里(以及每次重启时都会复现)反代(nginx/Caddy)找不到后端 8090 端口导致的。

#停止当前容器:
docker compose down
#编辑配置文件:
nano docker-compose.yml

docker-compose.yml:

services:
  halo-mysql:
    image: mysql:8.0
    container_name: halo-mysql
    restart: unless-stopped
    security_opt:
      - seccomp:unconfined
    command:
      - --default-authentication-plugin=mysql_native_password
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: "HaloUser123456"
      MYSQL_DATABASE: halo
      MYSQL_USER: halo
      MYSQL_PASSWORD: "HaloUser123456"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 30s
    volumes:
      - ./mysql-data:/var/lib/mysql
    networks:
      - default

  halo:
    image: ghcr.io/halo-dev/halo:2.24.2
    container_name: halo
    restart: unless-stopped
    depends_on:
      halo-mysql:
        condition: service_healthy
    ports:
      - "8090:8090"
    volumes:
      - ./data:/root/.halo2
    environment:
      - SPRING_R2DBC_URL=r2dbc:pool:mysql://halo-mysql:3306/halo
      - SPRING_R2DBC_USERNAME=halo
      - SPRING_R2DBC_PASSWORD=HaloUser123456
      - SPRING_SQL_INIT_PLATFORM=mysql
      - HALO_EXTERNAL_URL=https://originspace.work
    networks:
      - default

networks:
  default:
    name: web-net
    external: true

#等待重启
docker compose up -d

其中docker镜像加速使用阿里云官方提供的加速源:

#编辑配置文件
nano /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://e1bn0vyt.mirror.aliyuncs.com"
  ]
}
systemctl daemon-reload
systemctl restart docker