讓運行在 Docker 中的 Ghost 支持阿里雲 OSS

讓運行在 Docker 中的 Ghost 支持阿里雲 OSS

最近在優化 Ghost 作為線上使用的內容管理後臺,作為線上使用的系統,不同於內部 MIS ,可靠性和應用性能需要有一定保障。

解決性能問題,最簡單的方案便是進行水平擴展,而我們知道,如果想要讓一個服務做到水平可擴展,除了要將應用運行狀態單獨持久化外,也必須做到文件儲存的持久化,雲平臺的對象儲存就是一個很好的文件持久化方案。

Ghost 是一個典型的單體應用,v3.x 版本的容器化文檔其實不多,而介紹如何使用 Aliyun OSS 的文檔更是沒有,折騰過程還是挺有趣的,記錄下來,希望能夠幫助到後面有需求的同學。

寫在前面

在使用三方自定義儲存部分其實寫的不是很好:

1. 文檔有效性不敢恭維,雖然內容中提到支持阿里雲,但是列表中的僅針對於 1.x 版本,其中阿里雲的 SDK 也比較舊,當時的 Node 環境也很陳舊。

2. 自定義文檔缺少技術細節、以及完整描述,需要通過實踐和閱讀源碼去驗證。

3. 完全沒提到如何在容器鏡像,尤其是官方鏡像中使用插件。

本文將通過相對流程化的容器方案,來解決以上問題。

之前的文章、 有提過,"如何對 Ghost 進行容器化封裝",感興趣的同學可以瞭解下。

在"反覆橫跳"踩了一堆坑之後,相對穩妥的低成本維護方案便是為 Ghost 編寫適合當前版本的儲存插件,並製作基於官方容器鏡像的補丁鏡像了。

在編寫插件之前,需要先確認官方環境中的 Node 版本,以確定符號語法:

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

執行完上述命令,你將得到 v12.16.1 的結果,看來可以直接使用 async/await 來編寫插件減少代碼量了。

編寫 OSS 儲存插件

參考,以及阿里雲 OSS SDK 完成儲存插件大概十幾分鍾就搞定了,相關代碼我已經上傳至 ,如果需要二次封裝,可以參考使用。

/**

* Ghost v3 Storage Adapter (Aliyun OSS)

* @author soulteary([email protected])

*/


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 插件的鏡像

為了保證運行鏡像性能足夠高、尺寸相對較小,我們需要使用 方案。

先定義基礎鏡像,並安裝剛剛編寫的 Ghost Aliyun OSS 插件。

FROM ghost:3.9.0-alpine as oss

LABEL maintainer="[email protected]"

WORKDIR $GHOST_INSTALL/current

RUN su-exec node yarn --verbose add ghost-aliyun-oss-store

接著定義運行使用的鏡像。

FROM ghost:3.9.0-alpine

LABEL maintainer="[email protected]"

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="[email protected]"

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

1


分享到:


相關文章: