使用 Mastodon 搭建個人資訊平臺:前篇

soulteary發表於2022-01-25

本篇文章是使用 Mastodon 搭建個人資訊平臺的第一篇內容,我將聊聊在容器環境中搭建 Mastodon 的一些細節。

同時,這篇文章或許你能夠找到的為數不多的關於如何在容器環境中搭建和優化 Mastodon 服務的內容。

寫在前面

隨著折騰的系統越來越多,我開始期望有一個地方能夠將這些系統中的訊息進行集中的呈現,讓我能夠快速清晰的瞭解到有什麼有趣的新鮮的、重要的事情發生了,以及讓我能夠通過更簡單的方式對已有系統中的資料進行快速的查詢,以及記錄一些突然出現的想法。

我認為以時間軸為線索的 Feed 流形式的資訊展示,配合和各種“虛擬應用”和 Bot 的對話方式或許能夠解決我這個階段的訴求。互動簡單直接、互動操作層級也淺,在多數查詢和記錄場景下,我只需要輸入內容,按下回車就能拿到我想要的資料,而不必開啟具體的應用的頁面,然後再一步一步、一步一步的操作。

簡單的互動示意圖

在以往工作和生活中,其實多多少少也有使用過一些包含了互動或者功能和我訴求有交集的工具,比如:在新浪雲工作使用的 TeamToy、在淘寶時使用的 Redmine 和阿里門戶、美團時使用的大象、之後使用的 Slack、企業微信、學城等等。

十年前在新浪雲使用的 TeamToy

不過這類方案多數都是內部或者 SaaS 化的方案,在個人使用場景下,尤其是結合各種 HomeLab 系統,我更希望它是一個私有化的服務。

對於新增“實體”,我比較剋制,所以所以在此之前的探索過程中,我對 Phabricator、Confluence 、WordPress、Dokuwiki、Outline 等各種我之前比較熟悉的系統都進行過了一些調查和簡單的二次開發,發現雖然能夠解決一部分問題,但是互動和體驗上總感覺不是那麼舒服。因為這些工具或多或少都是基於協作出發,或者基於內容整理出發,而不是資訊匯聚和展示。我需要一個即使一個人使用也能很爽的方案。

於是,我開始徹底嘗試切換思路,尋找一個上文中提到的,以時間軸為資訊展示線索,能夠和工具中的 Bot 互動,來記錄我的想法、將各種我關注的事件實時匯聚到工具中,能夠以簡單的命令和方法查詢各種系統中已有的資料。最終,我選擇了 Mastodon,一個兩年前我就已經摺騰過一陣的 “Twitter / Weibo Like” 的產品。

已經成長到兩萬顆星星的 Mastodon

在開始折騰之前,我們先來聊聊它的技術架構。

技術架構

Mastodon 的技術架構屬於比較經典的 Web 架構,主要的功能元件有:前端應用(React SPA)、應用介面(Ruby Rails6)、推送服務(Node Express + WS)、後臺任務(Ruby Sidekiq)、快取和佇列(Redis)、資料庫(Postgres),以及可選的全文索引(Elasticsearch 7)構成。

Mastodon 應用架構中的主要構成

除此之外,支援使用匿名網路通訊的方式和網際網路上其他不同的社群例項通訊,交換社群已釋出內容,來完成其分散式社群的構想。不過這個功能不在本文範圍之內,而且非常簡單,就不囉嗦展開了。

基礎服務準備

在折騰應用之前,我們先完成應用對於基礎服務的依賴設施的搭建。先來聊聊網路規劃。

搭建應用閘道器,進行網路規劃

和以往應用一樣,我們使用 Traefik 作為服務應用閘道器,讓應用可以使用服務註冊的方式動態地接入 Traefik。並且使用 Traefik 提供 SSL 裝載、基礎的 SSO 鑑權等。

如果你還不瞭解 Traefik,可以閱讀之前的內容進行學習和了解。

Mastodon 所在主機網路規劃

