讓運行在 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、編程上的一些問題,也會在羣裏不按期的分享一些技術沙龍的資料。

喜歡折騰的小夥伴歡迎掃碼添加好友。(請註明來源和目的,不然不會經過審覈)

關於折騰羣入羣的那些事

相關文章
相關標籤/搜索