想做一个加载快、后台干净、写作体验顺手的独立博客,Ghost 往往比塞满插件的 WordPress 省心。它自带会员、订阅、主题和内容 API,前台也没有一堆用不上的管理脚本。
但 Ghost 不是拉起一个容器就能放心上线。生产环境需要 MySQL 8、正确的公开 URL、HTTPS 反向代理、持久化内容目录和可恢复的数据库备份。邮件没配好时,成员登录链接、密码重置和邀请也会失效。
这篇教程用 Docker 官方 ghost:6-alpine 镜像和 MySQL 8,通过 Docker Compose 管理服务,Caddy 负责自动申请 HTTPS。Ghost 的 2368 端口只监听 127.0.0.1,MySQL 完全不映射到宿主机公网。
Ghost 更像“内容出版工具”,不是万能建站框架。
| 需求 | 更合适的选择 | 原因 |
|---|---|---|
| 专注写文章、会员订阅、Newsletter | Ghost | 编辑器和会员体系集成得更直接 |
| 商城、论坛、复杂插件生态 | WordPress | 插件和主题选择更多 |
| 纯文档、极低成本、无需后台 | Hugo / Astro | 可以生成静态文件,攻击面小 |
| 需要把内容提供给 App 或前端 | Ghost | 内置 Content API,可作为 Headless CMS |
Ghost 的优点是写作和发布路径短,缺点是扩展方式没有 WordPress 那么随意。需要复杂表单、电商或几十种业务插件时,硬把 Ghost 改成万能 CMS,维护成本反而更高。
Ghost 官方生产栈要求 MySQL 8,最低内存基线是 1GB。Docker 同时运行 Ghost、MySQL 和宿主机上的 Caddy 时,我更建议从 2 核、2GB 内存、30GB SSD 起步;图片较多、会员量增长或需要频繁发信时,用 2 核、4GB 会轻松很多。
| 场景 | 建议配置 |
|---|---|
| 新博客、访问量不高 | 2 核 2GB、30GB SSD |
| 多作者、会员功能、较多图片 | 2-4 核 4GB、50GB SSD |
| 已有大量历史内容或高并发 | 4 核 8GB 起,并单独评估数据库和对象存储 |
系统以 Ubuntu 24.04 LTS 为例。提前把 blog.example.com 的 A / AAAA 记录指向 VPS,安全组和 UFW 只开放 SSH、80、443。
按 Docker 官方仓库安装 Docker Engine 与 Compose 插件。完成后确认使用的是 Compose v2:
docker --version
docker compose version
如果命令只能写成带连字符的 docker-compose,说明还在使用旧版 Compose v1,先升级再继续。
创建部署目录:
sudo mkdir -p /opt/ghost/backups
sudo chown -R "$USER":"$USER" /opt/ghost
cd /opt/ghost
Ghost 使用单独的数据库账号,不要让应用拿 MySQL root 密码。生成两组不同的随机值:
openssl rand -hex 32
openssl rand -hex 32
nano .env
chmod 600 .env
把两个随机值分别写入 .env:
GHOST_URL=https://blog.example.com
MYSQL_ROOT_PASSWORD=替换成第一组随机值
MYSQL_PASSWORD=替换成第二组随机值
GHOST_URL 必须是读者最终访问的完整 HTTPS 地址。它会影响后台跳转、Canonical URL、邮件链接和资源地址;不要先写 IP,上线后再忘记修改。
创建 /opt/ghost/compose.yaml:
services:
db:
image: mysql:8.0
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}"
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}"
volumes:
- ghost_mysql:/var/lib/mysql
healthcheck:
test:
- CMD-SHELL
- >-
mysqladmin ping -h 127.0.0.1 -uroot
-p$$MYSQL_ROOT_PASSWORD --silent
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ghost:
image: ghost:6-alpine
container_name: ghost
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:2368:2368"
environment:
NODE_ENV: production
url: "${GHOST_URL:?set GHOST_URL in .env}"
database__client: mysql
database__connection__host: db
database__connection__port: 3306
database__connection__user: ghost
database__connection__password: "${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}"
database__connection__database: ghost
volumes:
- ghost_content:/var/lib/ghost/content
healthcheck:
test:
- CMD
- node
- -e
- >-
require('http').get('http://127.0.0.1:2368/ghost/',
r=>process.exit(r.statusCode<500?0:1))
.on('error',()=>process.exit(1))
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
ghost_mysql:
name: ghost_mysql
ghost_content:
name: ghost_content
这里固定 ghost:6-alpine,更新时会跟进 Ghost 6 的补丁和小版本,但不会在重建容器时突然跨到下一个大版本。MySQL 同样固定在 8.0 大版本,因为 Ghost 6 的生产环境只支持 MySQL 8,不要擅自替换成 MariaDB 或 PostgreSQL。
启动前先让 Compose 检查变量和 YAML:
docker compose config >/dev/null
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 ghost
第一次初始化 MySQL 和迁移 Ghost 数据库会花一点时间。running 不等于已经可用,以 docker compose ps 中两个服务都 healthy 为准。
安装 Caddy 后编辑 /etc/caddy/Caddyfile:
blog.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:2368
request_body {
max_size 50MB
}
}
Caddy 的 reverse_proxy 会设置 Ghost 需要的 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto。如果换成手写 Nginx 配置,漏掉 X-Forwarded-Proto https 很容易造成无限重定向。
检查并加载配置:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
还不熟悉证书和多域名配置,可以对照VPS 用 Caddy 反向代理完全指南。
确认 Ghost 后端只监听本机:
sudo ss -lntp | grep ':2368'
curl -I http://127.0.0.1:2368
curl -I https://blog.example.com
第一条应该显示 127.0.0.1:2368,不能是 0.0.0.0:2368。MySQL 没有 ports 配置,公网扫描也不应该看到 3306。
浏览器打开:
https://blog.example.com/ghost/
按页面提示创建站点所有者。完成后检查三项:
- Settings 中的站点 URL 与
GHOST_URL一致; - 上传一张测试图片,刷新后仍能显示;
- 发布一篇草稿,前台链接和 Canonical URL 都使用 HTTPS 域名。
Ghost 的核心内容在 MySQL,图片、主题、routes.yaml 等文件在 /var/lib/ghost/content。两部分任何一边没持久化,备份都不完整。
Ghost 生产环境需要可用的事务邮件,用于员工邀请、密码重置、会员注册和登录链接。普通 SMTP 可以负责这些邮件;批量 Newsletter 不是一回事,自托管 Ghost 当前只支持通过 Mailgun API 发送群发邮件。
在 .env 追加 SMTP 配置:
MAIL_FROM='My Blog <[email protected]>'
MAIL_HOST=smtp.example.com
MAIL_PORT=465
MAIL_SECURE=true
MAIL_USER=替换成SMTP用户名
MAIL_PASSWORD=替换成SMTP密码
再把下面这些字段加入 ghost.environment:
mail__transport: SMTP
mail__from: "${MAIL_FROM}"
mail__options__host: "${MAIL_HOST}"
mail__options__port: "${MAIL_PORT}"
mail__options__secure: "${MAIL_SECURE}"
mail__options__auth__user: "${MAIL_USER}"
mail__options__auth__pass: "${MAIL_PASSWORD}"
重建 Ghost 容器并查看日志:
docker compose up -d ghost
docker compose logs --tail=100 ghost
不要只测试“SMTP 端口能连通”。实际邀请一个测试员工,再走一遍密码重置或会员登录链接。发送域名还要配置 SPF、DKIM、DMARC,否则程序显示发送成功,邮件仍可能进入垃圾箱。
这套 Compose 提供的是核心 Ghost 博客、MySQL 和 HTTPS。Ghost 6 的完整自建 Web Analytics、ActivityPub 等功能依赖官方仍标记为 preview 的新 Docker Compose 工具链,它会额外部署 Caddy、Tinybird 和相关服务。
如果只是写博客、使用主题、会员和 Content API,不需要为了“功能齐全”一次塞进所有组件。确实要自建分析或 Social Web 时,再按 Ghost 官方 Docker 预览文档单独评估资源、迁移和回滚,不要把两套编排直接混在一个文件里。
先创建备份目录:
cd /opt/ghost
mkdir -p backups
用 MySQL root 账号做一致性转储,并在宿主机压缩:
docker compose exec -T db sh -c \
'mysqldump --single-transaction --quick --routines --triggers \
--databases "$MYSQL_DATABASE" -uroot -p"$MYSQL_ROOT_PASSWORD"' \
| gzip > "backups/ghost-db-$(date +%F-%H%M%S).sql.gz"
再备份图片、主题和路由配置所在的内容卷:
docker run --rm \
-v ghost_content:/source:ro \
-v "$PWD/backups:/backup" \
alpine sh -c \
'tar -czf /backup/ghost-content-$(date +%F-%H%M%S).tar.gz -C /source .'
另外保存 compose.yaml 和 .env 的加密副本。备份目录仍在同一台 VPS 上时,硬盘损坏会一起丢失;应再复制到另一台机器或对象存储,并设置保留周期。
恢复测试可以先在非生产 VPS 上进行。数据库转储包含建库语句,导入前先停止 Ghost:
docker compose stop ghost
gzip -dc backups/ghost-db-YYYY-MM-DD-HHMMSS.sql.gz \
| docker compose exec -T db sh -c \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD"'
docker compose start ghost
内容卷恢复会覆盖现有文件,必须确认使用的是正确备份和测试环境。恢复后实际打开后台、文章图片和主题页面,而不是只看导入命令返回 0。更完整的异机演练流程可以参考VPS 备份恢复演练指南。
小版本更新前也先做数据库与内容卷备份:
cd /opt/ghost
docker compose pull ghost
docker compose up -d ghost
docker compose ps
docker compose logs --tail=200 ghost
因为镜像固定为 ghost:6-alpine,这组命令只在 Ghost 6 内更新。准备跨大版本时,先把当前大版本更新到最新小版本,检查 Ghost 的 breaking changes,并用 GScan 验证自定义主题。不要直接把 6 改成 latest 后重建生产站。
升级后的验收至少包括:后台登录、文章编辑、图片上传、会员登录邮件、主题首页和 API 集成。容器没有重启循环,只能证明进程启动了,不能证明博客功能完整。
docker compose ps
docker compose logs --tail=200 db
docker compose exec db mysqladmin ping -h 127.0.0.1 -uroot -p
先看 MySQL 是否 healthy,再核对 .env 与 Ghost 的数据库账号。MySQL 数据卷已经初始化后,修改 MYSQL_PASSWORD 不会自动修改数据库里的旧密码;不要以为重建容器会重新创建账号。
docker compose logs --tail=200 ghost
curl -I http://127.0.0.1:2368
sudo journalctl -u caddy --since '15 minutes ago' --no-pager
本机 2368 都打不开时,问题在 Ghost 或 MySQL;本机正常、域名 502 时,再查 Caddy 和防火墙。不要一上来就重装整套服务。
先确认 .env 中 GHOST_URL 使用 https://,并检查反向代理是否传递 X-Forwarded-Proto: https。Caddy 默认会处理;套了第二层 CDN 或另一台代理时,要继续检查上游协议头。
Caddy 示例把请求体限制为 50MB。文件更大时先确认它是否合理,再同步调整代理和 Ghost 的限制。不要为了省事改成无限制,公开后台仍可能被大请求耗尽磁盘和内存。
db和ghost都显示 healthy;- 2368 只监听
127.0.0.1,3306 没有公网端口; - HTTP 自动跳到 HTTPS,后台没有循环重定向;
- 可以创建管理员、发布文章、上传图片和切换主题;
- 事务邮件实际送达,SPF、DKIM、DMARC 校验通过;
- 重启 VPS 后 Ghost 与 MySQL 自动恢复;
- 数据库和内容卷都已复制到异机,并完成一次恢复测试。
生产环境的健康检查、日志限制和回滚原则,可以继续看VPS Docker Compose 生产配置清单。
如果目标是专心写文章和经营会员,2 核 2GB VPS 就能把 Ghost 跑起来;真正决定这套博客能不能长期使用的,不是首页有多快,而是 URL、邮件、MySQL、图片卷、升级和恢复能否一起工作。把这几条链路验收完,再迁移正式域名和历史文章。
