最近花了一晚上,用Docker把Halo私有化部署到自己的服务器上,终于把折腾写作平台这事儿一次性了结了。Halo本身就是个定位很清晰的开源博客系统,配上Docker这种跑法,数据全部掌控在自己手里,主题、插件、写作界面都足够克制,没有一堆我用不上的社交按钮和推广位。这篇文章就把我从零开始到跑起来、再到日常维护的整个实操过程整理出来,包括一些踩坑经验,给同样想搭建一个专属写作环境的朋友做个参考。
适合谁看?如果你受够了SaaS平台动不动改版、加广告、锁数据,或者只是想要一个干净、能专注写东西、又能完全掌控内容的博客,那这套方案基本是性价比最高的选择。下面内容不涉及任何高深理论,跟着一步步来就行。
1. 为什么最终选Halo:我对写作平台的真实需求
1.1 写作这件事,需求其实远比你想象的简单
我前前后后用过的博客方案不算少。早期的静态博客确实轻,但每次写篇文章都得走本地编辑器、push、CI构建这一套流程,新鲜劲儿过去之后就会觉得麻烦,尤其在手机上想改个错别字都费劲。后来用过的几个动态博客平台,倒是省事了,可平台方一改版,整个后台和前端就面目全非,几个月不登录甚至要找半天自己文章在哪儿。
更核心的问题在于数据归属。文章是写在别人服务器上的,导出倒是能导出,但每次都让我有种不安定感。时间越长,我越清楚自己需要的其实是一个极简的写作阵地:打开后台就是一个清爽的编辑器,写完保存、发布,内容完全在我自己的数据库里,前端展示由我决定,不需要平台给我推送“推荐阅读”“热门标签”之类的东西。
1.2 在几种主流方案里做的取舍
我认真对比过几类方案,各有各的优点,也各有让我放弃的理由。
纯静态博客生成器(比如Hexo、Hugo这类):胜在部署成本低、访问速度快,纯静态文件扔到任何服务器或CDN上都能跑。缺点也很明显,写作流程不够顺滑——本地编辑、命令生成、部署,中间环环依赖,换设备写作更是麻烦。对我这种偶尔想用iPad写两句的人来说,这个流程太重了。
传统动态博客程序(比如WordPress):功能是真的强,插件生态也丰富,但很多功能对我来说都是过剩的。后台各种仪表盘、评论管理、媒体库、用户系统,开局就一堆东西需要配置,总让我觉得“这玩意儿是个系统,不是个写作工具”。加上更新频率高、安全补丁要跟进,维护成本并不低。
Halo:属于Java生态里比较成熟的博客系统,界面干净,写作体验接近在线文档编辑器。最关键的一点是它的扩展性没有好到让人迷失,默认体验就很克制。后台没有满屏的统计和推送,文章、页面、附件、主题这几个核心模块一目了然。配合Docker部署之后,更新升级就是拉一个镜像、重建容器的工夫。
1.3 用Docker私有化部署的真实理由
很多人会有疑问:直接在生产环境装一个Java应用不就行了,为什么非要多套一层Docker?
原因很实际。第一,环境隔离。Halo依赖Java运行环境,系统版本、JDK版本稍微有点出入,本地跑起来就全是坑。Docker镜像把所有依赖都打包好了,我只需要一个能跑容器的Linux环境,不需要在宿主机上折腾JDK。第二,升级和回滚方便。以后版本迭代,我只需要改一下镜像版本号然后重新创建容器,旧版本镜像还在本地,出问题随时退回去。第三,备份的时候不用考虑复杂的依赖关系。我只需要把容器的数据卷目录拷走,或者做数据库的定时备份,恢复起来思路非常清晰。
还有一点可能很多人没意识到:Docker部署能让你的数据路径变得完全可控。我会把数据卷挂载到宿主机固定目录下,数据库文件、上传的图片、日志、配置全部落在规划好的目录里。以后不管换机器还是迁移,直接把整个目录搬走就能恢复,不需要再回忆“当初我到底装在哪里了”。
2. 部署前的环境准备与镜像选择
2.1 服务器基础环境和Docker安装
如果你手头已经有一台Linux服务器,哪怕配置很一般(1核2G内存就足够起步),跑Halo是绰绰有余的。Halo 2.x默认内置H2数据库时,内存占用大概在300到500MB之间;如果换成PostgreSQL,也基本不会超过1GB。对于个人博客来说,完全在预算范围内。
以Ubuntu系统为例,Docker的安装其实已经非常简单了。我习惯用官方脚本的方式,但生产环境还是推荐通过apt仓库来装,这样后续升级更方便。
# 安装依赖包 sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # 添加Docker官方GPG密钥 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加软件源 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装Docker sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成之后,记得执行一下验证命令:
sudo systemctl enable docker && sudo systemctl start docker sudo docker run hello-world看到hello-world镜像能正常拉取、容器能正常退出,Docker环境就算就绪了。如果你用的是CentOS、Debian或者OpenWrt这类系统,安装命令会稍微不同,但整体思路一样,建议直接参考对应系统的官方文档,不要照抄一个命令就完事。
2.2 目录规划:从一开始就别乱
Docker部署最忌讳的就是数据卷挂在哪儿全凭心情,时间一长自己都忘了容器里哪些目录是重要的。我推荐的目录结构是这样:
/opt/halo/ ├── docker-compose.yml ├── .env ├── halo/ # Halo工作目录,里面包含 themes、logs、uploads 等子目录 └── postgres/ # 如果使用PostgreSQL,数据库文件放在这里所有博客相关的文件都收敛在这一个父目录下,备份的时候直接打包这个目录就行。这里有一点要提醒新手:不要把数据卷直接挂到root用户的家目录或者其他和系统文件混在一起的位置,不然哪天系统清理垃圾文件,很可能误伤数据。
2.3 镜像版本怎么选:稳定优先,别追新
Halo的官方仓库在Docker Hub上叫halohub/halo,2.x系列是目前的主流版本。在选择具体tag时,我的原则是:除非需要新功能,否则不追最新版本,优先选稳定版。
# 查看远程可用的版本列表 docker search halohub/halo # 或者直接访问镜像仓库页面查看标签信息在docker-compose.yml里,镜像版本号我一般写成具体的小版本,比如halohub/halo:2.17.0,而不是写latest,原因很简单:latest会在你执行docker compose pull的时候自动拉到最新版本,这种隐式升级一旦碰到breaking change,你可能连早餐都没吃完就得开始排查故障。写成固定版本号,升级变成一次显式的、可控的操作,配合备份再做升级,才是稳妥的流程。
数据库方面,极简起步可以用Halo默认的内置H2数据库,它零配置、启动快,几百篇文章毫无压力。但如果愿意多花一点点功夫,我更推荐直接上PostgreSQL,毕竟生产环境的数据存储用文件型数据库始终让人觉得不够踏实,后面做定时备份、数据恢复也更容易理解和操作。
3. 用docker-compose把Halo跑起来
3.1 最小化compose文件是什么样的
先给一个最简单、可以立刻跑起来的配置。如果你只是想先体验一下Halo,或者对数据库没有特别要求,那就用这个:
# /opt/halo/docker-compose.yml version: "3.8" services: halo: image: halohub/halo:2.17.0 container_name: halo restart: always ports: - "8090:8090" volumes: - /opt/halo/halo:/root/.halo2 environment: TZ: Asia/Shanghai就这么点东西,执行启动就能得到一个可用的博客系统:
cd /opt/halo docker compose up -d docker compose logs -f halo # 跟踪容器日志首次启动时,Halo会在/root/.halo2(对应宿主机/opt/halo/halo目录)下初始化配置文件和H2数据库。等日志出现类似“Started HaloApplication”的字样,说明服务已经起来了。此时访问http://服务器IP:8090,就能进入初始化安装页面。
3.2 带上PostgreSQL的完整配置
如果想把数据存储放到PostgreSQL里,就在这个目录下再建一个postgres数据目录,并在compose文件里加上数据库服务:
# /opt/halo/docker-compose.yml version: "3.8" services: postgres: image: postgres:16.3 container_name: halo-postgres restart: always environment: POSTGRES_DB: halo POSTGRES_USER: halo POSTGRES_PASSWORD: changeme_strong_password volumes: - /opt/halo/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U halo -d halo"] interval: 10s timeout: 5s retries: 5 halo: image: halohub/halo:2.17.0 container_name: halo restart: always depends_on: postgres: condition: service_healthy ports: - "8090:8090" volumes: - /opt/halo/halo:/root/.halo2 environment: TZ: Asia/Shanghai SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/halo SPRING_DATASOURCE_USERNAME: halo SPRING_DATASOURCE_PASSWORD: changeme_strong_password有一个很重要的细节:Halo在初始化时,如果检测到配置了数据库连接信息,就不会再让你选择H2数据库,而是直接使用PostgreSQL。所以要让上边的环境变量在第一次初始化前就生效,这个顺序别搞反。我见过有人先启动Halo把H2库初始化完了,再回头想切到PostgreSQL,结果数据迁移折腾了好一阵。
启动方式一样:
cd /opt/halo docker compose up -d这次postgres容器会先启动,通过健康检查之后halo容器才会开始初始化,避免出现应用先起来了但数据库还没准备好的竞态问题。
3.3 初始化安装时容易忽略的细节
首次打开Halo的安装页面,会要求设置管理员用户名、密码和邮箱。这个环节没有太多玄机,但有两个要点值得注意:
管理员的初始密码一定要设置成强密码。因为博客系统本身暴露在公网,弱密码就是给扫描工具和暴力破解送人头的。安装完成之后,系统会直接进入后台,你可以随时在“用户”设置里修改密码或者绑定登录方式。
Halo 2.x的初始化过程中,选择数据库类型的界面只有在没有检测到外部数据源时才会出现。如果你通过环境变量配置了PostgreSQL,就不会看到数据库类型选择步骤了,直接进入管理员信息设置页面。
初始化完成后,进入后台第一件事,我建议先把默认的主题换成自己真正要长期用的主题,然后把站点名称、Logo、页脚信息改掉。空跑一下发布一篇测试文章,确认整个链路——前台访问、后台编辑、文章发布、图片上传——都畅通无阻,再开始安心地写正式内容。
4. 极简配置:把Halo调教成顺手的样子
4.1 主题选择:克制是第一位
Halo官方主题市场里有不少主题,风格分化很明显。有的主题看起来功能特别丰富,首页各种区块、轮播、归档、标签云,仿佛什么都有。实际上手你会发现,那些效果大多需要配一堆设置项,写文章时想的不是内容本身,而是“这个内容在前台看起来会不会太挤”。
我个人的主题选择标准非常功利:首页能清晰展示文章标题摘要就行,正文排版干净,移动端阅读舒服。目前用的是一款官方出的极简主题,首页就是文章列表加简单分页,没有多余的视觉元素,阅读时正文宽度也控制得很好。这种主题还有一个额外好处——几乎没有需要长期维护的设置项,换服务器、恢复备份之后,几分钟就能恢复出和原来一样的展示效果。
如果你有动手能力,Halo主题本质上就是一套模板,支持在后台直接编辑主题源码,加一行自定义CSS、改一个HTML片段都很方便。这也是私有化部署的爽点:前端最终长什么样,由你一个人说了算。
4.2 后台和编辑器设置:减少一切干扰
Halo 2.x的默认编辑器是所见即所得模式(富文本),整体布局和语雀、Notion这类在线文档比较接近,左边工具栏、中间编辑区、右边属性面板,用起来基本不需要重新学习。
我自己的使用习惯是:
- 关闭不必要的功能模块。后台的“评论”模块,如果我暂时不想开放,就直接不启用;插件也是装一个用一个,绝不装一堆用不上的插件在后台吃灰。
- 附件命名规则早定好。上传图片时我习惯按
年/月/文件名的格式组织,Halo的附件管理支持按目录分组,一开始分好类,后面写文章配图时找起来效率高很多。 - 文章分类和标签克制使用。不要为了逼自己“体系化”而建一二十个分类。分类一旦多了,每次写作都纠结该归到哪,反而成了负担。
4.3 访问链路:Nginx反代、HTTPS和域名绑定
默认情况下,Halo监听8090端口,但直接暴露这个端口写文章,体验不够好,也不太安全。更常见的做法是用Nginx做反向代理,把“当前域名+443端口”的请求转发到内部的8090端口,然后再申请一个免费的SSL证书,把HTTPS加上。这样用户访问的就是一个标准的HTTPS网站,而不是一把梭的IP加端口。
反代配置核心部分长这样:
server { listen 80; server_name blog.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { proxy_pass http://127.0.0.1:8090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }证书部分,如果不想折腾acme.sh,也可以用Caddy这类自动申请证书的反代工具,配置会更简洁,但Nginx的思路还是最通用的,网上资料也最多。
注意:配置HTTPS时,
X-Forwarded-Proto这个请求头一定要带上,Halo会根据它来生成正确的链接。不然你会看到网站前端能打开,后台却因为请求协议判断错误而出现登录或者资源加载异常。
4.4 日常写作流程:打开后台直接开写
配置完成后,我的写作流程变成了:浏览器输入域名,进入后台,新建文章,写字,传图,发布。不需要打开任何本地编辑器,不需要处理命令。Halo的自动保存机制是周期性的,中途意外关掉页面,草稿也不会丢。这一点在我实际使用中很加分,谁都有过写到一半浏览器崩溃、或者临时要去开会直接合上笔记本的经历。
手机端我也试过直接用浏览器登录后台写东西,虽然不比本地Markdown编辑器输入体验好,但应急改稿、回复评论是没有问题的。配合PWA特性,Halo本身也能把站点“安装”到手机桌面,进出体验更像一个独立App。
5. 数据备份、恢复与版本升级的实操套路
5.1 备份思路:分清哪些是数据、哪些是行为痕迹
私有化部署的所有好处都建立在“数据安全”这个前提上,所以备份是绝对绕不开的一环。需要备份的东西其实不多,主要就两块:
- 数据库:如果用了PostgreSQL,整个PostgreSQL数据目录就是你的核心资产。文章内容、用户信息、评论、设置项全在里面。
- 工作目录:也就是挂载到容器里的
/opt/halo/halo目录。里面包含上传的附件图片、主题文件、日志等。这些属于“内容资产”,丢了也能通过代码仓库和下载包补回一部分,但上传的图片丢了就很难恢复。
所以我的备份策略非常简单粗暴:既然这两个目录都在/opt/halo下面,那就直接对整个目录做定期压缩备份:
# 手动备份 tar -czf /backup/halo_backup_$(date +%Y%m%d%H%M).tar.gz /opt/halo # 配合cron定时任务 0 3 * * * /usr/bin/tar -czf /backup/halo_backup_$(date +\%Y\%m\%d).tar.gz /opt/halo >> /backup/backup.log 2>&1 && find /backup -name "halo_backup_*.tar.gz" -mtime +7 -delete这个思路的好处是,恢复时不需要先装好Halo再从数据库单独恢复,直接把整个打包目录解压回原位置,然后重新执行docker compose up -d,应用和数据库就一起回来了。对于个人博客来说,这种整目录备份虽然简单粗暴,但确实最不容易出错。
注意:定时任务中
%符号在crontab里有特殊含义,需要写成\%,或者把脚本放到一个单独的.sh文件里再调度,我第一次写的时候就被这个坑过,备份目录里出现了不少“莫名其妙命名”的文件。
5.2 恢复流程:不能只备份不演练
光备份不演练等于没备份。我建议每做完一次备份,就顺手在本地或者另一台测试机器上验证一次恢复流程。具体做法:
- 把备份包解压到新的服务器
/opt/halo目录下。 - 安装好Docker,进入该目录。
- 执行
docker compose up -d。 - 等容器起来之后,用初始化管理员账号登录后台,抽查两篇文章和几张图片是否正常显示。
- 确认无误,再把这个“恢复演练”的服务器收起来,避免和正式环境混淆。
这套流程曾经帮我避免过一次事故。有次升级Halo版本后,页面打开显示异常,我一开始以为是自己配置问题,后来回头翻日志才发现是某个插件不兼容新版本。因为做过完整的恢复演练,我可以很从容地把docker-compose.yml里的版本号改成升级前的镜像版本,然后重新构建容器,整套回滚操作不到五分钟就完成了。
5.3 升级Halo版本的规范化操作步骤
Halo社区更新节奏还是比较活跃的,基本每个小版本都有一些bugfix和体验优化。版本升级我建议采用下面这个流程,每一步都在日志里留下记录:
# 进入部署目录 cd /opt/halo # 停掉容器 docker compose stop halo # 备份当前数据目录(升级前必要的保险) tar -czf /backup/pre_upgrade_$(date +%Y%m%d%H%M).tar.gz /opt/halo # 修改docker-compose.yml里的镜像版本号 # 拉取新镜像并重新创建容器 docker compose pull halo docker compose up -d halo # 检查日志输出,确认启动成功 docker compose logs -f halo升级最忌讳的事情就是跳过备份直接拉最新镜像。有一些版本更新会涉及数据结构迁移,虽然理论上Halo自己会处理,但万一迁移过程出问题,你没有回滚的余地。备份只需要几十秒和一点点磁盘空间,这个成本无论如何都不能省。
升级完成之后,我通常会顺手做一个小检查:打开前台首页,确认文章列表和主题渲染正常;登录后台,看一下设置项有没有丢失;再打开一篇含图片的文章,确认附件路径没有发生变化。全部通过,这次升级才算真正完成。
6. 实际运行阶段值得关注的几个细节
6.1 内存占用表现与容器资源限制
Halo作为Java应用,启动阶段的CPU和内存占用会短暂冲高,运行平稳后会回落。个人博客场景下,1核2G内存的服务器足够从容。如果服务器上同时还跑了其他容器,建议给Halo配置一个合理的资源上限,防止某个容器异常占用拖垮整机。
在compose文件里可以直接加上部署层的资源限制:
deploy: resources: limits: cpus: "1.0" memory: 1G实测下来,带PostgreSQL的Halo稳态内存占用在500MB到800MB之间,这个限额既能覆盖日常峰值,又不会把资源余量吃光。如果你用的是1G内存的小机器,需要再斟酌一下限额数值,适当放宽到1.2G可能更稳妥,具体以你自己服务器的实际情况为准。
6.2 日志轮转:别让日志文件撑爆磁盘
Docker容器一旦长期运行,日志文件会一直增长。刚开始可能没感觉,但如果你后台偶尔出现报错,日志会迅速累积,时间长了完全有可能把磁盘占满。配置一个docker daemon.json的日志轮转是最省心的方案:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "5" } }这个配置是针对Docker守护进程全局生效的,改完之后需要重启Docker服务。我的建议是趁部署初期就把它配置好,避免以后某天发现磁盘莫名满了,还得回头清理容器日志。
6.3 日常健康检查与可用性维护
Halo是Java应用,到目前为止我跑了一段时间,稳定性没什么可挑的。但“稳定”不等于“不用管”,日常维护里我至少会做三件事:
- 每周看一眼容器状态和日志,确认没有异常的WARN或ERROR。
- 每个月手动触发一次备份,并简单检查备份包大小是否正常。
- 关注Halo官方社区或者GitHub仓库的Release动态,遇到重要版本更新就规划升级窗口,而不是骤然更新。
如果你把博客当成长期写作阵地,这些维护工作不需要多频繁,但一定要养成习惯。私有化部署最大的优势是自由,最大的代价是责任——服务器安全和数据安全都得自己扛,好在Halo本身把大部分运维复杂度都吃掉了,剩下的活儿并不多。
6.4 安全加固的几个小动作
既然博客暴露在公网,有几个基本的安全动作建议部署当口就做完:
- 修改SSH端口并禁用密码登录,改用密钥登录。这是服务器层面最基本的底线,不只是为了博客安全。
- 不要用裸IP加端口访问后台。能用域名尽量用域名,能套HTTPS一定套HTTPS,至少避免被扫描工具直接识别出应用类型。
- 后台管理员账号不要和日常写作账号混用。Halo支持多用户,虽然个人博客可能只有你自己,但万一要邀请朋友一起写,给普通账号即可,不要共享管理员密码。
- 操作系统层面的防火墙只放行必要端口。Nginx的80/443、SSH端口放行,8090端口如果不希望外部直接访问,就只在防火墙里放行内网或干脆不放行,利用Nginx做代理访问。
做完这些小动作,个人博客被“莫名其妙入侵”的概率会大幅下降。不是说这样就能绝对安全,而是把这些基础门槛立起来之后,绝大多数脚本扫描就会被挡在门外。
7. 写在最后:这套方案的真实上限在哪里
如果你只是需要一个能安静写字、数据归属明确、前端完全可控的个人博客,Halo配合Docker这套组合,技术门槛低、后期维护成本也不高,完全称得上“写作神器”。它不会逼你用Markdown、不会强制你折腾部署流水线,也不会在你后台塞满没用的推荐位和统计图表。
从长远看,这套方案的成长空间也足够。文章多了之后可以继续扩展插件、接入对象存储、把附件放到专用存储服务里,甚至以后想给博客加个独立的评论系统,也都不算难事。现阶段先把写作这件事本身处理好,比什么都重要。
如果你也准备动手部署,我的建议是:别纠结太多架构上的事情,先把最小的compose文件跑起来,登进去写一篇带有图片的测试文章,感受一下整个链路的顺畅度。跑通之后,再按本文提到的备份、反代、HTTPS这些步骤一步步完善。好的工具不是一上来就配置齐全的,而是用着用着觉得“这里可以更好”,然后花几分钟微调出来的样子。我现在的博客就是这样一个状态——简单、稳定、写了很久也没遇到过劝退的麻烦。