Ghost Blog 搭建 - 自己动手丰衣足食系列

Ghost Blog 搭建 - 自己动手丰衣足食系列
Photo by Arnel Hasanovic / Unsplash

前言

本文会介绍基于 Docker 快速搭建一套 Ghost 博客,但是不会包含 VPS 申请、域名申请、备案申请等。

为什么选择 Ghost

最早使用 Wordpress,后面有使用一段时间 Jekyll,本次为什么选择 Ghost 呢?
Jekyll 之前是基于 Github 的全静态 Blog,受限于 Github 的速度,国内不是太友好,加之皮肤很少而且还丑首先弃掉了。

Wordpress 基于 php 有非常成熟的社区,网上各种资料都非常多,数量巨大的插件和皮肤。而且不限于 Blog 功能,还能变身电商平台。另外很多 VPS 服务商直接提供了 Wordpress 的镜像可以做到开箱即用。

Ghost 基于 Node.js 开发所以更加的轻量化,时间上也更加的年轻,所以设计、体验以及特性都更加现代化。比如像内建的 SEO,体验更好的编辑器,原生 REST API 支持等。功能上还有非常大的特点:内置付费模式和邮件订阅。

当然以上这些都不是最终选择 Ghost 的原因,唯一原因是「没有用过」,想体验体验。

一点点建议

通过一段时间 Ghost 的使用,给个人使用一点点建议,如果只是想简单写写 Blog,不想太折腾(指不去整那些花活)喜欢更好的体验,可以选择 Ghost。但是如果有很多的想法(功能的扩展,样式啊),也不太想折腾(不想自己手搓各种功能),使用 Wordpress 通过扩展和皮肤会更适合一些。

最后也是最最重要的,对个人博客来说以上平台的选择都是浮云,回归本质内容为王

准备

  • 服务器: VPS 准备,能有单核和 1G 内存就够了,目前使用的腾讯的轻量应用服务器(2 核、2G)
  • 域名: 指向当前服务器的可用域名
  • 系统: 可选择使用 Ubuntu/Debian,且安装好 Docker 环境(Docker 环境和 compose 的简要安装
  • 其它: 使用国内的空间和域名还需要备案等,否则无法访问 80 和 443 端口

搭建

整体结构如下:

考虑是搭建的个人博客,流量不会太大,所以把几个容器放在了一起。如果流量上来了,可以考虑上 CDN,拆成多个容器分别扩容,Ghost 的图片放到各种云存储上等等。如果流量能起来这些都不是问题。

如果对 Docker 已经非常了解,可以直接跳到最终配置,按需进行修改。

Caddy 配置

Caddy 是使用 Golang 编写支持 HTTP/2 的 Web 服务端,在这里作为反向代理和 SSL 证书的自动签发。

创建文件夹

sudo mkdir /opt/ghost
sudo chown {USER_NAME}:{GROUP_NAME} /opt/ghost
mkdir /opt/ghost/caddy

{USER_NAME} 可通过命令 whoami 获取
{GROUP_NAME} 一般和 {USER_NAME} 一致,可通过命令 groups {USER_NAME} 查看当前用户所在的用户组。

创建 Compose 文件

cd /opt/ghost
touch compose.yml
services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    container_name: caddy
    ports:
      - 80:80
      - 443:443
    volumes:
      - /opt/ghost/caddy/Caddyfile:/etc/caddy/Caddyfile

创建 Caddyfile 文件

在目录 /opt/ghost/caddy 路径下创建 Caddyfile,如下内容:

{
    email {MAIL_ADDRESS}
}
(dns_provider) {
    tls {
        dns dnspod {DNSPOD_TOKEN}
    }
}
*.{DOMAIN_NAME}, {DOMAIN_NAME} {
    encode zstd gzip
    import dns_provider
    header {
        Strict-Transport-Security max-age=31536000;
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy no-referrer-when-downgrade
    }
}

其中比较关键的配置:
email 是用于管理网站证书的ACME账户的电子邮件地址,把 {MAIL_ADDRESS} 替换为自己的邮箱地址。
dns_provider DNS 服务的提供商片段,在签发证书时需要增加 DNS 记录进行验证。这里因为使用腾讯 DNSPod 管理域名,所以指定 dns_provider 为 dnspod。同时需要配置对应的 DNSPOD_TOKEN,dnspod 的 TOKEN 格式为 {APP_ID},{APP_TOKEN},获取方式可以参考腾讯帮助文档密钥管理
{DOMAIN_NAME} 替换为自己的域名,其中 * 代表申请的是泛域名证书。

现在用下面的命令启动,测试一下

cd /opt/ghost
docker compose up

会发现启动失败,找到不 dnspod 模块,查了下是官方容器默认没有编译进去,所以需要手动进行编译。

编译 DNS provider

创建 Dockfile

/opt/ghost/caddy 目录下,创建 Dockfile 文件,内容如下

FROM caddy:2-builder-alpine AS builder
ENV GOPROXY "https://goproxy.cn,direct"
RUN xcaddy build \
    --with github.com/caddy-dns/dnspod

FROM caddy:2-alpine

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

需要修改的内容在 --with 后面部分,因为使用 DNSPod 所以编译了对应的模块。完整的 DNS 服务商支持列表可以在 caddy-dns 看到,包含:vultr、cloudflare、digitalocean、godaddy 和 alidns 等。根据自身的 DNS 服务商选择,一般只需要配置 token,具体规则见各模块的说明。

修改 compose.yaml 文件
services:
  caddy:
    # image: caddy:2-alpine
    build: ./caddy
    restart: unless-stopped
    container_name: caddy
    ports:
      - 80:80
      - 443:443
    volumes:
      - /opt/ghost/caddy/Caddyfile:/etc/caddy/Caddyfile

使用自己编译的镜像。

Caddy 的替代方案:ACME + Nginx

也可以使用 acme.shNginx 实现反向代理和 SSL 证书自动续签同样的效果,基于 Docker 的配置可以参考 deploy to docker container 的第 5 部分实现。
这里选择使用 Caddy 也是因为之前没有用过。

Ghost + MySQL

创建文件夹

cd /opt/ghost
mkdir ghost db

修改 compose.yaml 文件

services:
  ...
  
  ghost:
    image: ghost:5-alpine
    restart: unless-stopped
    container_name: ghost
    volumes:
      - /opt/ghost/ghost/data:/var/lib/ghost/content
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__database: ${MYSQL_DATABASE}
      database__connection__user: ${MYSQL_USERNAME}
      database__connection__password: ${MYSQL_PASSWORD}
      mail__transport: SMTP
      mail__options__host: ${SMTP_HOST}
      mail__options__port: ${SMTP_PORT}
      mail__options__auth__user: ${SMTP_USERNAME}
      mail__options__auth__pass: ${SMTP_PASSWORD}
	  mail__from: Notification <{MAIL_ADDRESS}>
      url: https://{DOMAIN_NAME}
    depends_on:
      - db

  db:
    image: mysql:8.0
    restart: unless-stopped
    container_name: db
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USERNAME}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - /opt/ghost/db:/var/lib/mysql

