让运行在 Docker 中的 Ghost 支持Aliyun OSS

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或从新修改使用,但须要注明来源。 署名 4.0 国际 (CC BY 4.0)html

本文做者: 苏洋node

建立时间: 2020年03月14日 统计字数: 8133字 阅读时间: 17分钟阅读 本文连接: soulteary.com/2020/03/14/…mysql


让运行在 Docker 中的 Ghost 支持Aliyun OSS

最近在优化 Ghost 做为线上使用的内容管理后台,做为线上使用的系统,不一样于内部 MIS ,可靠性和应用性能须要有必定保障。git

解决性能问题,最简单的方案即是进行水平扩展,而咱们知道,若是想要让一个服务作到水平可扩展,除了要将应用运行状态单独持久化外,也必须作到文件储存的持久化,云平台的对象储存就是一个很好的文件持久化方案。github

Ghost 是一个典型的单体应用,v3.x 版本的容器化文档其实很少,而介绍如何使用 Aliyun OSS 的文档更是没有,折腾过程仍是挺有趣的,记录下来,但愿可以帮助到后面有需求的同窗。web

写在前面

官方文档在使用三方自定义储存部分其实写的不是很好:sql

  1. 文档有效性不敢恭维,虽然内容中提到支持Aliyun,可是列表中的Aliyun OSS插件仅针对于 1.x 版本,其中Aliyun的 SDK 也比较旧,当时的 Node 环境也很陈旧。
  2. 自定义文档缺乏技术细节、以及完整描述,须要经过实践和阅读源码去验证。
  3. 彻底没提到如何在容器镜像,尤为是官方镜像中使用插件。

本文将经过相对流程化的容器方案,来解决以上问题。docker

以前的文章《从定制 Ghost 镜像聊聊优化 Dockerfile》修理 Ghost 中文输入法的 BUG 有提过,“如何对 Ghost 进行容器化封装”,感兴趣的同窗能够了解下。数据库

在“反复横跳”踩了一堆坑以后,相对稳妥的低成本维护方案即是为 Ghost 编写适合当前版本的储存插件,并制做基于官方容器镜像的补丁镜像了。编程

在编写插件以前,须要先确认官方环境中的 Node 版本,以肯定符号语法:

docker run --rm -it --entrypoint /usr/local/bin/node ghost:3.9.0-alpine -v
复制代码

执行完上述命令,你将获得 v12.16.1 的结果,看来能够直接使用 async/await 来编写插件减小代码量了。

编写 OSS 储存插件

参考官方模版,以及Aliyun OSS SDK 完成储存插件大概十几分钟就搞定了,相关代码我已经上传至 GitHub,若是须要二次封装,能够参考使用。

/**
 * Ghost v3 Storage Adapter (Aliyun OSS)
 * @author soulteary(soulteary@gmail.com)
 */

const AliOSS = require("ali-oss");
const GhostStorage = require("ghost-storage-base");
const { createReadStream } = require("fs");
const { resolve } = require("path");

class AliOSSAdapter extends GhostStorage {
  constructor(config) {
    super();
    this.config = config || {};
    this.oss = new AliOSS({
      region: config.region,
      accessKeyId: config.accessKeyId,
      accessKeySecret: config.accessKeySecret,
      bucket: config.bucket
    });

    this.ossURL = `${config.bucket}.${config.region}.aliyuncs.com`;
    this.regexp = new RegExp(`^https?://${this.ossURL}`, "i");
    this.domain = config.domain || null;
    this.notfound = config.notfound || null;
  }

