技术栈: Linux、Docker、Docker Compose、Halo、PostgreSQL、Caddy、HTTPS
部署环境: 腾讯云服务器、Rocky Linux、个人域名
搭建时间: 2026 年 10 月 31 日
博客地址: https://www.zhangshangwu.cyou

前言

这是我的第一篇博客,记录一下这个博客本身是如何搭建起来的。

我有一台腾讯云服务器和一个已经备案的个人域名,希望利用现有资源搭建一个属于自己的技术博客,主要用于记录 Linux、Docker、Kubernetes、SRE 等方面的学习笔记和实践项目。

最初考虑过 Typecho、Hugo、WordPress、Halo 等开源项目。最后选择了 Halo + PostgreSQL + Caddy,通过 Docker Compose 统一部署。

选择这套架构主要有几个原因:

  • Halo: 开源博客系统,拥有可视化管理后台、主题和插件生态,适合长期维护。

  • PostgreSQL: 负责持久化存储博客数据。

  • Caddy: 配置简单,支持自动申请和续期 HTTPS 证书。

  • Docker Compose: 将多个服务统一编排,便于部署、升级和迁移。

整个部署过程并非完全顺利,先后遇到了 Docker Hub 镜像拉取超时、域名 HTTPS 异常、Halo 首页返回 404 等问题。

本文记录完整的搭建过程,以及这些问题的排查和解决思路。


一、整体架构设计

首先看一下最终架构:

                  互联网用户
                      |
                      | HTTPS
                      v
          www.zhangshangwu.cyou
                      |
                      | DNS 解析
                      v
              腾讯云公网服务器
                      |
               TCP 80 / 443
                      |
       +-----------------------------+
       |       Docker Compose        |
       |                             |
       |    +-------------------+    |
       |    |       Caddy       |    |
       |    |  HTTPS / 反向代理  |    |
       |    +---------+---------+    |
       |              |              |
       |        HTTP :8090           |
       |              |              |
       |    +---------v---------+    |
       |    |       Halo        |    |
       |    |    博客应用服务    |    |
       |    +---------+---------+    |
       |              |              |
       |      PostgreSQL :5432       |
       |              |              |
       |    +---------v---------+    |
       |    |    PostgreSQL     |    |
       |    |      数据库       |    |
       |    +-------------------+    |
       |                             |
       +-----------------------------+
                      |
               宿主机磁盘
                /opt/myblog

整个系统由三个容器组成:

服务

镜像

作用

Halo

registry.fit2cloud.com/halo/halo:2.27

博客应用、管理后台、主题渲染

PostgreSQL

postgres:16

存储文章、用户和配置数据

Caddy

caddy:2

HTTPS、证书管理、反向代理

架构设计的核心原则:将入口服务、业务服务和数据库分离。

公网只开放 Caddy 的 80、443 端口,Halo 和 PostgreSQL 通过 Docker 内部网络通信,不需要直接暴露到公网。


二、服务器环境准备

2.1 基础环境

本次使用的环境:

项目

配置

云服务商

腾讯云

操作系统

Rocky Linux

容器运行时

Docker Engine

编排工具

Docker Compose

域名

zhangshangwu.cyou

博客主域名

www.zhangshangwu.cyou

首先检查服务器环境:

# 查看操作系统
cat /etc/os-release

# 查看内核版本
uname -r

# 查看 Docker 版本
docker --version

# 查看 Docker Compose 版本
docker compose version

# 查看端口监听情况
ss -lntp

本次部署时 Docker 已经安装完成,因此不再展开 Docker Engine 的安装步骤。

2.2 配置域名解析

在域名 DNS 控制台添加 A 记录:

主机记录

类型

记录值

www

A

服务器公网 IP

@

A

服务器公网 IP

其中:

  • www 对应 www.zhangshangwu.cyou。

  • @ 对应根域名 zhangshangwu.cyou。

通过以下命令验证:

dig +short www.zhangshangwu.cyou
dig +short zhangshangwu.cyou