敏感数据的配置放在 /opt/ghost 目录下的 .env 文件中进行定义
mail__ 部分是可选配置,如果只是个人博客也可不配置。MAIL_ADDERSS 配置为发件人的邮件地址
DOMAIN_NAME 替换为域名地址,后继通过 https://{DOAMIN_NAME} 进行访问。

.env 文件配置

MYSQL_USERNAME=ghost
MYSQL_PASSWORD=ghost_password
MYSQL_DATABASE=ghost
MYSQL_ROOT_PASSWORD=root_password

SMTP_HOST={SMTP_SERVER}
SMTP_PORT={SMTP_PORT}
SMTP_USERNAME={SMTP_USERNAME}
SMTP_PASSWORD={SMTP_PASSWORD}

MYSQL_ 配置按需修改
SMTP_ 配置为可选配置,可从邮件服务商处获取

最终配置

目录结构

[/opt/ghost]
 ├─ [caddy]
 │   ├─ Caddyfile
 │   └─ Dockerfile
 ├─ [db]
 ├─ [ghost]
 │   └─ [data]
 ├─ .env
 └─ compose.yaml

Caddyfile

{
    email {MAIL_ADDRESS}
}
(dns_provider) {
    tls {
        dns dnspod {DNSPOD_TOKEN}
    }
}
*.{DOMAIN_NAME}, {DOMAIN_NAME} {
    encode zstd gzip
	import dns_provider
    header {
        Strict-Transport-Security max-age=31536000;
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy no-referrer-when-downgrade
    }
}

Dockerfile

FROM caddy:2-builder-alpine AS builder
ENV GOPROXY "https://goproxy.cn,direct"
RUN xcaddy build \
    --with github.com/caddy-dns/dnspod

FROM caddy:2-alpine

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

.env

MYSQL_USERNAME=ghost
MYSQL_PASSWORD=ghost_password
MYSQL_DATABASE=ghost
MYSQL_ROOT_PASSWORD=root_password

SMTP_HOST={SMTP_SERVER}
SMTP_PORT={SMTP_PORT}
SMTP_USERNAME={SMTP_USERNAME}
SMTP_PASSWORD={SMTP_PASSWORD}

compose.yaml

services:
  caddy:
    image: caddy:2-alpine
    # build: caddy
    restart: unless-stopped
    container_name: caddy
    ports:
      - 80:80
      - 443:443
    volumes:
      - /opt/ghost/caddy/Caddyfile:/etc/caddy/Caddyfile

  ghost:
    image: ghost:5-alpine
    restart: unless-stopped
    container_name: ghost
    volumes:
      - /opt/ghost/ghost/data:/var/lib/ghost/content
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__database: ${MYSQL_DATABASE}
      database__connection__user: ${MYSQL_USERNAME}
      database__connection__password: ${MYSQL_PASSWORD}
      mail__transport: SMTP
      mail__options__host: ${SMTP_HOST}
      mail__options__port: ${SMTP_PORT}
      mail__options__auth__user: ${SMTP_USERNAME}
      mail__options__auth__pass: ${SMTP_PASSWORD}
	  mail__from: Notification <{MAIL_ADDRESS}>
      url: https://{DOMAIN_NAME}
    depends_on:
      - db

  db:
    image: mysql:8.0
    restart: unless-stopped
    container_name: db
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USERNAME}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - /opt/ghost/db:/var/lib/mysql

启动及更新服务

# 启动
docker compose up -d

# Ghost 升级
docker compose down
docker compose pull
docker compose up -d

初见 Ghost

输入 compose.yaml 文件中定义的域名,即可看到 Ghost 的首页

在域名后面加上 /ghost 进入管理的初始化界面,填入一些基本信息完成配置。(可放心填写这些信息后面可随时修改)

现在就可以开始码字了 🎉


参考文档

  1. Building a Caddy container stack for easy HTTPS with Docker and Ghost