我希望 Mastodon 各個元件在能夠通訊、必要的服務能夠使用 Traefik 進行服務註冊,提供 Web 訪問的前提下,還能和主機上其他的容器服務在網路層面相互隔離。

出於上面的考慮,我們可以執行命令,建立一個額外的虛擬網路卡進行元件之間的通訊打通:

docker network create mastodon_networks

搭建資料庫:Postgres

官方配置檔案中,對於資料庫的定義是這樣的:

version: '3'
services:

  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - internal_network
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
    volumes:
      - ./postgres14:/var/lib/postgresql/data
    environment:
      - "POSTGRES_HOST_AUTH_METHOD=trust"

雖然也能使用,但是資料庫執行之後,我們會收到程式到一些執行警告。

********************************************************************************
WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow
         anyone with access to the Postgres port to access your database without
         a password, even if POSTGRES_PASSWORD is set. See PostgreSQL
         documentation about "trust":
         https://www.postgresql.org/docs/current/auth-trust.html
         In Docker's default configuration, this is effectively any other
         container on the same system.

         It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace
         it with "-e POSTGRES_PASSWORD=password" instead to set a password in
         "docker run".
********************************************************************************

在應用執行過程中,資料庫終端會不斷地積累一些請求日誌、後臺任務執行結果日誌輸出,最終會產生一個非常大的應用日誌檔案。在極端的情況下,甚至可能因此將磁碟佔滿,影響整臺伺服器上其他應用的正常執行。

所以,我結合實際狀況,我對上面的配置做了一些簡單調整:

version: '3'
services:

  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/var/lib/postgresql/data
    environment:
      - "POSTGRES_DB=mastodon"
      - "POSTGRES_USER=mastodon"
      - "POSTGRES_PASSWORD=mastodon"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

將上面的內容儲存到 postgres 目錄的 docker-compose.yml 檔案中之後,我們使用 docker-compose up -d 啟動服務,稍等片刻,使用 docker-compose ps 檢視應用,可以看到服務執行正常。

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
postgres-db-1       "docker-entrypoint.s…"   db                  running (healthy)   5432/tcp

這部分的配置和程式碼已經上傳至 GitHub,有需要可以自取:https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/postgres

搭建快取和佇列服務:Redis

預設的 Redis 啟動會在 30秒之後提供服務,對於我們而言有一些久。為了讓 Redis 開始提供響應的時間更快,我同樣對官方配置中的內容進行了簡單的調整:

version: '3'
services:

  redis:
    restart: always
    image: redis:6-alpine
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/data
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

將配置儲存到 redis 目錄的 docker-compose.yml 後,我們使用 docker-compose up -d 啟動服務,稍等片刻,使用 docker-compose ps 檢視應用,可以看到服務執行正常。

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
redis-redis-1       "docker-entrypoint.s…"   redis               running (healthy)   6379/tcp

這部分的配置和程式碼也已經上傳至 GitHub,有需要可以自取:https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/redis

搭建全文檢索:Elasticsearch

這個元件對於 Mastodon 是可選的,有幾個情況下你可能不需要使用 ES:

  • 你的機器資源非常緊張,啟用 ES 將額外的佔用 500MB~1GB 的記憶體
  • 你的站點內容和使用者數並不多
  • 你的搜尋次數非常有限
  • 你期望使用資源和效能更高的檢索方案

在 2018 年的 PG CONF EU 上,Oleg Bartunov 曾經做過一個分享,關於使用 Postgres 在全文檢索場景的使用,感興趣可以自行了解

當然,出於對官方選擇的尊重,我們還是簡單展開一下 ES 的搭建和使用。同樣基於官方配置進行簡單調整,可以完成一個新的基礎編排檔案:

version: '3'
services:

  es:
    restart: always
    container_name: es-mastodon
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.name=es-mastodon"
      - "discovery.type=single-node"
      - "bootstrap.memory_lock=true"
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/usr/share/elasticsearch/data:rw
    ulimits:
      memlock:
        soft: -1
        hard: -1
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

