+++ date = "2024-01-08" tags = ["bluesky","pds"] title = "blueskyをself-hostする" slug = "bluesky" +++ 少し前にblueskyをself-host(セルフホスト)しました。 ![](https://raw.githubusercontent.com/syui/img/master/other/bluesky_atproto_self_hosting_20240108_0001.png) - https://web.syu.is |server|url|src| |---|---|---| |pds|https://syu.is|[src](https://github.com/bluesky-social/atproto/tree/main/services/pds)| |plc|https://plc.syu.is|[src](https://github.com/did-method-plc/did-method-plc/tree/main/packages/server)| |mod|https://mod.syu.is|[src](https://github.com/bluesky-social/atproto/tree/main/services/ozone)| |bgs|https://bgs.syu.is|[src](https://github.com/bluesky-social/indigo/tree/main/cmd/bigsky)| |appview|https://api.syu.is|[src](https://github.com/bluesky-social/atproto/tree/main/services/bsky)| |web|https://web.syu.is|[src](https://github.com/bluesky-social/social-app)| 以前はbluesky(pds+appview)を動かしていたのですが、一般的にblueskyと呼ばれるものは複数のserverに依存しています。例えば、plc, bgsです。今ではpdsにあったappviewも分離しています。 bsky.teamがそれぞれのsandboxを用意してくれていますが、全部をself-hostしないといつか動かなくなります。 |server|env|body| |---|---|---| |pds|PDS_DID_PLC_URL|`https://plc.${host}`| |pds|PDS_BSKY_APP_VIEW_URL|`https://api.${host}`| |pds|PDS_BSKY_APP_VIEW_DID|did:web:api.${host}| |pds|PDS_MOD_SERVICE_URL|`https://mod.${host}`| |pds|PDS_MOD_SERVICE_DID|did:web:mod.${host}| |pds|PDS_CRAWLERS|`https://bgs.${host}`| nostrで活動されているikuradonさんが全部のserverをセルフホストしているのを見て、どうやら他のserverも動作できる環境にあるようだと思ったので立ててみました。なお、ikuradonさんに結構助けてもらった。 今回、`syu.is`というdomainを取りました。なお、`.is`はあまりおすすめしません。これはアイスランドが独自方針で管理している感じで、メール認証などが必要です。その他、様々な制限があります。 ```sh git clone https://github.com/bluesky-social/atproto cd atproto git clone https://github.com/did-method-plc/did-method-plc ./repos/did-method-plc git clone https://github.com/bluesky-social/indigo ./repos/indigo git clone https://github.com/bluesky-social/social-app ./repos/social-app touch .plc.env .bsky.env .bgs.env .pds.env .db.env .mod.env .web.env ``` ```sh mkdir -p ./postgres/init echo '-- plc CREATE DATABASE plc; GRANT ALL PRIVILEGES ON DATABASE plc TO postgres; -- bgs CREATE DATABASE bgs; CREATE DATABASE carstore; GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres; GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres; -- bsky(appview) CREATE DATABASE bsky; GRANT ALL PRIVILEGES ON DATABASE bsky TO postgres; -- ozone(moderation) CREATE DATABASE mod; GRANT ALL PRIVILEGES ON DATABASE mod TO postgres; -- pds CREATE DATABASE pds; GRANT ALL PRIVILEGES ON DATABASE pds TO postgres; ' >> ./postgres/init/init.sql ``` ### compose.yaml docker composeで構築しました。大体は以下のような感じの構成です。 下記は最小構成なので、自分なりに読み替えてください。 ```yml:compose.yaml services: plc: ports: - 2582:3000 build: context: ./repos/did-method-plc/ dockerfile: packages/server/Dockerfile env_file: - ./.plc.env depends_on: - db bgs: ports: - 2470:2470 build: context: ./repos/indigo/ dockerfile: cmd/bigsky/Dockerfile env_file: - ./.bgs.env volumes: - ./data/bgs/:/data/ depends_on: - db bsky: ports: - 2584:3000 build: context: ./ dockerfile: services/bsky/Dockerfile env_file: - ./.bsky.env depends_on: - db - redis bsky-daemon: build: context: ./ dockerfile: services/bsky/Dockerfile command: node --enable-source-maps daemon.js env_file: - ./.bsky.env depends_on: - bsky - db - redis bsky-indexer: build: context: ./ dockerfile: services/bsky/Dockerfile command: node --enable-source-maps indexer.js env_file: - ./.bsky.env volumes: - ./data/bsky/cache/:/cache/ depends_on: - bsky - db - redis bsky-ingester: build: context: ./ dockerfile: services/bsky/Dockerfile command: node --enable-source-maps ingester.js env_file: - ./.bsky.env volumes: - ./data/bsky/cache/:/cache/ depends_on: - bsky - db - redis mod: ports: - 2585:3000 build: context: ./ dockerfile: services/ozone/Dockerfile env_file: - ./.mod.env depends_on: - db mod-daemon: build: context: ./ dockerfile: services/ozone/Dockerfile command: node --enable-source-maps daemon.js env_file: - ./.mod.env depends_on: - mod - db pds: ports: - 2583:3000 build: context: ./ dockerfile: services/pds/Dockerfile env_file: - ./.pds.env volumes: - ./data/pds/:/data/ depends_on: - db social-app: ports: - 8100:8100 build: context: ./repos/social-app/ dockerfile: Dockerfile env_file: - ./.web.env command: "/usr/bin/bskyweb serve" db: image: postgres:latest env_file: - ./.db.env volumes: - ./postgres/init/:/docker-entrypoint-initdb.d/ - ./data/db/:/var/lib/postgresql/data/ redis: image: redis volumes: - ./data/redis/:/data/ ``` ### hint 必要に応じて、以下の設定などを使用するとよいでしょう。 ```yaml:compose.yaml services: plc: depends_on: db: # 依存先のサービスが起動したら起動する condition: service_started ``` ```yaml:compose.yaml services: plc: depends_on: db: # 依存先のサービスが起動して、なおかつ、 healthcheck が通ったら起動する condition: service_healthy db: healthcheck: # https://docs.docker.jp/compose/compose-file/compose-file-v3.html test: ["CMD-SHELL", "pg_isready"] interval: 10s timeout: 5s retries: 5 ``` ```yaml:compose.yaml services: plc: # https://docs.docker.jp/v19.03/config/container/start-containers-automatically.html # コンテナが停止すると常に再起動します restart: always ``` ```yaml:compose.yaml services: db: # https://hub.docker.com/_/postgres # postgresのversionを固定 image: postgres:16 ``` ```yaml:compose.yaml services: db: # localhostからのアクセスを可能にする # postgresql://postgres:postgres@127.0.0.1:5432 ports: - 5432:5432 ``` ```yaml:compose.yaml services: db: # postgresql://user:password@127.0.0.1/test environment: POSTGRES_USER: user POSTGRES_DB: test POSTGRES_PASSWORD: password ``` ### env dbのurlになります。全部別々の`.env`に書いてください。 ```sh:.*.env # pds PDS_DB_POSTGRES_URL=postgresql://postgres:postgres@db/pds # bsky(appview) DB_PRIMARY_POSTGRES_URL=postgres://postgres:postgres@db/bsky DB_REPLICA_POSTGRES_URLS=postgres://postgres:postgres@db/bsky # bgs DATABASE_URL=postgres://postgres:postgres@db/bgs CARSTORE_DATABASE_URL=postgres://postgres:postgres@db/carstore # mod OZONE_DB_POSTGRES_URL=postgres://postgres:postgres@db/mod # plc DATABASE_URL=postgres://postgres:postgres@db/plc # email PDS_EMAIL_SMTP_URL=smtps://$username:$password@smtp.gmail.com ``` 環境変数をまとめます。 |server|env|body| |---|---|---| |bsky|DB_PRIMARY_POSTGRES_URL|postgres://postgres:postgres@db/bsky| |bsky|DB_REPLICA_POSTGRES_URLS|postgres://postgres:postgres@db/bsky| |bsky|DB_REPLICA_TAGS_ANY|0| |bsky|PUBLIC_URL|`https://api.${host}`| |bsky|SERVER_DID|did:web:api.${host}| |bsky|DID_PLC_URL|`https://plc.${host}`| |bsky|BLOB_CACHE_LOC|/cache/| |bsky|SEARCH_ENDPOINT|`https://search.${host}`| |bsky|REDIS_HOST|redis| |bsky|INDEXER_PARTITION_IDS|0| |bsky|INGESTER_PARTITION_COUNT|1| |bsky|PUSH_NOTIFICATION_ENDPOINT|`https://push.bsky.${host}/api/push`| |bsky|REPO_PROVIDER|wss://${host}| |bsky|IMG_URI_ENDPOINT|`https://cdn.${host}/img`| |bsky|ODERATION_SERVICE_DID|did:web:mod.${host}| |bsky|MODERATION_PUSH_URL|`https://admin:${OZONE_ADMIN_PASSWORD}@mod.${host}`| |bsky|ADMIN_PASSWORD|xxx| |bsky|MODERATOR_PASSWORD|xxx| |bsky|TRIAGE_PASSWORD|xxx| |bsky|SERVICE_SIGNING_KEY|$ openssl ecparam --name secp256k1 --genkey ...| |bsky|IMG_URI_SALT|xxx| |bsky|IMG_URI_KEY|xxx| |server|env|body| |---|---|---| |bgs|DATABASE_URL|postgres://postgres:postgres@db/bgs| |bgs|CARSTORE_DATABASE_URL|postgres://postgres:postgres@db/carstore| |bgs|DATA_DIR|/data| |bgs|ATP_PLC_HOST|`https://plc.${host}`| |bgs|BGS_ADMIN_KEY|xxx| |server|env|body| |---|---|---| |mod|OZONE_PUBLIC_URL|`https://mod.${host}`| |mod|OZONE_SERVER_DID|did:web:mod.${host}| |mod|OZONE_APPVIEW_URL|`https://api.${host}`| |mod|OZONE_APPVIEW_DID|did:web:api.${host}| |mod|OZONE_PDS_URL|`https://${host}`| |mod|OZONE_PDS_DID|did:web:${host}| |mod|OZONE_DB_POSTGRES_URL|postgres://postgres:postgres@db/mod| |mod|OZONE_DID_PLC_URL|`https://plc.${host}`| |mod|MODERATION_PUSH_URL|`https://admin:${OZONE_ADMIN_PASSWORD}@mod.${host}`| |mod|OZONE_ADMIN_PASSWORD|xxx| |mod|OZONE_MODERATOR_PASSWORD|xxx| |mod|OZONE_TRIAGE_PASSWORD|xxx| |mod|OZONE_SIGNING_KEY_HEX|xxx| |server|env|body| |---|---|---| |pds|PDS_HOSTNAME|${host}| |pds|PDS_DATA_DIRECTORY|/data| |pds|PDS_DB_POSTGRES_URL|postgresql://postgres:postgres@db/pds| |pds|PDS_DID_PLC_URL|`https://plc.${host}`| |pds|PDS_BSKY_APP_VIEW_URL|`https://api.${host}`| |pds|PDS_BSKY_APP_VIEW_DID|did:web:api.${host}| |pds|PDS_MOD_SERVICE_URL|`https://mod.${host}`| |pds|PDS_MOD_SERVICE_DID|did:web:mod.${host}| |pds|PDS_CRAWLERS|`https://bgs.${host}`| |pds|PDS_EMAIL_SMTP_URL|smtps://$username:$password@smtp.gmail.com| |pds|PDS_EMAIL_FROM_ADDRESS|no-reply@${host}| |pds|PDS_INVITE_REQUIRED (招待コード)|false| |pds|PDS_INVITE_INTERVAL|604800000| |pds|PDS_BLOBSTORE_DISK_LOCATION|/data/img/static| |pds|PDS_BLOBSTORE_DISK_TMP_LOCATION|/data/img/tmp| |pds|PDS_JWT_SECRET|`$ openssl rand --hex 16`| |pds|PDS_ADMIN_PASSWORD|xxx| |pds|PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX|xxx| |pds|PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX|xxx| |server|env|body| |---|---|---| |plc|DATABASE_URL|postgres://postgres:postgres@db/plc| |plc|DB_CREDS_JSON|'{"username":"postgres","password":"postgres","host":"db","port":"5432","database":"plc"}'| |plc|ENABLE_MIGRATIONS|true| |plc|DB_MIGRATE_CREDS_JSON|'{"username":"postgres","password":"postgres","host":"db","port":"5432","database":"plc"}'| `xxx`は以下のコマンドなどで作成してもいいと思います。 ```sh $ openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 ``` ### SERVER_DID / SERVICE_SIGNING_KEY > `SERVICE_SIGNING_KEY`に入れる値は`SERVER_DID=did:web:xxx`の場合、ローカル環境で生成したもので構いません。しかし、`SERVER_DID=did:plc:xxx`を使用する場合はplcからkeyを登録します。 ```sh $ openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 SERVICE_SIGNING_KEY=xxx ``` 以下は現時点では必要ありません。 bsky(appview)の`SERVER_DID`は`did:web:xxx`という形式と`did:plc:xxx`という形式を使えます。後者はplcに登録して使うものと思われ、連合が開始する際に重要になるかもしれません。 `SERVICE_SIGNING_KEY`は`SERVER_DID`を取得したときのsign-keyだと思われます。 これはatprotoの`dev-env`でexampleが書かれています。 ```ts:atproto/packages/dev-env/src/bsky.ts static async create(cfg: BskyConfig): Promise { // packages/crypto/tests/keypairs.test.ts const serviceKeypair = await Secp256k1Keypair.create({ exportable: true }) console.log(`ROTATION_KEY=${serviceKeypair.did()}`) const exported = await serviceKeypair.export() const plcClient = new PlcClient(cfg.plcUrl) const port = cfg.port || (await getPort()) const url = `http://localhost:${port}` const serverDid = await plcClient.createDid({ signingKey: serviceKeypair.did(), rotationKeys: [serviceKeypair.did()], handle: 'bsky.test', pds: `http://localhost:${port}`, signer: serviceKeypair, }) console.log(`SERVER_DID=${serverDid}`) const server = bsky.BskyAppView.create({ db, redis: redisCache, config, algos: cfg.algos, imgInvalidator: cfg.imgInvalidator, signingKey: serviceKeypair, }) ``` ```sh $ make deps $ make build $ make test $ make run-dev-env ``` appviewをcreateする際の`signingKey: serviceKeypair`の部分を見てください。objを使用しています。 つまり、signingKeyに`obj`を入れると動きますが、`services/bsky/api.ts`では以下のような処理がなされます。 ```ts:atproto/services/bsky/api.js const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey) ``` didを作成したときに`Secp256k1Keypair`でimportできる値を`SERVICE_SIGNING_KEY`に入れてください。 あるいはコードを書き換えてobjをいれるのでもいけますが、現実的ではありません。 ```ts:atproto/services/bsky/api.js // const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey) const signingKey = process.env.SERVICE_SIGNING_OBJ ``` ```ts:atproto/packages/crypto/tests/keypairs.test.ts keypair = await Secp256k1Keypair.create({ exportable: true }) const exported = await keypair.export() imported = await Secp256k1Keypair.import(exported, { exportable: true }) expect(keypair.did()).toBe(imported.did()) ``` plcへの登録は以下のコマンドだと思われます。 ```sh # https://web.plc.directory/api/redoc#operation/ResolveDid $ url=https://plc.$host/did:plc:pyc2ihzpelxtg4cdkfzbhcv4 $ json='{ "type": "create", "signingKey": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", "recoveryKey": "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", "handle": "first-post.bsky.social", "service": "https://bsky.social", "prev": null, "sig": "yvN4nQYWTZTDl9nKSSyC5EC3nsF5g4S56OmRg9G6_-pM6FCItV2U2u14riiMGyHiCD86l6O-1xC5MPwf8vVsRw" }' $ curl -X POST -H "Content-Type: application/json" -d "$json" $url | jq . ``` ### invite-code ```sh $ host=example.com $ admin_password="admin-pass" $ url=https://$host/xrpc/com.atproto.server.createInviteCode $ json="{\"useCount\":1}" $ curl -X POST -u admin:${admin_password} -H "Content-Type: application/json" -d "$json" -sL $url | jq . ``` ### social-app svgを作りました。 ```ts:social-app/src/view/icons/Logotype.tsx import React from 'react' import Svg, {Path, SvgProps, PathProps} from 'react-native-svg' import {usePalette} from '#/lib/hooks/usePalette' const ratio = 17 / 64 export function Logotype({ fill, ...rest }: {fill?: PathProps['fill']} & SvgProps) { const pal = usePalette('default') // @ts-ignore it's fiiiiine const size = parseInt(rest.width || 32) return ( ) } ``` その他、書き換えを行うscriptです。頻繁にupdateすると思うので、mergeはきつい。 ```sh host=syu.is name=${host%%.*} domain=${host##*.} cd $d/repos/social-app/src if [ -n "`grep -R bsky.social .`" ];then for f (`grep -R bsky.social . |cut -d : -f 1`) sed -i -e "s/bsky\.social/${name}\.${domain}/g" $f fi if [ -n "`grep -R "isSandbox: false" .`" ];then for f (`grep -R "isSandbox: false" . |cut -d : -f 1`) sed -i -e "s/isSandbox: false/isSandbox: true/g" $f fi if [ -n "`grep -R SANDBOX .`" ];then for f (`grep -R SANDBOX . |cut -d : -f 1`) sed -i -e "s/SANDBOX/${name}\.${domain}/g" $f fi f=./view/com/modals/ServerInput.tsx if [ -n "`grep -R Bluesky.Social $f`" ] && [ -f $f ];then sed -i -e "s/Bluesky\.Social/${name}\.${domain}/g" $f fi f=./state/queries/preferences/moderation.ts if [ -n "`grep -R 'Bluesky Social' $f`" ] && [ -f $f ];then sed -i -e "s/Bluesky Social/${name}\.${domain}/g" $f fi f=./view/com/auth/create/Step1.tsx if [ -n "`grep -R 'Bluesky' $f`" ] && [ -f $f ];then sed -i -e "s/Bluesky/${name}\.${domain}/g" $f fi f=./lib/strings/url-helpers.ts if [ -n "`grep -R 'Bluesky Social' $f`" ] && [ -f $f ];then sed -i -e "s/Bluesky Social/${name}\.${domain}/g" $f fi f=./view/icons/Logotype.tsx o=$d/icons/Logotype.tsx if [ -n "`grep -R 'M8.478 6.252c1.503.538 2.3 1.7' $f`" ] && [ -f $f ] && [ -f $o ];then cp -rf $o $f fi ```