Bluesky Docker
feed-generatorを使ってBlueskyのCustomFeedを作るBlueskyのカスタムフィードを作ってみたくなったので、nodeとかDockerとかミリシラですが、自分のサーバーで運用できる状態を構築してみました。
httpsアクセスのための準備
SkyFeedのジェネレーター等を使わず自前のサーバーで運用する場合、サーバーはhttpsプロトコル経由でアクセスできる必要があります。となると、TLSサーバーが必要証明書になるので、certbotを使って証明書を発行しておきます。なお、諸事情により、DNS認証です。
まずは、以下のようなDockerfileを用意します。ローカル環境に入れたくなかったのでdocker経由ですが、ローカル環境に既にcertbotがある場合はそれを利用するでも問題ありません。
FROM ubuntu:latest RUN apt update -y && apt install -y certbot
Dockerfileと同じ階層にletsencryptディレクトリを作ります。
mkdir letsencrypt
docker経由で証明書の生成します。
docker build -t certbot ./ docker run -it -v "./letsencrypt:/etc/letsencrypt" --name certbot certbot:latest /bin/bash # ここからはコンテナの中の操作 docker:/# certbot --manual --preferred-challenges dns certonly -d customfeed.example.com # メールアドレスの入力を求められる Saving debug log to /var/log/letsencrypt/letsencrypt.log Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): ${YOUR_EMAIL_ADDRESS} # 規約の同意等 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must agree in order to register with the ACME server. Do you agree? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: y - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let's Encrypt project and the non-profit organization that develops Certbot? We'd like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: y Account registered. Requesting a certificate for customfeed.example.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please deploy a DNS TXT record under the name: _acme-challenge.customfeed.example.com. with the following value: ${ACME_CHALLENGE_TEXT} # 長ったらしい英数字のテキスト Before continuing, verify the TXT record has been deployed. Depending on the DNS provider, this may take some time, from a few seconds to multiple minutes. You can check if it has finished deploying with aid of online tools, such as the Google Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.coustomfeed.example.com. Look for one or more bolded line(s) below the line ';ANSWER'. It should show the value(s) you've just added. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue
ここまで来たら、自身のドメインのDNS管理画面をブラウザで開き、TXTレコードに_acme-challenge.${YOUR_DOMAIN}と表示されている長い英数字の文字列を設定します。少し待ってから、ターミナルに戻り、Enterを叩きます。
なお、CloudflareのDNSを利用している場合、ドメインのルートやレベル1のサブドメインであれば、Universal SSLが肩代わりしてくれます。ただし、レベル2以上のサブドメインはFreeプランでは対応できないので、ClodflareのDNS上ではProxyを無効にした上で上記のような対応をする必要があります。
Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/customfeed.example.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/customfeed.example.com/privkey.pem
成功すれば、上記のような出力がされ証明書が発行されます。このパスは更新時の差し替えの手間を軽減するためにシンボリックリンクになっていますが、今は実態が欲しいので、シンボリックリンクの先にある実態を取得しておきます。
feed-generatorの取得
bluesky-socialのGitHubリポジトリから、feed-generatorをcloneします。が、直接ソースコードを改変する事になるので、forkした方がいいかもしれません。ここではそのままcloneします。
git clone [email protected]:bluesky-social/feed-generator.git
環境変数の設定
.env.exampleを.envにリネームします。コピーして.envを作っても良いです。この中に、feed-generatorが利用する環境変数を設定します。定義されている環境変数の内、必須で設定する必要があるのは以下の3つです。
- FEEDGEN_LISTENHOST
- リッスンするホスト名。今回はdockerコンテナ内での利用になるので、feed-generatorを動かすコンテナの名前を記述する必要があります(localhostではない)。
- FEEDGEN_HOSTNAME
- feed-generatorを稼動させるサーバーのホスト名。今回の例でいうと、customfeed.example.com。
- FEEDGEN_PUBLISHER_DID
- 自身のdid。
自身のdidはコメントにあるように、以下へアクセスした時に戻ってくる値です。
https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${YOUR_HANDLE}
加えて、取得したフィードを永続化させたい場合は、FEEDGEN_SQLITE_LOCATIONにsqliteのdbへのファイルパスを記述します。今回は/db/feed.dbとします。
上記ふまえて.envを記述すると以下のようになります。
# Whichever port you want to run this on FEEDGEN_PORT=3000 # Change this to use a different bind address FEEDGEN_LISTENHOST="feed-generator" # Set to something like db.sqlite to store persistently FEEDGEN_SQLITE_LOCATION="/db/feed.db" # Don't change unless you're working in a different environment than the primary Bluesky network FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bsky.social" # Set this to the hostname that you intend to run the service at FEEDGEN_HOSTNAME="customfeed.example.com" # Set this to the DID of the account you'll use to publish the feed # You can find your accounts DID by going to # https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${YOUR_HANDLE} FEEDGEN_PUBLISHER_DID="did:plc:abcde...." # Only use this if you want a service did different from did:web # FEEDGEN_SERVICE_DID="did:plc:abcde..." # Delay between reconnect attempts to the firehose subscription endpoint (in milliseconds) FEEDGEN_SUBSCRIPTION_RECONNECT_DELAY=3000
カスタムフィードの構築
ここでは、"bird"という文字列が含まれるポストを取得するカスタムフィードを作る事にします。
まずは、src/algosディレクトリにあるwhats-alf.tsをコピーして、bird.tsを作ります。そして、"whats-alf"となっているshortname変数を"bird"に変更します。
// max 15 chars - export const shortname = 'whats-alf' + export const shortname = 'bird' export const handler = async (ctx: AppContext, params: QueryParams) => {
また、同じディレクトリにあるindex.tsを開き、whats-alf.ts由来のものをbird.ts由来に変更します。
- import * as whatsAlf from './whats-alf' + import * as bird from './bird' type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise<AlgoOutput>
const algos: Record<string, AlgoHandler> = { - [whatsAlf.shortname]: whatsAlf.handler, + [bird.shortname]: bird.handler, }
そして、srcディレクトリのsubscription.tsを開き、"bird"が含まれる投稿を取得するフィルターを作ります。
const postsToCreate = ops.posts.creates .filter((create) => { - // only alf-related posts - return create.record.text.toLowerCase().includes('alf') + return create.record.text.toLowerCase().includes('bird') }) .map((create) => {
Dockerfileの作成
feed-generatorをDockerコンテナ内で稼動させるためのDockerfileを作成します。プロダクション向けに構築したかったので、NODE_ENVをproductionにしたり、npm installにdevをオミットするオプションを付けたりしています。
FROM node:18-alpine WORKDIR /build COPY ./feed-generator /build RUN npm install RUN npm run build FROM node:18-alpine ENV NODE_ENV production WORKDIR /app RUN apk add --no-cache tini COPY --chown=node:node --from=0 /build/dist /app COPY --chown=node:node --from=0 /build/package.json /app/package.json COPY --chown=node:node --from=0 /build/.env /app/.env RUN npm install --omit=dev USER node ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "/app/index.js"]
docker-compose.ymlの作成
feed-generatorのサーバーとリバースプロキシとなるnginxの設定を記述します。今回は自前でproxyの設定を構築するのが面倒だったので、jwilder/nginx-proxyを利用しました。
vhost.d/customfeed.example.comファイルを作り、そこにnginx-proxyが要求するVIRTUAL_HOSTの設定を記述します。
location /xrpc { proxy_pass http://feed-generator:3000/xrpc; #CORS add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Methods "POST, GET, OPTIONS"; add_header Access-Control-Allow-Headers "Origin, Authorization, Accept"; add_header Access-Control-Allow-Credentials true; }
locationをルート(/)にすると、nginx-proxy本体が持っている設定とカニバってしまうため、/xrpcをlocationにしています。また、Dockerのネットワーク上に構築されるため、pxory_passはlocalhostではなく、FEEDGEN_LISTENHOST同様、feed-generatorのコンテナ名を設定します。
次に、TLS証明書を格納するためのcertsディレクトリを用意し、先に作成したfullchain.pemとprivatekey.pemをnginx-proxyのVIRTUAL_HOST名を冠する形で、各々customfeed.example.com.crt、customfeed.example.com.keyにリネームしてcertsディレクトリ内に設置します。
準備が整ったので、docker-compose.ymlを記述します。
version: "3.8" services: feed-generator: container_name: feed-generator build: context: ./ dockerfile: ./Dockerfile volumes: - ./db:/db environment: - VIRTUAL_HOST=customfeed.example.com - VIRTUAL_PORT=3000 networks: - default nginx-proxy: container_name: nginx-proxy image: jwilder/nginx-proxy:alpine volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./vhost.d:/etc/nginx/vhost.d:ro - ./certs:/etc/nginx/certs:ro restart: always environment: - ENABLE_IPV6=true - TZ=Asia/Tokyo networks: - default ports: - 80:80 - 443:443 networks: default:
Custom Feedサーバーを立ち上げる
docker-compose up -d
を実行して、サーバーが立ち上がれば成功です。
https://{FEEDGEN_HOSTNAME}/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://{FEEDGEN_PUBLISHER_DID}/app.bsky.feed.generator/{algos/bird.tsに記述したshortname}
にアクセスしてjsonが返ってくる事を確認します。
Feedのpublish
Blueskyからアクセスしてもらうために、Feedをpublishする必要があります。publishの設定のためfeed-generatorのscripts/publishFeedGen.tsを改変します。
// YOUR bluesky handle // Ex: user.bsky.social - const handle = '' + const handle = {Blueskyのユーザー名} // YOUR bluesky password, or preferably an App Password (found in your client settings) // Ex: abcd-1234-efgh-5678 - const password = '' + const password = {AppPassword} // A short name for the record that will show in urls // Lowercase with no spaces. // Ex: whats-hot - const recordName = '' + const recordName = 'bird' // algos/bird.tsのshortnameと同じ値にする必要がある // A display name for your feed // Ex: What's Hot - const displayName = '' + const displayName = 'Bird' // フィードの表示名 // (Optional) A description of your feed // Ex: Top trending content from the whole network - const description = '' + const description = 'Posts include bird' // フィードの説明 // (Optional) The path to an image to be used as your feed's avatar // Ex: ~/path/to/avatar.jpeg const avatar: string = ''
AppPasswordを直接.tsファイルに記述する事に抵抗が芽生えるので、環境変数経由で読み取るように改変するのが良いと思います。
npm install npm run publishFeed
してもいいんですが、今回はこれもdocker経由で行います。
FROM node:18-alpine WORKDIR /publish COPY feed-generator /publish RUN npm install CMD ["npm", "run", "publishFeed"]
このようなDockerfileをfeed-generatorディレクトリの1つ上の階層に用意します。あとは、docker build & runでpublishされます。
docker build -t publish_feed -f Dockerfile . docker run publish_feed
All done 🎉
が表示されれば、publishは成功です。お疲れ様でした。
2024年2月12日 追記
追記を忘れていたのですが、環境変数FEEDGEN_SUBSCRIPTION_ENDPOINTに指定するURIが、2023年11月半ばに変更されました。 現在は以下のURIを指定しないと正常に動きません。
FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bsky.network"