不過,如果我們將上面的編排檔案儲存,並嘗試啟動服務,會遇到一個經典的問題,目錄許可權不正確,服務無法啟動:

"stacktrace": ["org.elasticsearch.bootstrap.StartupException: ElasticsearchException[failed to bind service]; nested: AccessDeniedException[/usr/share/elasticsearch/data/nodes];",
"at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:174) ~[elasticsearch-7.10.2.jar:7.10.2]",
...

ElasticsearchException[failed to bind service]; nested: AccessDeniedException[/usr/share/elasticsearch/data/nodes];
Likely root cause: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes
    at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)
...

解決問題的方案很簡單,我們將資料目錄的許可權設定為容器內的 ES 程式可操作即可:(強烈不推薦使用簡單粗暴的 chmod 777

mkdir -p data
chown -R 1000:1000 data
docker-compose down && docker-compose up -d

執行完上述命令,重啟容器程式之後,再次使用 docker-compose ps 命令檢視應用狀況,我們可以看到程式執行正常。

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
es-mastodon         "/tini -- /usr/local…"   es                  running (healthy)   9300/tcp

這部分的配置和程式碼也已經上傳至 GitHub,有需要可以自取: https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/elasticsearch

應用搭建

基礎服務搭建完畢之後,我們來完成應用的搭建和部署。

應用初始化

為了方便應用初始化,我寫了一個簡單的編排配置:

version: "3"
services:
  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; tail -f /etc/hosts"
    networks:
      - mastodon_networks

networks:
  mastodon_networks:
    external: true

將上面的內容儲存為 docker-compose.init.yml,接著先使用 docker-compose up -d 啟動一個 Mastodon 安裝就緒的容器備用。

在容器啟動之後,我們執行下面的命令啟動 Mastodon 安裝載入程式:

docker-compose -f docker-compose.init.yml exec web bundle exec rake mastodon:setup

執行完畢上面的命令,會進入互動式命令列,我們忽略掉所有的警告資訊,可以得到類似下面的日誌(示例,你可以根據自己的情況調整)

Your instance is identified by its domain name. Changing it afterward will break things.
Domain name: hub.lab.com

Single user mode disables registrations and redirects the landing page to your public profile.
Do you want to enable single user mode? yes

Are you using Docker to run Mastodon? Yes

PostgreSQL host: db
PostgreSQL port: 5432
Name of PostgreSQL database: postgres
Name of PostgreSQL user: postgres
Password of PostgreSQL user: 
Database configuration works! ?

Redis host: redis
Redis port: 6379
Redis password: 
Redis configuration works! ?

Do you want to store uploaded files on the cloud? No

Do you want to send e-mails from localhost? yes
E-mail address to send e-mails "from": "(Mastodon <notifications@hub.lab.com>)"
Send a test e-mail with this configuration right now? no

This configuration will be written to .env.production
Save configuration? Yes
Below is your configuration, save it to an .env.production file outside Docker:

# Generated with mastodon:setup on 2022-01-24 08:49:51 UTC

# Some variables in this file will be interpreted differently whether you are
# using docker-compose or not.

LOCAL_DOMAIN=hub.lab.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=ce1111c9cd51305cd680aee4d9c2d6fe71e1ba003ea31cc27bd98792653535d72a13c386d8a7413c28d30d5561f7b18b0e56f0d0e8b107b694443390d4e9a888
OTP_SECRET=bcb50204394bdce54a0783f1ef2e72a998ad2f107a0ee4dc3b61557f5c12b5c76267c0512e3d08b85f668ec054d42cdbbe0a42ded70cbd0a70be70346e666d05
VAPID_PRIVATE_KEY=QzEMwqTatuKGLSI3x4gmFkFsxi2Vqd4taExqQtZMfNM=
VAPID_PUBLIC_KEY=BFBQg5vnT3AOW2TBi7OSSxkr28Zz2VZg7Jv203APIS5rPBOveXxCx34Okur-8Rti_sD07P4-rAgu3iBSsSrsqBE=
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=mastodon
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS="Mastodon <notifications@hub.lab.com>"

It is also saved within this container so you can proceed with this wizard.

Now that configuration is saved, the database schema must be loaded.
If the database already exists, this will erase its contents.
Prepare the database now? Yes
Running `RAILS_ENV=production rails db:setup` ...


Database 'postgres' already exists
[strong_migrations] DANGER: No lock timeout set
Done!

All done! You can now power on the Mastodon server ?

Do you want to create an admin user straight away? Yes
Username: soulteary
E-mail: soulteary@gmail.com
You can login with the password: 76a17e7e1d52056fdd0fcada9080f474
You can change your password once you login.

在上面的互動程式中,為了節約時間,我選擇了不使用外部服務儲存檔案、不使用外部服務傳送郵件,你可以根據自己的需求進行調整。

在命令執行過程中,我們可能會看到一些和 Redis 相關的報錯資訊:Error connecting to Redis on localhost:6379 (Errno::ECONNREFUSED) 。這是因為我們在啟動配置程式,進行應用初始化的時候,並沒有預先正確配置 Redis 伺服器,這並不說明我們的配置是錯誤的,只是尚未生效,不必驚慌。

這部分的配置和程式碼也已經上傳至 GitHub,有需要可以自取:https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/app

更新應用配置

接下來,我們需要將上面日誌輸出中和配置有關的資訊儲存到一個配置檔案 .env.production 裡。

# Generated with mastodon:setup on 2022-01-24 08:49:51 UTC

# Some variables in this file will be interpreted differently whether you are
# using docker-compose or not.

LOCAL_DOMAIN=hub.lab.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=ce1111c9cd51305cd680aee4d9c2d6fe71e1ba003ea31cc27bd98792653535d72a13c386d8a7413c28d30d5561f7b18b0e56f0d0e8b107b694443390d4e9a888
OTP_SECRET=bcb50204394bdce54a0783f1ef2e72a998ad2f107a0ee4dc3b61557f5c12b5c76267c0512e3d08b85f668ec054d42cdbbe0a42ded70cbd0a70be70346e666d05
VAPID_PRIVATE_KEY=QzEMwqTatuKGLSI3x4gmFkFsxi2Vqd4taExqQtZMfNM=
VAPID_PUBLIC_KEY=BFBQg5vnT3AOW2TBi7OSSxkr28Zz2VZg7Jv203APIS5rPBOveXxCx34Okur-8Rti_sD07P4-rAgu3iBSsSrsqBE=
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=mastodon
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS="Mastodon <notifications@hub.lab.com>"

這裡需要注意的一點是,傳送郵件通知配置中的 SMTP_FROM_ADDRESS 的內容需要使用雙引號包裹,如果在上面互動式終端配置過程中,我們使用回車“一路 Next” 可能會出現生成的配置內容漏加引號的問題。

如果出現了這個問題,手動在儲存檔案的時候加上引號就行,不需要重新執行命令。

調整應用 Web 服務配置

和之前搭建基礎設施和調整配置一樣,我們針對官方配置模版進行一個簡單的調整,可以得到讓服務執行最小的容器編排配置:

version: '3'
services:

  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    env_file: .env.production
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - mastodon_networks
    healthcheck:
     test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
     interval: 15s
     retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  streaming:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: node ./streaming
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    environment:
      - "STREAMING_CLUSTER_NUM=1"
      - "NODE_ENV=production"

  sidekiq:
    image: tootsuite/mastodon:v3.4.4
    environment:
      - "RAILS_ENV=production"
    env_file: .env.production
    restart: always
    command: bundle exec sidekiq
    networks:
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro


networks:
  mastodon_networks:
    external: true

將上面的內容儲存後,我們將服務啟動。因為此時我們並未對映任何埠到伺服器“本地”,所以暫時我們還不能訪問這些服務。

為了解決這個問題,我們需要配置 Mastodon 這個應用的前端代理。

配置服務前端代理

服務預設使用 Ruby Puma 作為 Web 伺服器、Node Express 提供推送和實時更新。為了解決前端資源跨域問題、以及進一步提升服務效能,我們可以採用 Nginx 對這些服務提供反向代理,將服務聚合在一起,並對其中的靜態資源進行一定的快取。

官方這裡有一個預設的模版,https://github.com/mastodon/mastodon/blob/main/dist/nginx.conf,不過這個配置適用於不使用容器、或者應用都執行在容器,Nginx 不使用容器執行的場景。

考慮到我們使用 Traefik 提供動態的服務註冊和 SSL 證照掛載,所以這個配置我們需要稍作調整才能使用(僅展示主要改動)。

location / {
  try_files $uri @proxy;
}

location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
  add_header Cache-Control "public, max-age=31536000, immutable";
  try_files $uri @proxy;
}

