2024-01-08 / @syui

bluesky , pds

blueskyをself-hostする

少し前にblueskyをself-host(セルフホスト)しました。

server url src
pds https://syu.is src
plc https://plc.syu.is src
mod https://mod.syu.is src
bgs https://bgs.syu.is src
appview https://api.syu.is src
web https://web.syu.is src

以前は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はあまりおすすめしません。これはアイスランドが独自方針で管理している感じで、メール認証などが必要です。その他、様々な制限があります。

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
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で構築しました。大体は以下のような感じの構成です。

下記は最小構成なので、自分なりに読み替えてください。

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

必要に応じて、以下の設定などを使用するとよいでしょう。

services:
    plc:
        depends_on:
            db:
                # 依存先のサービスが起動したら起動する
                condition: service_started
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
services:
    plc:
        # https://docs.docker.jp/v19.03/config/container/start-containers-automatically.html
        # コンテナが停止すると常に再起動します 
        restart: always
services:
    db:
        # https://hub.docker.com/_/postgres
        # postgresのversionを固定
        image: postgres:16
services:
    db:
        # localhostからのアクセスを可能にする
        # postgresql://postgres:[email protected]:5432
        ports:
          - 5432:5432
services:
    db:
        # postgresql://user:[email protected]/test
        environment:
          POSTGRES_USER: user
          POSTGRES_DB: test
          POSTGRES_PASSWORD: password 

env

dbのurlになります。全部別々の.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:[email protected]

環境変数をまとめます。

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:[email protected]
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は以下のコマンドなどで作成してもいいと思います。

$ 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を登録します。

$ 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_DIDdid:web:xxxという形式とdid:plc:xxxという形式を使えます。後者はplcに登録して使うものと思われ、連合が開始する際に重要になるかもしれません。

SERVICE_SIGNING_KEYSERVER_DIDを取得したときのsign-keyだと思われます。

これはatprotoのdev-envでexampleが書かれています。

  static async create(cfg: BskyConfig): Promise<TestBsky> {
    // 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,
    })
$ make deps
$ make build
$ make test

$ make run-dev-env

appviewをcreateする際のsigningKey: serviceKeypairの部分を見てください。objを使用しています。

つまり、signingKeyにobjを入れると動きますが、services/bsky/api.tsでは以下のような処理がなされます。

 const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey)

didを作成したときにSecp256k1Keypairでimportできる値をSERVICE_SIGNING_KEYに入れてください。

あるいはコードを書き換えてobjをいれるのでもいけますが、現実的ではありません。

// const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey)
const signingKey = process.env.SERVICE_SIGNING_OBJ
      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への登録は以下のコマンドだと思われます。

# 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

$ 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を作りました。

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 (
    <Svg
      fill="none"
      viewBox="0 0 2821.6379 794.29016"
      {...rest}
      width={size}
      height={Number(size) * ratio}>
      <g
      transform="matrix(0.1,0,0,-0.1,-282.80153,1445)"
      fill="#000000"
      stroke="none"
      >
      <path
      d="m 24787,14443 c -4,-3 -7,-224 -7,-490 v -483 h 545 545 v 490 490 h -538 c -296,0 -542,-3 -545,-7 z"
      />
      <path
       d="m 5190,13285 c -8,-3 -96,-12 -195,-20 -199,-16 -296,-32 -430,-70 -49,-14 -115,-32 -145,-40 -153,-39 -504,-198 -662,-301 -21,-13 -57,-36 -80,-51 -24,-16 -72,-50 -109,-78 -241,-182 -377,-315 -528,-517 -119,-158 -120,-160 -106,-188 23,-45 140,-140 560,-457 110,-84 319,-242 465,-353 146,-110 369,-279 495,-375 791,-600 723,-549 1049,-799 269,-207 398,-307 524,-403 l 114,-86 -49,-49 c -128,-131 -378,-258 -588,-299 -66,-13 -357,-13 -420,0 -115,23 -172,39 -202,54 -18,10 -37,17 -43,17 -24,0 -171,81 -255,141 -50,35 -146,121 -215,192 -69,70 -133,127 -142,127 -19,0 -153,-63 -177,-83 -9,-7 -54,-35 -101,-62 -47,-26 -110,-64 -140,-85 -30,-20 -97,-61 -148,-91 -51,-30 -107,-62 -124,-72 -18,-10 -57,-38 -87,-61 -70,-53 -252,-168 -446,-281 -82,-49 -158,-95 -168,-104 -16,-16 -14,-21 34,-106 165,-289 544,-666 867,-860 9,-6 35,-22 58,-37 36,-23 267,-138 349,-173 108,-47 160,-67 240,-93 150,-48 201,-62 228,-62 14,0 64,-9 109,-19 197,-46 302,-56 573,-56 197,1 281,5 345,17 47,9 110,19 141,22 31,3 75,13 99,21 23,8 56,15 72,15 17,0 51,7 77,15 25,8 80,24 121,36 41,12 125,41 185,66 61,25 124,50 140,56 17,7 89,42 160,78 113,58 177,98 395,246 82,56 273,232 403,371 127,136 237,271 237,291 0,12 -208,179 -425,342 -186,140 -1121,843 -1720,1294 -264,199 -589,444 -723,546 -134,101 -274,208 -312,237 -39,29 -70,58 -70,66 0,21 107,115 203,179 95,64 237,133 295,143 20,4 49,12 63,20 96,48 519,48 619,-1 14,-7 41,-16 60,-20 65,-13 262,-118 360,-191 99,-74 250,-230 372,-384 34,-44 70,-80 80,-80 9,0 39,15 67,33 28,17 70,44 94,58 23,15 121,76 217,136 96,60 254,156 350,213 96,56 272,160 390,230 118,70 230,135 248,144 41,21 41,45 -3,116 -18,30 -46,75 -61,100 -64,106 -252,348 -352,454 -106,112 -169,171 -293,274 -197,164 -310,235 -594,376 -215,106 -519,205 -735,240 -137,22 -574,51 -610,41 z"
   />
    <path
       d="m 29095,12736 c -421,-44 -744,-157 -975,-342 -259,-207 -396,-446 -465,-809 -16,-84 -23,-301 -14,-425 36,-518 257,-859 694,-1070 166,-80 284,-116 716,-215 449,-103 514,-121 646,-186 218,-107 308,-253 306,-494 -2,-303 -188,-477 -588,-551 -140,-26 -640,-27 -825,-1 -275,38 -486,94 -776,203 -94,35 -174,64 -178,64 -3,0 -6,-175 -6,-389 v -388 l 88,-41 c 404,-187 905,-282 1486,-282 675,0 1154,150 1465,459 196,194 311,434 362,756 20,121 17,476 -5,600 -89,517 -358,800 -945,994 -130,43 -241,71 -616,156 -137,31 -299,73 -360,92 -331,106 -455,246 -455,511 1,249 127,412 387,501 272,92 801,76 1269,-39 116,-28 326,-96 432,-138 l 52,-22 -2,406 -3,406 -66,28 c -154,66 -413,140 -604,174 -279,49 -756,69 -1020,42 z"
      />
    <path
       d="m 9630,12676 c 0,-2 403,-996 896,-2208 493,-1211 898,-2211 901,-2220 6,-21 -135,-386 -193,-499 -104,-202 -256,-324 -471,-380 -118,-30 -340,-43 -516,-29 -84,6 -185,17 -225,24 -40,7 -75,10 -77,7 -3,-2 -5,-163 -5,-357 v -353 l 63,-30 c 194,-93 493,-138 801,-120 414,23 683,115 937,319 174,140 357,402 474,680 26,62 1945,5159 1945,5167 0,2 -232,2 -516,1 l -517,-3 -556,-1550 c -485,-1350 -560,-1550 -578,-1553 -18,-3 -26,11 -62,105 -23,59 -295,758 -605,1553 l -562,1445 -567,3 c -312,1 -567,0 -567,-2 z"
      />
    <path
       d="m 15690,10867 c 0,-1970 -1,-1936 56,-2153 128,-497 461,-787 1014,-885 147,-26 440,-36 605,-20 413,40 834,200 1181,447 35,25 73,44 88,44 14,0 26,-1 26,-3 0,-2 20,-94 45,-205 25,-111 45,-204 45,-207 0,-3 209,-5 465,-5 h 465 v 2400 2400 h -535 -535 l -2,-1866 -3,-1866 -115,-50 c -425,-185 -743,-252 -1088,-227 -302,21 -472,109 -562,293 -79,161 -74,11 -77,1969 l -3,1747 h -535 -535 z"
      />
    <path
       d="m 24785,12668 c -3,-7 -4,-1086 -3,-2398 l 3,-2385 538,-3 537,-2 v 2400 2400 h -535 c -419,0 -537,-3 -540,-12 z"
      />
    <path
       d="m 21660,8275 v -545 h 565 565 v 545 545 h -565 -565 z"
      />
  </g>
    </Svg>
  )
}

その他、書き換えを行うscriptです。頻繁にupdateすると思うので、mergeはきつい。

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