如果返回服务器的公网 IP,说明 DNS 解析基本正确。

需要注意,DNS 解析成功只代表域名能够找到服务器,并不代表 HTTP、HTTPS 服务一定正常。

2.3 配置防火墙

在腾讯云安全组中放行:

  • TCP 80:HTTP。

  • TCP 443:HTTPS。

  • TCP 22:SSH,建议限制来源 IP。

如果 Rocky Linux 启用了 firewalld,还需要在系统层面放行 Web 服务:

firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

到这里,服务器的基本网络环境就准备好了。


三、创建 Docker Compose 项目

3.1 创建工作目录

统一使用 /opt/myblog 管理博客项目:

mkdir -p /opt/myblog
cd /opt/myblog

mkdir -p data

项目目录设计如下:

/opt/myblog/
├── compose.yaml
├── Caddyfile
├── .env
└── data/
    ├── postgres/
    ├── halo/
    ├── caddy/
    └── caddy-config/

各部分的作用:

  • compose.yaml:定义和编排容器。

  • Caddyfile:配置反向代理和 HTTPS。

  • .env:存储环境变量。

  • data/postgres:数据库持久化目录。

  • data/halo:Halo 工作目录。

  • data/caddy:Caddy 证书等数据。

将配置文件和持久化数据集中管理,可以降低后续维护和迁移的复杂度。

3.2 创建环境变量文件

先生成一个随机数据库密码:

openssl rand -hex 24

随后创建 .env:

vim /opt/myblog/.env

内容如下:

BLOG_DOMAIN=www.zhangshangwu.cyou
POSTGRES_PASSWORD=替换为随机生成的数据库密码

实际密码需要替换成生成的随机值,不能直接使用上面的占位文字。

为什么使用 .env?

因为 Docker Compose 支持通过 ${变量名} 引用环境变量。

例如:

environment:
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

这样不需要在 Compose 文件的多个位置重复编写密码。

不过,.env 只是配置文件,并不是加密存储。需要限制文件权限,并且避免上传到公开仓库。

chmod 600 /opt/myblog/.env

四、编写 Docker Compose 配置

创建配置文件:

vim /opt/myblog/compose.yaml

写入以下内容:

services:
  # PostgreSQL 数据库
  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: halo
      POSTGRES_USER: halo
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data:Z
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "halo", "-d", "halo"]
      interval: 10s
      timeout: 5s
      retries: 10

  # Halo 博客服务
  halo:
    image: registry.fit2cloud.com/halo/halo:2.27
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      JVM_OPTS: "-Xms256m -Xmx512m"
    volumes:
      - ./data/halo:/root/.halo2:Z
    command:
      - --spring.r2dbc.url=r2dbc:pool:postgresql://db:5432/halo
      - --spring.r2dbc.username=halo
      - --spring.r2dbc.password=${POSTGRES_PASSWORD}
      - --spring.sql.init.platform=postgresql
      - --halo.external-url=https://${BLOG_DOMAIN}/
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 60s

  # Caddy 反向代理
  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      halo:
        condition: service_healthy
    environment:
      BLOG_DOMAIN: ${BLOG_DOMAIN}
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro,Z
      - ./data/caddy:/data:Z
      - ./data/caddy-config:/config:Z

4.1 数据持久化:volumes

以 PostgreSQL 为例:

volumes:
  - ./data/postgres:/var/lib/postgresql/data:Z

这是 Docker 的绑定挂载。

格式:

宿主机目录:容器内部目录

PostgreSQL 在容器内写入 /var/lib/postgresql/data,实际数据被保存到宿主机的 /opt/myblog/data/postgres。

这样即使重新创建数据库容器,只要使用原来的数据目录且版本兼容,数据库就可以继续使用。

:Z 是与 SELinux 文件标签相关的挂载选项,在 Rocky Linux 这类常启用 SELinux 的系统上很有用。