location /sw.js {
  add_header Cache-Control "public, max-age=0";
  try_files $uri @proxy;
}

location @proxy {
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto "https";
  proxy_set_header Proxy "";
  proxy_pass_header Server;

  proxy_pass http://web:3000;
  proxy_buffering on;
  proxy_redirect off;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  proxy_cache CACHE;
  proxy_cache_valid 200 7d;
  proxy_cache_valid 410 24h;
  proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
  add_header X-Cached $upstream_cache_status;

  tcp_nodelay on;
}

location /api/v1/streaming {
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto "https";
  proxy_set_header Proxy "";

  proxy_pass http://streaming:4000;
  proxy_buffering off;
  proxy_redirect off;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  tcp_nodelay on;
}

在上面的配置中,Nginx “整合”了我們提到的來自 Ruby 和 Node 的兩套 Web 服務,並且針對靜態資源做了基礎的快取操作,對於可以快取的內容做了長達一週的 LRU 快取。

看到這裡,我們的服務似乎能正常的跑起來了。但是,是真的沒有問題嗎?

應用問題修正和架構調優

當我們將服務執行起來之後,即使應用看上去一切正常,此刻我們會遇到第一個問題。日誌裡頻繁出現“X-Accel-Mapping header missing”的警告提示。

觸發這個問題的原因在 https://github.com/mastodon/mastodon/issues/3221 中有被披露,不過社群並沒有給出好的解決方案。解決這個問題其實很簡單,將靜態資源徹底從 Ruby Web 服務中遷出即可:一來可以解決這個問題,二來則可以提升服務整體效能,以及在未來讓服務更容易做水平擴充套件。