  async exists(filename, targetDir = this.getTargetDir("/")) {
    try {
      const { status } = await this.oss.head(resolve(targetDir, filename));
      return status === 404;
    } catch (err) {
      return false;
    }
  }
  delete() {
    // it's unnecessary // Ghost missing UX } serve() { return function(req, res, next) { next(); }; } async read(options) { try { const { meta } = await this.oss.head(options.path); if (meta && meta.path) { return meta.path; } else { return this.notfound; } } catch (err) { console.error(`Read Image Error ${err}`); return this.notfound; } } async save(image, targetDir = this.getTargetDir("/")) { try { const filename = await this.getUniqueFileName(image, targetDir); const { url } = await this.oss.put(filename, createReadStream(image.path)); if (url && url.indexOf(`://${this.ossURL}`) > -1) { return this.domain ? url.replace(this.regexp, this.domain) : url; } else { return this.notfound; } } catch (err) { console.error(`Upload Image Error ${err}`); return this.notfound; } } } module.exports = AliOSSAdapter; 复制代码

这里支持的配置内容有:

{
  "storage": {
    "active": "ghost-aliyun-oss-store",
    "ghost-aliyun-oss-store": {
      "accessKeyId": "YOUR_ACCESS_KEY_ID",
      "accessKeySecret": "YOUR_ACCESS_SERCET",
      "bucket": "YOUR_BUCKET_NAME",
      "region": "oss-cn-beijing",
      "domain": "https://your-public-domian",
      "notfound": "https://s3-img.meituan.net/v1/mss_3d027b52ec5a4d589e68050845611e68/ff/n0/0k/4n/3s_73850.jpg"
    }
  }
}
复制代码

其中 domian、是可选项,若是你须要使用 CDN 域名,请在这个字段里配置。

封装支持 OSS 插件的镜像

为了保证运行镜像性能足够高、尺寸相对较小,咱们须要使用 docker multistage build方案。

先定义基础镜像,并安装刚刚编写的 Ghost Aliyun OSS 插件。

FROM ghost:3.9.0-alpine as oss
LABEL maintainer="soulteary@gmail.com"
WORKDIR $GHOST_INSTALL/current
RUN su-exec node yarn --verbose add ghost-aliyun-oss-store
复制代码

接着定义运行使用的镜像。

FROM ghost:3.9.0-alpine
LABEL maintainer="soulteary@gmail.com"
COPY --chown=node:node --from=oss $GHOST_INSTALL/current/node_modules $GHOST_INSTALL/current/node_modules
RUN mkdir -p $GHOST_INSTALL/current/content/adapters/storage/
RUN echo "module.exports = require('ghost-aliyun-oss-store');" > $GHOST_INSTALL/current/content/adapters/storage/ghost-aliyun-oss-store.js
复制代码

参考以前的两篇文章,若是想解决“不能正常进行中文输入”的问题,而且提取出了构建后的内容,能够在镜像中添加下面的内容:

COPY ./docker-assets/admin-views  $GHOST_INSTALL/current/core/server/web/admin/views
COPY ./docker-assets/built/assets $GHOST_INSTALL/current/core/built/assets
复制代码

若是你不但愿将配置单独抽象为文件,能够添加下面的内容。

RUN set -ex; \
    su-exec node ghost config storage.active ghost-aliyun-oss-store; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.accessKeyId YOUR_ACCESS_KEY_ID; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.accessKeySecret YOUR_ACCESS_SERCET; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.bucket YOUR_BUCKET_NAME; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.region oss-cn-beijing; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.domain https://your-public-domian; \
    su-exec node ghost config storage.ghost-aliyun-oss-store.notfound https://s3-img.meituan.net/v1/mss_3d027b52ec5a4d589e68050845611e68/ff/n0/0k/4n/3s_73850.jpg; \
    su-exec node ghost config privacy.useUpdateCheck false; \
    su-exec node ghost config privacy.useGravatar false; \
    su-exec node ghost config privacy.useRpcPing false; \
    su-exec node ghost config privacy.useStructuredData false; \
复制代码

固然,为了更方便更新内容,抽象为单独的文件是更好的选择,好比像下面这样编写 config.production.json 配置文件。

{
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "privacy": {
    "useUpdateCheck": false,
    "useGravatar": false,
    "useRpcPing": false,
    "useStructuredData": false
  },
  "storage": {
    "active": "ghost-aliyun-oss-store",
    "ghost-aliyun-oss-store": {
      "accessKeyId": "YOUR_ACCESS_KEY_ID",
      "accessKeySecret": "YOUR_ACCESS_SERCET",
      "bucket": "baai-news-upload",
      "region": "oss-cn-beijing",
      "domain": "https://your-public-domian",
      "notfound": "https://s3-img.meituan.net/v1/mss_3d027b52ec5a4d589e68050845611e68/ff/n0/0k/4n/3s_73850.jpg"
    }
  }
}
复制代码

完整的容器编排文件

上面聊了许多定制化的选项,那么一个最小可用的容器编排配置是什么样的呢?其实大概不到十行,就足以知足咱们的基础需求。

FROM ghost:3.9.0-alpine as oss
WORKDIR $GHOST_INSTALL/current
RUN su-exec node yarn --verbose add ghost-aliyun-oss-store

FROM ghost:3.9.0-alpine
LABEL maintainer="soulteary@gmail.com"
COPY --chown=node:node --from=oss $GHOST_INSTALL/current/node_modules $GHOST_INSTALL/current/node_modules
RUN mkdir -p $GHOST_INSTALL/current/content/adapters/storage/
RUN echo "module.exports = require('ghost-aliyun-oss-store');" > $GHOST_INSTALL/current/content/adapters/storage/ghost-aliyun-oss-store.js
复制代码

将上面的内容保存为 Dockerfile,若是须要其余的功能,能够参考上面的内容进行适当修改。

docker build -t soulteary/ghost-with-oss:3.9.0 -f Dockerfile .
复制代码

执行上面的命令,稍等片刻,一个衍生自Ghost官方镜像,支持 OSS 的容器镜像就构建完毕了。

如何使用镜像

这里给出一个完整编排文件供你们参考,若是不想使用 Traefik,只须要将端口单独暴露出来便可。

至于 Traefik 如何使用,参考我以往的文章,熟悉以后,你将会发现一片新的天地。

version: "3.6"

services:

  ghost-with-oss:
    image: soulteary/ghost-with-oss:3.9.0
    expose:
      - 2368
    environment:
      url: https://ghost.lab.io
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__port: 3306
      database__connection__user: root
      database__connection__password: ghost
      database__connection__database: ghost
      NODE_ENV: production
    volumes:
      # 这里参考前篇文章,或者本篇文章内容,选择性使用
      # 解决 Ghost 中文输入的问题
      # - ./docker-assets/built/assets:/var/lib/ghost/versions/current/core/built/assets:ro
      # - ./docker-assets/admin-views:/var/lib/ghost/current/core/server/web/admin/views:ro
      - ./config.production.json:/var/lib/ghost/config.production.json    
    extra_hosts:
      - "ghost.lab.io:127.0.0.1"
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.ghostweb.entrypoints=http"
      - "traefik.http.routers.ghostweb.middlewares=https-redirect@file"
      - "traefik.http.routers.ghostweb.rule=Host(`ghost.lab.io`)"
      - "traefik.http.routers.ghostssl.middlewares=content-compress@file"
      - "traefik.http.routers.ghostssl.entrypoints=https"
      - "traefik.http.routers.ghostssl.tls=true"
      - "traefik.http.routers.ghostssl.rule=Host(`ghost.lab.io`)"
      - "traefik.http.services.ghostbackend.loadbalancer.server.scheme=http"
      - "traefik.http.services.ghostbackend.loadbalancer.server.port=2368"

networks:
  traefik:
    external: true
复制代码

将上面内容保存为 docker-compose.yml,使用 docker-compose up -d 启动应用,最后访问配置里定义的域名便可开始使用这个支持 OSS 功能的 Ghost 。

固然,若是你没有线上数据库,也可使用 docker-compose 启动一个数据库:

version: '3'
services:

  db:
    image: mysql:5.7
    container_name: ghost-db
    expose:
      - 3306
    networks:
      - traefik
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ghost
    volumes:
      - ./localdb:/var/lib/mysql

networks:
  traefik:
    external: true
复制代码

最后

本篇内容,以封装 Ghost 定制镜像简单说明了如何基于官方镜像进行扩展,并简单示范了 Docker Multistage Build,以及 Ghost 3.x 版本如何使用 Aliyun OSS。

或许下一篇内容会聊聊,Ghost 这类本来不支持 SSO 单点登陆的应用如何快速接入 SSO。

--EOF


我如今有一个小小的折腾群,里面汇集了一些喜欢折腾的小伙伴。

在不发广告的状况下,咱们在里面会一块儿聊聊软件、HomeLab、编程上的一些问题,也会在群里不按期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴欢迎扫码添加好友。(请注明来源和目的,不然不会经过审核)

关于折腾群入群的那些事

相关文章
相关标签/搜索