需要注意:数据持久化不等于备份。 如果宿主机磁盘丢失或目录被误删,挂载本身无法恢复数据。

4.2 服务依赖:depends_on

Halo 依赖 PostgreSQL:

depends_on:
  db:
    condition: service_healthy

这表示 PostgreSQL 通过健康检查后,Compose 才启动 Halo。

同样,Caddy 等待 Halo 健康检查通过后再启动。

三个服务形成以下依赖关系:

PostgreSQL Healthy
        |
        v
    Halo Healthy
        |
        v
    Caddy Started

4.3 健康检查:healthcheck

PostgreSQL 使用:

pg_isready -U halo -d halo

检查数据库是否已经准备好接受连接。

Halo 使用:

http://localhost:8090/actuator/health/readiness

检测应用就绪状态。

因此可以通过:

docker compose ps

查看容器是否处于 healthy 状态。

需要区分 Up 和 Healthy:前者代表容器进程在运行,后者代表设定的健康检查已经通过。

4.4 容器间如何通信?

Halo 的数据库连接地址是:

r2dbc:pool:postgresql://db:5432/halo

其中:

  • postgresql:数据库类型。

  • db:Compose 服务名称。

  • 5432:数据库端口。

  • halo:数据库名称。

这里没有使用容器 IP,而是直接使用服务名 db。

原因是 Docker Compose 会创建内部网络,并为服务提供名称解析。

所以:

Halo → db:5432
Caddy → halo:8090

不需要手动配置容器 IP 地址。


五、配置 Caddy 反向代理和 HTTPS

创建配置文件:

vim /opt/myblog/Caddyfile

最初的配置十分简单:

{$BLOG_DOMAIN} {
    encode zstd gzip
    reverse_proxy halo:8090
}

其中:

  • {$BLOG_DOMAIN}:使用环境变量指定域名。

  • encode zstd gzip:启用 HTTP 响应压缩。

  • reverse_proxy halo:8090:将请求转发给 Halo 容器。

后来为了同时支持带 www 和不带 www 的域名,调整为:

zhangshangwu.cyou {
    redir https://www.zhangshangwu.cyou{uri} permanent
}

www.zhangshangwu.cyou {
    encode zstd gzip
    reverse_proxy halo:8090
}

现在的访问逻辑是:

http://www.zhangshangwu.cyou
             |
             v
https://www.zhangshangwu.cyou
             |
             v
          Caddy
             |
             v
        halo:8090

而不带 www 的域名也会跳转到 www。

为什么选择 Caddy?

主要是因为 HTTPS 配置简单。

传统方案经常需要自行配置 Nginx、Certbot 和证书续期。

Caddy 可以根据站点域名,通过 ACME 协议向 Let's Encrypt 等 CA 申请证书,并自动管理证书续期。

前提是域名解析和外部网络验证条件满足。

本次部署最终在日志中看到:

certificate obtained successfully
identifier: www.zhangshangwu.cyou
issuer: acme-v02.api.letsencrypt.org-directory

说明 www 域名的 HTTPS 证书已经成功签发。


六、启动整个博客

首先检查配置:

cd /opt/myblog

docker compose config --quiet

如果没有报错,执行:

docker compose up -d

这个命令会:

  1. 读取 Compose 配置。

  2. 检查并拉取所需镜像。

  3. 创建 Docker 网络。

  4. 创建和启动数据库容器。

  5. 等待数据库健康。

  6. 启动 Halo 容器。

  7. 等待 Halo 健康。

  8. 启动 Caddy 容器。

不过,实际部署时在镜像拉取阶段就遇到了第一个问题。


七、故障排查记录

7.1 故障一:Docker Hub 镜像拉取超时

第一次执行:

docker compose up -d

出现:

Image postgres:16 Interrupted
Image caddy:2 Error

核心报错:

failed to resolve reference
https://registry-1.docker.io/...

dial tcp ...:443: i/o timeout

故障分析:

postgres:16 和 caddy:2 默认从 Docker Hub 获取镜像。

