技术栈: 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 |
其中:
通过以下命令验证:
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
这个命令会:
读取 Compose 配置。
检查并拉取所需镜像。
创建 Docker 网络。
创建和启动数据库容器。
等待数据库健康。
启动 Halo 容器。
等待 Halo 健康。
启动 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"
这里:
但数据库备份并不包含 Halo 所有附件和主题文件,因此还需要备份 Halo 工作目录,或者使用 Halo 后台提供的完整备份功能。
建议将备份保存到本地电脑或对象存储中,避免服务器故障导致数据与备份一起丢失。
9.3 版本更新
以后升级 Halo 时,基本流程如下:
创建完整备份。
查看新版本更新说明。
确认主题和插件的兼容性。
修改 compose.yaml 中 Halo 镜像版本。
拉取镜像并重新创建服务。
验证后台、首页和文章访问。
常用命令:
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,将它逐步扩展为一个可以长期维护的个人服务。
这是第一篇文章,也是这个博客的起点。
以后就在这里记录学习、实践、问题,以及解决问题的过程。
相关项目:
<p style=""></p><blockquote><p style=""><strong>技术栈:</strong> Linux、Docker、Docker Compose、Halo、PostgreSQL、Caddy、HTTPS<br><strong>部署环境:</strong> 腾讯云服务器、Rocky Linux、个人域名<br><strong>搭建时间:</strong> 2026 年 10 月 31 日<br><strong>博客地址:</strong> <a href="https://www.zhangshangwu.cyou/">https://www.zhangshangwu.cyou</a></p></blockquote><h2 id="%E5%89%8D%E8%A8%80" style="">前言</h2><p style="">这是我的第一篇博客,记录一下这个博客本身是如何搭建起来的。</p><p style="">我有一台腾讯云服务器和一个已经备案的个人域名,希望利用现有资源搭建一个属于自己的技术博客,主要用于记录 Linux、Docker、Kubernetes、SRE 等方面的学习笔记和实践项目。</p><p style="">最初考虑过 Typecho、Hugo、WordPress、Halo 等开源项目。最后选择了 <strong>Halo + PostgreSQL + Caddy</strong>,通过 Docker Compose 统一部署。</p><p style="">选择这套架构主要有几个原因:</p><ul><li><p style=""><strong>Halo:</strong> 开源博客系统,拥有可视化管理后台、主题和插件生态,适合长期维护。</p></li><li><p style=""><strong>PostgreSQL:</strong> 负责持久化存储博客数据。</p></li><li><p style=""><strong>Caddy:</strong> 配置简单,支持自动申请和续期 HTTPS 证书。</p></li><li><p style=""><strong>Docker Compose:</strong> 将多个服务统一编排,便于部署、升级和迁移。</p></li></ul><p style="">整个部署过程并非完全顺利,先后遇到了 Docker Hub 镜像拉取超时、域名 HTTPS 异常、Halo 首页返回 404 等问题。</p><p style="">本文记录完整的搭建过程,以及这些问题的排查和解决思路。</p><hr><h2 id="%E4%B8%80%E3%80%81%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1" style="">一、整体架构设计</h2><p style="">首先看一下最终架构:</p><pre><code> 互联网用户
|
| HTTPS
v
www.zhangshangwu.cyou
|
| DNS 解析
v
腾讯云公网服务器
|
TCP 80 / 443
|
+-----------------------------+
| Docker Compose |
| |
| +-------------------+ |
| | Caddy | |
| | HTTPS / 反向代理 | |
| +---------+---------+ |
| | |
| HTTP :8090 |
| | |
| +---------v---------+ |
| | Halo | |
| | 博客应用服务 | |
| +---------+---------+ |
| | |
| PostgreSQL :5432 |
| | |
| +---------v---------+ |
| | PostgreSQL | |
| | 数据库 | |
| +-------------------+ |
| |
+-----------------------------+
|
宿主机磁盘
/opt/myblog</code></pre><p style="">整个系统由三个容器组成:</p><div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow: auto hidden; width: 100%; max-width: 100%; min-width: 0px;"><table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;"><tbody><tr><th colspan="1" rowspan="1"><p style="">服务</p></th><th colspan="1" rowspan="1"><p style="">镜像</p></th><th colspan="1" rowspan="1"><p style="">作用</p></th></tr><tr><td colspan="1" rowspan="1"><p style="">Halo</p></td><td colspan="1" rowspan="1"><p style=""><code>registry.fit2cloud.com/halo/halo:2.27</code></p></td><td colspan="1" rowspan="1"><p style="">博客应用、管理后台、主题渲染</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">PostgreSQL</p></td><td colspan="1" rowspan="1"><p style=""><code>postgres:16</code></p></td><td colspan="1" rowspan="1"><p style="">存储文章、用户和配置数据</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">Caddy</p></td><td colspan="1" rowspan="1"><p style=""><code>caddy:2</code></p></td><td colspan="1" rowspan="1"><p style="">HTTPS、证书管理、反向代理</p></td></tr></tbody></table></div><p style=""><strong>架构设计的核心原则:将入口服务、业务服务和数据库分离。</strong></p><p style="">公网只开放 Caddy 的 80、443 端口,Halo 和 PostgreSQL 通过 Docker 内部网络通信,不需要直接暴露到公网。</p><hr><h2 id="%E4%BA%8C%E3%80%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87" style="">二、服务器环境准备</h2><h3 id="2.1-%E5%9F%BA%E7%A1%80%E7%8E%AF%E5%A2%83" style="">2.1 基础环境</h3><p style="">本次使用的环境:</p><div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow: auto hidden; width: 100%; max-width: 100%; min-width: 0px;"><table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;"><tbody><tr><th colspan="1" rowspan="1"><p style="">项目</p></th><th colspan="1" rowspan="1"><p style="">配置</p></th></tr><tr><td colspan="1" rowspan="1"><p style="">云服务商</p></td><td colspan="1" rowspan="1"><p style="">腾讯云</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">操作系统</p></td><td colspan="1" rowspan="1"><p style="">Rocky Linux</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">容器运行时</p></td><td colspan="1" rowspan="1"><p style="">Docker Engine</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">编排工具</p></td><td colspan="1" rowspan="1"><p style="">Docker Compose</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">域名</p></td><td colspan="1" rowspan="1"><p style="">zhangshangwu.cyou</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">博客主域名</p></td><td colspan="1" rowspan="1"><p style=""><a href="http://www.zhangshangwu.cyou/">www.zhangshangwu.cyou</a></p></td></tr></tbody></table></div><p style="">首先检查服务器环境:</p><pre><code># 查看操作系统
cat /etc/os-release
# 查看内核版本
uname -r
# 查看 Docker 版本
docker --version
# 查看 Docker Compose 版本
docker compose version
# 查看端口监听情况
ss -lntp</code></pre><p style="">本次部署时 Docker 已经安装完成,因此不再展开 Docker Engine 的安装步骤。</p><h3 id="2.2-%E9%85%8D%E7%BD%AE%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90" style="">2.2 配置域名解析</h3><p style="">在域名 DNS 控制台添加 A 记录:</p><div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow: auto hidden; width: 100%; max-width: 100%; min-width: 0px;"><table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;"><tbody><tr><th colspan="1" rowspan="1"><p style="">主机记录</p></th><th colspan="1" rowspan="1"><p style="">类型</p></th><th colspan="1" rowspan="1"><p style="">记录值</p></th></tr><tr><td colspan="1" rowspan="1"><p style="">www</p></td><td colspan="1" rowspan="1"><p style="">A</p></td><td colspan="1" rowspan="1"><p style="">服务器公网 IP</p></td></tr><tr><td colspan="1" rowspan="1"><p style="">@</p></td><td colspan="1" rowspan="1"><p style="">A</p></td><td colspan="1" rowspan="1"><p style="">服务器公网 IP</p></td></tr></tbody></table></div><p style="">其中:</p><ul><li><p style=""><code>www</code> 对应 <code>www.zhangshangwu.cyou</code>。</p></li><li><p style=""><code>@</code> 对应根域名 <code>zhangshangwu.cyou</code>。</p></li></ul><p style="">通过以下命令验证:</p><pre><code>dig +short www.zhangshangwu.cyou
dig +short zhangshangwu.cyou</code></pre><p style="">如果返回服务器的公网 IP,说明 DNS 解析基本正确。</p><p style="">需要注意,DNS 解析成功只代表域名能够找到服务器,并不代表 HTTP、HTTPS 服务一定正常。</p><h3 id="2.3-%E9%85%8D%E7%BD%AE%E9%98%B2%E7%81%AB%E5%A2%99" style="">2.3 配置防火墙</h3><p style="">在腾讯云安全组中放行:</p><ul><li><p style="">TCP 80:HTTP。</p></li><li><p style="">TCP 443:HTTPS。</p></li><li><p style="">TCP 22:SSH,建议限制来源 IP。</p></li></ul><p style="">如果 Rocky Linux 启用了 firewalld,还需要在系统层面放行 Web 服务:</p><pre><code>firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload</code></pre><p style="">到这里,服务器的基本网络环境就准备好了。</p><hr><h2 id="%E4%B8%89%E3%80%81%E5%88%9B%E5%BB%BA-docker-compose-%E9%A1%B9%E7%9B%AE" style="">三、创建 Docker Compose 项目</h2><h3 id="3.1-%E5%88%9B%E5%BB%BA%E5%B7%A5%E4%BD%9C%E7%9B%AE%E5%BD%95" style="">3.1 创建工作目录</h3><p style="">统一使用 <code>/opt/myblog</code> 管理博客项目:</p><pre><code>mkdir -p /opt/myblog
cd /opt/myblog
mkdir -p data</code></pre><p style="">项目目录设计如下:</p><pre><code>/opt/myblog/
├── compose.yaml
├── Caddyfile
├── .env
└── data/
├── postgres/
├── halo/
├── caddy/
└── caddy-config/</code></pre><p style="">各部分的作用:</p><ul><li><p style=""><code>compose.yaml</code>:定义和编排容器。</p></li><li><p style=""><code>Caddyfile</code>:配置反向代理和 HTTPS。</p></li><li><p style=""><code>.env</code>:存储环境变量。</p></li><li><p style=""><code>data/postgres</code>:数据库持久化目录。</p></li><li><p style=""><code>data/halo</code>:Halo 工作目录。</p></li><li><p style=""><code>data/caddy</code>:Caddy 证书等数据。</p></li></ul><p style="">将配置文件和持久化数据集中管理,可以降低后续维护和迁移的复杂度。</p><h3 id="3.2-%E5%88%9B%E5%BB%BA%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E6%96%87%E4%BB%B6" style="">3.2 创建环境变量文件</h3><p style="">先生成一个随机数据库密码:</p><pre><code>openssl rand -hex 24</code></pre><p style="">随后创建 <code>.env</code>:</p><pre><code>vim /opt/myblog/.env</code></pre><p style="">内容如下:</p><pre><code>BLOG_DOMAIN=www.zhangshangwu.cyou
POSTGRES_PASSWORD=替换为随机生成的数据库密码</code></pre><p style="">实际密码需要替换成生成的随机值,不能直接使用上面的占位文字。</p><p style=""><strong>为什么使用 </strong><code>.env</code><strong>?</strong></p><p style="">因为 Docker Compose 支持通过 <code>${变量名}</code> 引用环境变量。</p><p style="">例如:</p><pre><code>environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}</code></pre><p style="">这样不需要在 Compose 文件的多个位置重复编写密码。</p><p style="">不过,<code>.env</code> 只是配置文件,并不是加密存储。需要限制文件权限,并且避免上传到公开仓库。</p><pre><code>chmod 600 /opt/myblog/.env</code></pre><hr><h2 id="%E5%9B%9B%E3%80%81%E7%BC%96%E5%86%99-docker-compose-%E9%85%8D%E7%BD%AE" style="">四、编写 Docker Compose 配置</h2><p style="">创建配置文件:</p><pre><code>vim /opt/myblog/compose.yaml</code></pre><p style="">写入以下内容:</p><pre><code>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</code></pre><h3 id="4.1-%E6%95%B0%E6%8D%AE%E6%8C%81%E4%B9%85%E5%8C%96%EF%BC%9Avolumes" style="">4.1 数据持久化:volumes</h3><p style="">以 PostgreSQL 为例:</p><pre><code>volumes:
- ./data/postgres:/var/lib/postgresql/data:Z</code></pre><p style="">这是 Docker 的绑定挂载。</p><p style="">格式:</p><pre><code>宿主机目录:容器内部目录</code></pre><p style="">PostgreSQL 在容器内写入 <code>/var/lib/postgresql/data</code>,实际数据被保存到宿主机的 <code>/opt/myblog/data/postgres</code>。</p><p style="">这样即使重新创建数据库容器,只要使用原来的数据目录且版本兼容,数据库就可以继续使用。</p><p style=""><code>:Z</code> 是与 SELinux 文件标签相关的挂载选项,在 Rocky Linux 这类常启用 SELinux 的系统上很有用。</p><p style="">需要注意:<strong>数据持久化不等于备份。</strong> 如果宿主机磁盘丢失或目录被误删,挂载本身无法恢复数据。</p><h3 id="4.2-%E6%9C%8D%E5%8A%A1%E4%BE%9D%E8%B5%96%EF%BC%9Adepends_on" style="">4.2 服务依赖:depends_on</h3><p style="">Halo 依赖 PostgreSQL:</p><pre><code>depends_on:
db:
condition: service_healthy</code></pre><p style="">这表示 PostgreSQL 通过健康检查后,Compose 才启动 Halo。</p><p style="">同样,Caddy 等待 Halo 健康检查通过后再启动。</p><p style="">三个服务形成以下依赖关系:</p><pre><code>PostgreSQL Healthy
|
v
Halo Healthy
|
v
Caddy Started</code></pre><h3 id="4.3-%E5%81%A5%E5%BA%B7%E6%A3%80%E6%9F%A5%EF%BC%9Ahealthcheck" style="">4.3 健康检查:healthcheck</h3><p style="">PostgreSQL 使用:</p><pre><code>pg_isready -U halo -d halo</code></pre><p style="">检查数据库是否已经准备好接受连接。</p><p style="">Halo 使用:</p><pre><code>http://localhost:8090/actuator/health/readiness</code></pre><p style="">检测应用就绪状态。</p><p style="">因此可以通过:</p><pre><code>docker compose ps</code></pre><p style="">查看容器是否处于 <code>healthy</code> 状态。</p><p style="">需要区分 <code>Up</code> 和 <code>Healthy</code>:前者代表容器进程在运行,后者代表设定的健康检查已经通过。</p><h3 id="4.4-%E5%AE%B9%E5%99%A8%E9%97%B4%E5%A6%82%E4%BD%95%E9%80%9A%E4%BF%A1%EF%BC%9F" style="">4.4 容器间如何通信?</h3><p style="">Halo 的数据库连接地址是:</p><pre><code>r2dbc:pool:postgresql://db:5432/halo</code></pre><p style="">其中:</p><ul><li><p style=""><code>postgresql</code>:数据库类型。</p></li><li><p style=""><code>db</code>:Compose 服务名称。</p></li><li><p style=""><code>5432</code>:数据库端口。</p></li><li><p style=""><code>halo</code>:数据库名称。</p></li></ul><p style="">这里没有使用容器 IP,而是直接使用服务名 <code>db</code>。</p><p style="">原因是 Docker Compose 会创建内部网络,并为服务提供名称解析。</p><p style="">所以:</p><pre><code>Halo → db:5432
Caddy → halo:8090</code></pre><p style="">不需要手动配置容器 IP 地址。</p><hr><h2 id="%E4%BA%94%E3%80%81%E9%85%8D%E7%BD%AE-caddy-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E5%92%8C-https" style="">五、配置 Caddy 反向代理和 HTTPS</h2><p style="">创建配置文件:</p><pre><code>vim /opt/myblog/Caddyfile</code></pre><p style="">最初的配置十分简单:</p><pre><code>{$BLOG_DOMAIN} {
encode zstd gzip
reverse_proxy halo:8090
}</code></pre><p style="">其中:</p><ul><li><p style=""><code>{$BLOG_DOMAIN}</code>:使用环境变量指定域名。</p></li><li><p style=""><code>encode zstd gzip</code>:启用 HTTP 响应压缩。</p></li><li><p style=""><code>reverse_proxy halo:8090</code>:将请求转发给 Halo 容器。</p></li></ul><p style="">后来为了同时支持带 www 和不带 www 的域名,调整为:</p><pre><code>zhangshangwu.cyou {
redir https://www.zhangshangwu.cyou{uri} permanent
}
www.zhangshangwu.cyou {
encode zstd gzip
reverse_proxy halo:8090
}</code></pre><p style="">现在的访问逻辑是:</p><pre><code>http://www.zhangshangwu.cyou
|
v
https://www.zhangshangwu.cyou
|
v
Caddy
|
v
halo:8090</code></pre><p style="">而不带 www 的域名也会跳转到 www。</p><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9-caddy%EF%BC%9F" style="">为什么选择 Caddy?</h3><p style="">主要是因为 HTTPS 配置简单。</p><p style="">传统方案经常需要自行配置 Nginx、Certbot 和证书续期。</p><p style="">Caddy 可以根据站点域名,通过 ACME 协议向 Let's Encrypt 等 CA 申请证书,并自动管理证书续期。</p><p style="">前提是域名解析和外部网络验证条件满足。</p><p style="">本次部署最终在日志中看到:</p><pre><code>certificate obtained successfully
identifier: www.zhangshangwu.cyou
issuer: acme-v02.api.letsencrypt.org-directory</code></pre><p style="">说明 www 域名的 HTTPS 证书已经成功签发。</p><hr><h2 id="%E5%85%AD%E3%80%81%E5%90%AF%E5%8A%A8%E6%95%B4%E4%B8%AA%E5%8D%9A%E5%AE%A2" style="">六、启动整个博客</h2><p style="">首先检查配置:</p><pre><code>cd /opt/myblog
docker compose config --quiet</code></pre><p style="">如果没有报错,执行:</p><pre><code>docker compose up -d</code></pre><p style="">这个命令会:</p><ol><li><p style="">读取 Compose 配置。</p></li><li><p style="">检查并拉取所需镜像。</p></li><li><p style="">创建 Docker 网络。</p></li><li><p style="">创建和启动数据库容器。</p></li><li><p style="">等待数据库健康。</p></li><li><p style="">启动 Halo 容器。</p></li><li><p style="">等待 Halo 健康。</p></li><li><p style="">启动 Caddy 容器。</p></li></ol><p style="">不过,实际部署时在镜像拉取阶段就遇到了第一个问题。</p><hr><h2 id="%E4%B8%83%E3%80%81%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5%E8%AE%B0%E5%BD%95" style="">七、故障排查记录</h2><h3 id="7.1-%E6%95%85%E9%9A%9C%E4%B8%80%EF%BC%9Adocker-hub-%E9%95%9C%E5%83%8F%E6%8B%89%E5%8F%96%E8%B6%85%E6%97%B6" style="">7.1 故障一:Docker Hub 镜像拉取超时</h3><p style="">第一次执行:</p><pre><code>docker compose up -d</code></pre><p style="">出现:</p><pre><code>Image postgres:16 Interrupted
Image caddy:2 Error</code></pre><p style="">核心报错:</p><pre><code>failed to resolve reference
https://registry-1.docker.io/...
dial tcp ...:443: i/o timeout</code></pre><p style=""><strong>故障分析:</strong></p><p style=""><code>postgres:16</code> 和 <code>caddy:2</code> 默认从 Docker Hub 获取镜像。</p><p style="">服务器访问 Docker Hub 时发生了网络超时,导致镜像下载失败。</p><p style="">但是 Halo 镜像:</p><pre><code>registry.fit2cloud.com/halo/halo:2.27</code></pre><p style="">可以正常拉取。</p><p style="">这说明 Docker 本身可以正常运行,问题集中在特定镜像仓库的网络访问上。</p><p style=""><strong>处理过程:</strong></p><p style="">检查 Docker 镜像加速配置:</p><pre><code>docker info | grep -A 5 "Registry Mirrors"</code></pre><p style="">再分别尝试拉取镜像:</p><pre><code>docker pull postgres:16
docker pull caddy:2</code></pre><p style="">可以在适用情况下配置可用的镜像加速源,或使用可信的镜像仓库代理。</p><p style="">重新尝试后,镜像最终成功下载。</p><p style="">再次执行:</p><pre><code>docker compose up -d
docker compose ps</code></pre><p style="">得到了:</p><pre><code>myblog-db-1 Up (healthy)
myblog-halo-1 Up (healthy)
myblog-caddy-1 Up</code></pre><p style="">说明三个服务均已启动,数据库和 Halo 的健康检查通过。</p><p style=""><strong>经验总结:镜像拉取失败和容器启动失败是两个不同阶段的问题。</strong> 前者通常先从仓库地址、DNS、网络连通性等方向排查。</p><h3 id="7.2-%E6%95%85%E9%9A%9C%E4%BA%8C%EF%BC%9Ahttps-%E6%AD%A3%E5%B8%B8%EF%BC%8C%E4%BD%86%E9%A6%96%E9%A1%B5%E8%BF%94%E5%9B%9E-404" style="">7.2 故障二:HTTPS 正常,但首页返回 404</h3><p style="">部署成功后,通过 curl 测试:</p><pre><code>curl -IL www.zhangshangwu.cyou</code></pre><p style="">得到:</p><pre><code>HTTP/1.1 308 Permanent Redirect
Location: https://www.zhangshangwu.cyou/
HTTP/2 404</code></pre><p style="">第一段响应:</p><pre><code>308 Permanent Redirect</code></pre><p style="">说明 Caddy 成功将 HTTP 重定向到了 HTTPS。</p><p style="">第二段响应:</p><pre><code>404</code></pre><p style="">说明 HTTPS 请求已经进入网站,但请求的首页资源没有正确返回。</p><p style="">后来又检查了裸域名:</p><pre><code>curl https://zhangshangwu.cyou/console</code></pre><p style="">出现 TLS 握手错误。</p><p style="">检查 Caddy 日志发现,最初成功签发证书的是 www 域名,而裸域名还没有得到已签发证书的确认。</p><p style="">因此调整 Caddy 配置,使两个域名都被正确识别,并将裸域名重定向到 www。</p><p style="">这里学到的是:<strong>DNS、TLS、HTTP 反向代理和应用业务属于不同层次。</strong></p><p style="">一个层次正常,不能说明其他层次也正常。</p><h3 id="7.3-%E6%95%85%E9%9A%9C%E4%B8%89%EF%BC%9Ahalo-%E9%A6%96%E9%A1%B5%E7%BC%BA%E5%B0%91%E4%B8%BB%E9%A2%98%E6%A8%A1%E6%9D%BF" style="">7.3 故障三:Halo 首页缺少主题模板</h3><p style="">接着使用 GET 请求检查首页:</p><pre><code>curl -i https://www.zhangshangwu.cyou/</code></pre><p style="">得到:</p><pre><code>{
"detail": "Template index was not found.",
"status": 404,
"title": "Resource Not Found"
}</code></pre><p style="">这次报错已经很明确:</p><pre><code>Template index was not found.</code></pre><p style="">即 Halo 没有找到首页对应的主题模板。</p><p style="">同时测试管理后台:</p><pre><code>curl -i https://www.zhangshangwu.cyou/console</code></pre><p style="">返回:</p><pre><code>HTTP/2 302
location: /login?authentication_required</code></pre><p style="">这是正常的未登录重定向,说明 Halo 后台能够响应请求。</p><p style="">因此可以基本排除 Caddy 无法连接 Halo 的问题,排查方向转向 Halo 的主题管理。</p><p style="">处理方法是在 Halo Console 中检查主题,安装并启用兼容的主题,确认主题提供首页模板。</p><p style="">最后,网站已经可以正常访问和使用。</p><p style=""><strong>经验总结:HTTP 404 不一定是 Nginx 或 Caddy 配置问题,也可能是后端应用或模板层的问题。</strong></p><hr><h2 id="%E5%85%AB%E3%80%81%E6%95%B4%E4%B8%AA%E8%AF%B7%E6%B1%82%E5%88%B0%E5%BA%95%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%8C%E6%88%90%E7%9A%84%EF%BC%9F" style="">八、整个请求到底是如何完成的?</h2><p style="">博客搭建完成之后,再从网络原理角度梳理一遍访问过程。</p><p style="">假设用户访问:</p><pre><code>https://www.zhangshangwu.cyou/</code></pre><p style="">完整流程为:</p><p style=""><strong>第一步:DNS 解析</strong></p><p style="">浏览器通过 DNS 查询得到服务器公网 IP。</p><p style=""><strong>第二步:建立连接</strong></p><p style="">浏览器连接服务器的 443 端口。</p><p style="">在 HTTP/2 场景下,通常通过 TCP 建立连接,再进行 TLS 握手。</p><p style=""><strong>第三步:TLS 握手</strong></p><p style="">Caddy 向浏览器提供对应域名的证书,双方协商加密连接。</p><p style=""><strong>第四步:Docker 端口映射</strong></p><p style="">公网请求进入宿主机 443 端口,再由 Docker 端口发布机制转发到 Caddy 容器。</p><p style=""><strong>第五步:Caddy 反向代理</strong></p><p style="">Caddy 根据站点配置,将请求转发到:</p><pre><code>http://halo:8090</code></pre><p style=""><strong>第六步:Halo 处理请求</strong></p><p style="">Halo 根据路由和主题配置处理请求。</p><p style="">如果需要读取文章、分类等数据,则访问:</p><pre><code>db:5432</code></pre><p style="">从 PostgreSQL 获取数据。</p><p style=""><strong>第七步:返回响应</strong></p><p style="">Halo 生成 HTTP 响应,经过 Caddy 返回给客户端。</p><p style=""><strong>第八步:浏览器渲染页面</strong></p><p style="">浏览器解析 HTML、CSS 和 JavaScript,并加载图片等资源,最终展示博客页面。</p><p style="">这个流程可以概括为:</p><pre><code>DNS
↓
TCP / TLS
↓
Caddy
↓
Docker 内部网络
↓
Halo
↓
PostgreSQL
↓
Halo 渲染响应
↓
Caddy
↓
浏览器</code></pre><p style="">需要注意,HTTPS 在 Caddy 处终止,Caddy 与 Halo 之间使用 Docker 内部 HTTP 通信。</p><hr><h2 id="%E4%B9%9D%E3%80%81%E5%8D%9A%E5%AE%A2%E7%9A%84%E6%97%A5%E5%B8%B8%E7%BB%B4%E6%8A%A4" style="">九、博客的日常维护</h2><p style="">网站成功部署不意味着以后完全不需要维护。</p><h3 id="9.1-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4" style="">9.1 常用命令</h3><p style="">进入项目目录:</p><pre><code>cd /opt/myblog</code></pre><p style="">查看容器:</p><pre><code>docker compose ps</code></pre><p style="">查看 Halo 日志:</p><pre><code>docker compose logs --tail=100 halo</code></pre><p style="">实时查看日志:</p><pre><code>docker compose logs -f halo</code></pre><p style="">重启 Halo:</p><pre><code>docker compose restart halo</code></pre><p style="">查看资源消耗:</p><pre><code>docker stats</code></pre><p style="">查看端口监听:</p><pre><code>ss -lntp</code></pre><p style="">检查 Caddy 日志:</p><pre><code>docker compose logs --tail=100 caddy</code></pre><p style="">以后故障排查通常先从这些命令入手。</p><h3 id="9.2-%E6%95%B0%E6%8D%AE%E5%A4%87%E4%BB%BD" style="">9.2 数据备份</h3><p style="">虽然数据库已经使用 Bind Mount 持久化,但还是必须定期备份。</p><p style="">例如,使用 <code>pg_dump</code> 备份 PostgreSQL:</p><pre><code>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"</code></pre><p style="">这里:</p><ul><li><p style=""><code>docker compose exec</code>:在容器内执行命令。</p></li><li><p style=""><code>pg_dump</code>:导出 PostgreSQL 数据库。</p></li><li><p style=""><code>-Fc</code>:使用自定义归档格式。</p></li><li><p style=""><code>></code>:将输出重定向到备份文件。</p></li></ul><p style="">但数据库备份并不包含 Halo 所有附件和主题文件,因此还需要备份 Halo 工作目录,或者使用 Halo 后台提供的完整备份功能。</p><p style="">建议将备份保存到本地电脑或对象存储中,避免服务器故障导致数据与备份一起丢失。</p><h3 id="9.3-%E7%89%88%E6%9C%AC%E6%9B%B4%E6%96%B0" style="">9.3 版本更新</h3><p style="">以后升级 Halo 时,基本流程如下:</p><ol><li><p style="">创建完整备份。</p></li><li><p style="">查看新版本更新说明。</p></li><li><p style="">确认主题和插件的兼容性。</p></li><li><p style="">修改 <code>compose.yaml</code> 中 Halo 镜像版本。</p></li><li><p style="">拉取镜像并重新创建服务。</p></li><li><p style="">验证后台、首页和文章访问。</p></li></ol><p style="">常用命令:</p><pre><code>docker compose pull halo
docker compose up -d
docker compose ps</code></pre><p style="">需要注意,PostgreSQL 的大版本升级涉及数据库格式兼容性,不能简单地随意修改镜像主版本。</p><hr><h2 id="%E5%8D%81%E3%80%81%E6%80%BB%E7%BB%93" style="">十、总结</h2><p style="">到这里,我的个人博客就正式搭建完成了。</p><p style="">回顾这次实践,整个过程涉及:</p><ul><li><p style="">Linux 服务器配置与管理。</p></li><li><p style="">DNS 域名解析。</p></li><li><p style="">Docker 镜像与容器。</p></li><li><p style="">Docker Compose 多容器编排。</p></li><li><p style="">Docker 网络与服务发现。</p></li><li><p style="">Bind Mount 数据持久化。</p></li><li><p style="">PostgreSQL 数据库。</p></li><li><p style="">Caddy 反向代理。</p></li><li><p style="">HTTPS、TLS 和证书管理。</p></li><li><p style="">HTTP 状态码与故障排查。</p></li></ul><p style="">虽然最终只运行了三个容器,但它已经包含了一套典型 Web 应用的主要组成部分:</p><p style=""><strong>流量入口、业务应用、数据库、持久化存储和自动化服务编排。</strong></p><p style="">这次最大的收获并不是运行了一个博客,而是在真实部署中经历了完整的排障过程。</p><p style="">从 Docker Hub 连接超时,到 HTTPS 证书问题,再到应用层的主题模板缺失,每个错误都对应不同的系统层次。</p><p style="">我也更加理解了一个运维中的基本思路:</p><blockquote><p style="">遇到故障,不要急着重装或者修改所有配置,而是先确定问题发生在哪一层,再根据日志、状态和实际请求结果逐步缩小范围。</p></blockquote><p style="">未来计划继续完善这套博客,例如增加自动备份、服务监控、告警和 CI/CD,将它逐步扩展为一个可以长期维护的个人服务。</p><p style="">这是第一篇文章,也是这个博客的起点。</p><p style=""><strong>以后就在这里记录学习、实践、问题,以及解决问题的过程。</strong></p><hr><p style=""><strong>相关项目:</strong></p><ul><li><p style="">Halo:<a href="https://github.com/halo-dev/halo">https://github.com/halo-dev/halo</a></p></li><li><p style="">Docker:<a href="https://docs.docker.com/">https://docs.docker.com/</a></p></li><li><p style="">Docker Compose:<a href="https://docs.docker.com/compose/">https://docs.docker.com/compose/</a></p></li><li><p style="">PostgreSQL:<a href="https://www.postgresql.org/">https://www.postgresql.org/</a></p></li><li><p style="">Caddy:<a href="https://caddyserver.com/docs/">https://caddyserver.com/docs/</a></p></li></ul><p style=""></p>