同時,當我們嘗試上傳圖片或者視訊的時候,你會發現由於容器掛載目錄的許可權問題,我們始終會得到錯誤的返回。有的人會使用 chmod 777 大法解決問題,然而這個並不是一個最佳實踐:存在潛在的安全問題,並且讓你的應用水平擴充套件的能力變得很差。

當然,還有一些細節問題,我們稍後再處理,先處理以上兩個問題。

拆分靜態資源服務

提到應用動靜資源拆分,在雲服務大環境下我們不免會想到 CDN。在 Mastodon 中,應用支援設定 CDN_HOST 來將靜態資源拆分到 CDN 伺服器。不過多數的服務維護者會採用讓 CDN 動態回源的方案來進行實現,在忽略一定程度的資料一致性的前提下,這樣的維護成本非常低,無需做任何調整和應用改動。

但是僅僅這樣做並解決不了我們在前文中提到的問題(CDN時效到了,還是會回源出觸發上面的問題)。並且也不利於私有化部署和使用(有額外的成本,不得不依賴公網服務)。

這裡有一個更好的方案是將我們的靜態資源重新封裝為一個獨立的服務執行

參考以往文章中針對容器進行多階段構建和優化的內容,很容易可以寫出類似下面的 Dockerfile:

FROM tootsuite/mastodon:v3.4.4 AS Builder

FROM nginx:1.21.4-alpine
COPY --from=Builder /opt/mastodon/public /usr/share/nginx/html

使用 docker build -t mastodon-assets . 將 Mastodon 的靜態資源和 Nginx 打包為一個新的映象之後,接著來編寫這個服務的容器編排配置:

version: '3'
services:

  mastodon-assets:
    image: mastodon-assets
    restart: always
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"

      - "traefik.http.middlewares.cors.headers.accessControlAllowMethods=GET,OPTIONS"
      - "traefik.http.middlewares.cors.headers.accessControlAllowHeaders=*"      
      - "traefik.http.middlewares.cors.headers.accessControlAllowOriginList=*"
      - "traefik.http.middlewares.cors.headers.accesscontrolmaxage=100"
      - "traefik.http.middlewares.cors.headers.addvaryheader=true"

      - "traefik.http.routers.mastodon-assets-http.middlewares=cors@docker"
      - "traefik.http.routers.mastodon-assets-http.entrypoints=http"
      - "traefik.http.routers.mastodon-assets-http.rule=Host(`hub-assets.lab.com`)"
 
      - "traefik.http.routers.mastodon-assets-https.middlewares=cors@docker"
      - "traefik.http.routers.mastodon-assets-https.entrypoints=https"
      - "traefik.http.routers.mastodon-assets-https.tls=true"
      - "traefik.http.routers.mastodon-assets-https.rule=Host(`hub-assets.lab.com`)"

      - "traefik.http.services.mastodon-assets-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-assets-backend.loadbalancer.server.port=80"

networks:
  traefik:
    external: true

將上面的內容儲存為 docker-compose.yml 之後,使用 docker-compose up -d 啟動服務,就將原本使用 Ruby 服務吞吐的靜態資源,切換到了使用獨立的 Nginx 服務來完成靜態資源吞吐的目的了。

當然,為了這個操作能夠生效,我們還需要在 .env.production 中新增下面的配置內容:

CDN_HOST=https://hub-assets.lab.com

獨立維護上傳資源

前文提到過,在預設的容器應用中,程式邏輯是讓 Ruby 應用維護和處理我們上傳的媒體檔案(圖片、視訊)。這個方案同樣不利於服務未來的水平擴充套件和拆分到合適的機器上執行,一個相對更好的方案是使用 S3 服務來針對使用者上傳的檔案進行管理,讓應用接近於無狀態執行。

《裝在筆記本里的私有云環境:網路儲存篇(上)》《裝在筆記本里的私有云環境:網路儲存篇(中)》兩篇內容中,我有介紹過如何使用 MinIO 來作為通用的儲存閘道器使用。所以,如何搭建和監控一個私有的 S3 服務,在這裡就不再贅述了,這裡僅聊聊一些不同之處。

這裡我採用的是同機部署,所以服務之間的訪問,是通過虛擬網路卡來解決的。因為服務都在 Traefik “後面”,所以互動協議也儘可以脫去 HTTPS (讓 Mastodon 直接使用容器服務名稱訪問即可)。

這裡有一個小細節,為了服務的正常執行,我們的 S3 Entrypoint 需要使用常見埠,比如 HTTP(80)、HTTPS(443),所以在 MinIO 服務中的執行命令需要調整為:

command: minio server /data --address 0.0.0.0:80 --listeners 1  --console-address 0.0.0.0:9001

但如果我們使用 HTTP 的話,會引出另外一個問題,就是 Mastodon 展示靜態資源的時候,會使用 HTTP 協議而不是我們期望的 HTTPS,這會造成在 Web 介面的媒體資源無法展示的問題。(不影響客戶端,如何解決限於篇幅,我們將在下篇內容中提到)

此外在 Mastodon 中使用 S3 服務作為檔案儲存後端,因為 S3 服務預設提供的 URL 路徑是 S3_DOMAIN_NAME/S3_BUCKET_NAME,所以我們需要在 S3_ALIAS_HOST 配置中做相同的設定。不過考慮到資源的訪問效能和效率問題,我們同樣可以啟動一個 Nginx 作為 MinIO 的靜態資源快取,並且進一步簡化這個配置,讓我們直接設定 S3_DOMAIN_NAME 即可,同樣會方便我們後續進行程式定製。

先來編寫這個服務的編排配置:

version: "3"
services:

  nginx-minio:
    image: nginx:1.21.4-alpine
    restart: always
    networks:
      - traefik
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 15s
      retries: 12
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.mastodon-s3-http.entrypoints=http"
      - "traefik.http.routers.mastodon-s3-http.rule=Host(`hub-res.lab.com`)"
      - "traefik.http.routers.mastodon-s3-https.entrypoints=https"
      - "traefik.http.routers.mastodon-s3-https.tls=true"
      - "traefik.http.routers.mastodon-s3-https.rule=Host(`hub-res.lab.com`)"
      - "traefik.http.services.mastodon-s3-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-s3-backend.loadbalancer.server.port=80"

  minio:
    image: ${DOCKER_MINIO_IMAGE_NAME}
    container_name: ${DOCKER_MINIO_HOSTNAME}
    volumes:
      - ./data/minio/data:/data:z
    command: minio server /data --address 0.0.0.0:80 --listeners 1  --console-address 0.0.0.0:9001
    environment:
      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
      - MINIO_REGION_NAME=${MINIO_REGION_NAME}
      - MINIO_BROWSER=${MINIO_BROWSER}
      - MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL}
      - MINIO_PROMETHEUS_AUTH_TYPE=public
    restart: always
    networks:
      - traefik
      - mastodon_networks
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.middlewares.minio-gzip.compress=true"
      - "traefik.http.routers.minio-admin.middlewares=minio-gzip"
      - "traefik.http.routers.minio-admin.entrypoints=https"
      - "traefik.http.routers.minio-admin.tls=true"
      - "traefik.http.routers.minio-admin.rule=Host(`${DOCKER_MINIO_ADMIN_DOMAIN}`)"
      - "traefik.http.routers.minio-admin.service=minio-admin-backend"
      - "traefik.http.services.minio-admin-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.minio-admin-backend.loadbalancer.server.port=9001"
    extra_hosts:
      - "${DOCKER_MINIO_HOSTNAME}:0.0.0.0"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/minio/health/live"]
      interval: 3s
      retries: 12
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true
  traefik:
    external: true

接著來進行 Nginx 的配置編寫:

server {
    listen 80;
    server_name localhost;

    keepalive_timeout 70;
    sendfile on;
    client_max_body_size 80m;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;

        proxy_connect_timeout 300;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;

        proxy_pass http://minio/mastodon/;
    }

    location /health {
        access_log off;
        return 200 "ok";
    }
}

然後是 MinIO 初始化指令碼 docker-compose.init.yml 的編寫:

version: "3"
services:

  minio-client:
    image: ${DOCKER_MINIO_CLIENT_IMAGE_NAME}
    entrypoint: >
      /bin/sh -c "
      /usr/bin/mc config host rm local;
      /usr/bin/mc config host add --quiet --api s3v4 local http://minio ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
      /usr/bin/mc mb --quiet local/${DEFAULT_S3_UPLOAD_BUCKET_NAME}/;
      /usr/bin/mc policy set public local/${DEFAULT_S3_UPLOAD_BUCKET_NAME};
      "
    networks:
      - traefik

networks:
  traefik:
    external: true

最後是 MinIO 執行所需要的基礎配置資訊 .env

# == MinIO
# optional: Set a publicly accessible domain name to manage the content stored in Outline

DOCKER_MINIO_IMAGE_NAME=minio/minio:RELEASE.2022-01-08T03-11-54Z
DOCKER_MINIO_HOSTNAME=mastodon-s3-api.lab.com
DOCKER_MINIO_ADMIN_DOMAIN=mastodon-s3.lab.com
MINIO_BROWSER=on
MINIO_BROWSER_REDIRECT_URL=https://${DOCKER_MINIO_ADMIN_DOMAIN}
# Select `Lowercase a-z and numbers` and 16-bit string length https://onlinerandomtools.com/generate-random-string
MINIO_ROOT_USER=6m2lx2ffmbr9ikod
# Select `Lowercase a-z and numbers` and 64-bit string length https://onlinerandomtools.com/generate-random-string
MINIO_ROOT_PASSWORD=2k78fpraq7rs5xlrti5p6cvb767a691h3jqi47ihbu75cx23twkzpok86sf1aw1e
MINIO_REGION_NAME=cn-homelab-1

# == MinIO Client
DOCKER_MINIO_CLIENT_IMAGE_NAME=minio/mc:RELEASE.2022-01-07T06-01-38Z

DEFAULT_S3_UPLOAD_BUCKET_NAME=mastodon

如何啟動和使用 MinIO 在之前的文章中有介紹,限於篇幅字數限制,就不做展開了。感興趣請自行翻閱,相關的程式碼已經上傳至 GitHub https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/minio

最終應用配置

好了,將上面的內容稍加整合,再做一些簡單的調整,我們便可以得到類似下面的配置啦:

version: '3'
services:

  mastodon-gateway:
    image: nginx:1.21.4-alpine
    restart: always
    networks:
      - traefik
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./nginx.conf:/etc/nginx/nginx.conf
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 15s
      retries: 12
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.mastodon-nginx-http.entrypoints=http"
      - "traefik.http.routers.mastodon-nginx-http.rule=Host(`hub.lab.com`)"
      - "traefik.http.routers.mastodon-nginx-https.entrypoints=https"
      - "traefik.http.routers.mastodon-nginx-https.tls=true"
      - "traefik.http.routers.mastodon-nginx-https.rule=Host(`hub.lab.com`)"
      - "traefik.http.services.mastodon-nginx-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-nginx-backend.loadbalancer.server.port=80"

  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    env_file: .env.production
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  streaming:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: node ./streaming
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    environment:
      - "STREAMING_CLUSTER_NUM=1"

  sidekiq:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: bundle exec sidekiq
    networks:
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

networks:
  mastodon_networks:
    external: true
  traefik:
    external: true

此外,因為我們配置了獨立的靜態資源服務和檔案儲存服務,所以需要在 .env.production 中額外新增一些配置:

CDN_HOST=https://hub-assets.lab.com
S3_ENABLED=true
S3_PROTOCOL=http
S3_REGION=cn-homelab-1
S3_ENDPOINT=http://mastodon-s3-api.lab.com
S3_BUCKET=mastodon
AWS_ACCESS_KEY_ID=6m2lx2ffmbr9ikod
AWS_SECRET_ACCESS_KEY=2k78fpraq7rs5xlrti5p6cvb767a691h3jqi47ihbu75cx23twkzpok86sf1aw1e
S3_ALIAS_HOST=hub-res.lab.com

使用熟悉的 docker-compose up -d 啟動服務,稍等片刻,我們便能看到正常啟動的應用了。

這部分相關的程式碼,已經上傳至 GitHub https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/app,有需要自取即可。

Mastodon 應用啟動後的第一個介面

點選登入,使用我們剛剛建立應用配置時的賬號郵箱和初始化密碼即可完成應用登陸,開始對 Mastodon 的探索啦。

登入 Mastodon 後的介面

最後

即使一再精簡內容,本文的字數也超過了多數平臺的長度限制,所以如果你在閱讀的過程中發現有一部分缺失,可以嘗試閱讀原文或者 GitHub 上的完整示例檔案來解決問題。

下一篇文章中,我將聊聊如何針對效能進一步做一些調優操作,以及解決本文未解決完的一些問題。

後續將陸續整理和分享一些在知識管理、知識庫建設過程中的小經驗,希望能幫助到同樣對這個領域感興趣、充滿好奇心的你。

--EOF


如果你覺得內容還算實用,歡迎點贊分享給你的朋友,在此謝過。

如果你想更快的看到後續內容的更新,請不吝“點贊”或“轉發分享”,這些免費的鼓勵將會影響後續有關內容的更新速度。


本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

原文連結: https://soulteary.com/2022/01...

相關文章