服务器访问 Docker Hub 时发生了网络超时,导致镜像下载失败。

但是 Halo 镜像:

registry.fit2cloud.com/halo/halo:2.27

可以正常拉取。

这说明 Docker 本身可以正常运行,问题集中在特定镜像仓库的网络访问上。

处理过程:

检查 Docker 镜像加速配置:

docker info | grep -A 5 "Registry Mirrors"

再分别尝试拉取镜像:

docker pull postgres:16
docker pull caddy:2

可以在适用情况下配置可用的镜像加速源,或使用可信的镜像仓库代理。

重新尝试后,镜像最终成功下载。

再次执行:

docker compose up -d
docker compose ps

得到了:

myblog-db-1      Up (healthy)
myblog-halo-1    Up (healthy)
myblog-caddy-1   Up

说明三个服务均已启动,数据库和 Halo 的健康检查通过。

经验总结:镜像拉取失败和容器启动失败是两个不同阶段的问题。 前者通常先从仓库地址、DNS、网络连通性等方向排查。

7.2 故障二:HTTPS 正常,但首页返回 404

部署成功后,通过 curl 测试:

curl -IL www.zhangshangwu.cyou

得到:

HTTP/1.1 308 Permanent Redirect
Location: https://www.zhangshangwu.cyou/

HTTP/2 404

第一段响应:

308 Permanent Redirect

说明 Caddy 成功将 HTTP 重定向到了 HTTPS。

第二段响应:

404

说明 HTTPS 请求已经进入网站,但请求的首页资源没有正确返回。

后来又检查了裸域名:

curl https://zhangshangwu.cyou/console

出现 TLS 握手错误。

检查 Caddy 日志发现,最初成功签发证书的是 www 域名,而裸域名还没有得到已签发证书的确认。

因此调整 Caddy 配置,使两个域名都被正确识别,并将裸域名重定向到 www。

这里学到的是:DNS、TLS、HTTP 反向代理和应用业务属于不同层次。

一个层次正常,不能说明其他层次也正常。

7.3 故障三:Halo 首页缺少主题模板

接着使用 GET 请求检查首页:

curl -i https://www.zhangshangwu.cyou/

得到:

{
  "detail": "Template index was not found.",
  "status": 404,
  "title": "Resource Not Found"
}

这次报错已经很明确:

Template index was not found.

即 Halo 没有找到首页对应的主题模板。

同时测试管理后台:

curl -i https://www.zhangshangwu.cyou/console

返回:

HTTP/2 302
location: /login?authentication_required

这是正常的未登录重定向,说明 Halo 后台能够响应请求。

因此可以基本排除 Caddy 无法连接 Halo 的问题,排查方向转向 Halo 的主题管理。

处理方法是在 Halo Console 中检查主题,安装并启用兼容的主题,确认主题提供首页模板。

最后,网站已经可以正常访问和使用。

经验总结:HTTP 404 不一定是 Nginx 或 Caddy 配置问题,也可能是后端应用或模板层的问题。


八、整个请求到底是如何完成的?

博客搭建完成之后,再从网络原理角度梳理一遍访问过程。

假设用户访问:

https://www.zhangshangwu.cyou/

完整流程为:

第一步:DNS 解析

浏览器通过 DNS 查询得到服务器公网 IP。

第二步:建立连接

浏览器连接服务器的 443 端口。

在 HTTP/2 场景下,通常通过 TCP 建立连接,再进行 TLS 握手。

第三步:TLS 握手

Caddy 向浏览器提供对应域名的证书,双方协商加密连接。

第四步:Docker 端口映射

公网请求进入宿主机 443 端口,再由 Docker 端口发布机制转发到 Caddy 容器。

第五步:Caddy 反向代理

Caddy 根据站点配置,将请求转发到:

http://halo:8090

第六步:Halo 处理请求

Halo 根据路由和主题配置处理请求。

如果需要读取文章、分类等数据,则访问:

db:5432

从 PostgreSQL 获取数据。

第七步:返回响应

Halo 生成 HTTP 响应,经过 Caddy 返回给客户端。

第八步:浏览器渲染页面

浏览器解析 HTML、CSS 和 JavaScript,并加载图片等资源,最终展示博客页面。

这个流程可以概括为:

DNS
 ↓
TCP / TLS
 ↓
Caddy
 ↓
Docker 内部网络
 ↓
Halo
 ↓
PostgreSQL
 ↓
Halo 渲染响应
 ↓
Caddy
 ↓
浏览器

需要注意,HTTPS 在 Caddy 处终止,Caddy 与 Halo 之间使用 Docker 内部 HTTP 通信。


九、博客的日常维护

网站成功部署不意味着以后完全不需要维护。

9.1 常用命令

进入项目目录:

cd /opt/myblog

查看容器:

docker compose ps

查看 Halo 日志:

docker compose logs --tail=100 halo

实时查看日志:

docker compose logs -f halo

重启 Halo:

docker compose restart halo

查看资源消耗:

docker stats

查看端口监听:

ss -lntp

检查 Caddy 日志:

docker compose logs --tail=100 caddy

以后故障排查通常先从这些命令入手。

9.2 数据备份

虽然数据库已经使用 Bind Mount 持久化,但还是必须定期备份。

例如,使用 pg_dump 备份 PostgreSQL:

cd /opt/myblog

mkdir -p backups
chmod 700 backups

docker compose exec -T db \
  pg_dump -U halo -d halo -Fc \
  > "backups/halo-$(date +%F).dump"

这里:

  • docker compose exec:在容器内执行命令。

  • pg_dump:导出 PostgreSQL 数据库。

  • -Fc:使用自定义归档格式。

  • >:将输出重定向到备份文件。

但数据库备份并不包含 Halo 所有附件和主题文件,因此还需要备份 Halo 工作目录,或者使用 Halo 后台提供的完整备份功能。

建议将备份保存到本地电脑或对象存储中,避免服务器故障导致数据与备份一起丢失。

9.3 版本更新

以后升级 Halo 时,基本流程如下:

  1. 创建完整备份。

  2. 查看新版本更新说明。

  3. 确认主题和插件的兼容性。

  4. 修改 compose.yaml 中 Halo 镜像版本。

  5. 拉取镜像并重新创建服务。

  6. 验证后台、首页和文章访问。

常用命令:

docker compose pull halo

docker compose up -d

docker compose ps

需要注意,PostgreSQL 的大版本升级涉及数据库格式兼容性,不能简单地随意修改镜像主版本。


十、总结

到这里,我的个人博客就正式搭建完成了。

回顾这次实践,整个过程涉及:

  • Linux 服务器配置与管理。

  • DNS 域名解析。

  • Docker 镜像与容器。

  • Docker Compose 多容器编排。

  • Docker 网络与服务发现。

  • Bind Mount 数据持久化。

  • PostgreSQL 数据库。

  • Caddy 反向代理。

  • HTTPS、TLS 和证书管理。

  • HTTP 状态码与故障排查。

虽然最终只运行了三个容器,但它已经包含了一套典型 Web 应用的主要组成部分:

流量入口、业务应用、数据库、持久化存储和自动化服务编排。

这次最大的收获并不是运行了一个博客,而是在真实部署中经历了完整的排障过程。

从 Docker Hub 连接超时,到 HTTPS 证书问题,再到应用层的主题模板缺失,每个错误都对应不同的系统层次。

我也更加理解了一个运维中的基本思路:

遇到故障,不要急着重装或者修改所有配置,而是先确定问题发生在哪一层,再根据日志、状态和实际请求结果逐步缩小范围。

未来计划继续完善这套博客,例如增加自动备份、服务监控、告警和 CI/CD,将它逐步扩展为一个可以长期维护的个人服务。

这是第一篇文章,也是这个博客的起点。

以后就在这里记录学习、实践、问题,以及解决问题的过程。


相关项目: