Open11

Bluesky PDSを自宅サーバーで建てようとする過程

takiponetakipone

Blueskyは、つぶやきを自前のサーバーに保持する PDS(Personal Data Server) を公開 している。PDSのセットアップに関する情報は以下の記事が詳しい。

https://zenn.dev/anon/articles/3c49cecdb3ecea

2024年2月にPDSをBlueskyのソーシャルネットワークに参加させるフェデレーション機能が公開された。

https://bsky.social/about/blog/02-22-2024-open-social-web

https://news.mynavi.jp/techplus/article/20240223-2890405/

マイナビの記事では以前流行ったMastodonとの比較が詳しく、ネットワークの健全性や運用に配慮した設計になっている点において安心感がある。

takiponetakipone

PDSのREADMEを読んでみよう。

https://github.com/bluesky-social/pds

リポジトリのAboutに Bluesky PDS (Personal Data Server) container image, compose file, and documentation とあるように、DockerイメージとDocker Composeが置いてあり、コードはTypescriptの @atproto/pds にある。

インストールスクリプト で手軽に試せるようになっている一方で、スクリプト自体はDocker周辺のセットアップを行っているだけなので、専用のLinux VMを用意しなくてもDockerの実行環境があればいけそうな雰囲気を感じとった。ちなみにx86_64だけではなくarm64もサポートしているのがポイント高い。

takiponetakipone

手元のMacbookにLimaが入っていたので、サポートするUbuntu 22.04のVMを立ち上げてセットアップしてみよう。

https://github.com/lima-vm/lima/blob/master/examples/ubuntu-lts.yaml

$ limactl start ubuntu-lts.yaml
? Creating an instance "ubuntu-lts"  [Use arrows to move, type to filter]
> Proceed with the current configuration
  Open an editor to review or modify the current configuration
  Choose another template (docker, podman, archlinux, fedora, ...)
  Exit
[Enter押下]
INFO[0092] Starting the instance "ubuntu-lts" with VM driver "qemu"
  :
$ limactl shell ubuntu-lts
$ cd
$ wget https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh
$ sudo bash ./installer.sh
* Detected supported distribution Ubuntu 22.04 LTS
curl: (7) Failed to connect to 169.254.169.254 port 80 after 1 ms: Network is unreachable
---------------------------------------
     Add DNS Record for Public IP
---------------------------------------

  From your DNS provider's control panel, create the required
  DNS record with the value of your server's public IP address.

  + Any DNS name that can be resolved on the public internet will work.
  + Replace example.com below with any valid domain name you control.
  + A TTL of 600 seconds (10 minutes) is recommended.

  Example DNS record:

    NAME                TYPE   VALUE
    ----                ----   -----
    example.com         A      Server's IP
    *.example.com       A      Server's IP

  **IMPORTANT**
  It's recommended to wait 3-5 minutes after creating a new DNS record
  before attempting to use it. This will allow time for the DNS record
  to be fully updated.

Enter your public DNS address (e.g. example.com): pds.example.com
Enter an admin email address (e.g. you@example.com): bluesky@example.com
  : (中略)
========================================================================
PDS installation successful!
------------------------------------------------------------------------

Check service status      : sudo systemctl status pds
Watch service logs        : sudo docker logs -f pds
Backup service data       : /pds
PDS Admin command         : pdsadmin

Required Firewall Ports
------------------------------------------------------------------------
Service                Direction  Port   Protocol  Source
-------                ---------  ----   --------  ----------------------
HTTP TLS verification  Inbound    80     TCP       Any
HTTP Control Panel     Inbound    443    TCP       Any

Required DNS entries
------------------------------------------------------------------------
Name                         Type       Value
-------                      ---------  ---------------
pds.takipone.com              A          Server's IP
*.pds.takipone.com            A          Server's IP

Detected public IP of this server: Server's IP

To see pdsadmin commands, run "pdsadmin help"

========================================================================
Create a PDS user account? (y/N):
[アカウント登録は今回はやらないのでEnter]
$

これでコンテナが立ち上がる

$ sudo docker ps
CONTAINER ID   IMAGE                            COMMAND                  CREATED         STATUS                   PORTS     NAMES
60b03f3e270f   caddy:2                          "caddy run --config …"   2 minutes ago   Up 2 minutes                       caddy
1601e08e12d8   containrrr/watchtower:latest     "/watchtower"            2 minutes ago   Up 2 minutes (healthy)             watchtower
f2e75f8e83dc   ghcr.io/bluesky-social/pds:0.4   "dumb-init -- node -…"   2 minutes ago   Up 2 minutes                       pds
takiponetakipone

関連ファイルは /pds に配置され、Docker周りの構成は /pds/compose.yaml にある。 /pds はrootにのみアクセス権があるため、 探索したいときはrootユーザーに susudo -i でスイッチするのが楽。

$ sudo -i
# cd /pds
# ls
account.sqlite      caddy             did_cache.sqlite-shm  sequencer.sqlite
account.sqlite-shm  compose.yaml      did_cache.sqlite-wal  sequencer.sqlite-shm
account.sqlite-wal  did_cache.sqlite  pds.env               sequencer.sqlite-wal
# apt install tree
# tree caddy/
caddy/
├── data
│   └── caddy
│       ├── last_clean.json
│       └── locks
└── etc
    └── caddy
        └── Caddyfile

install.sh と置いてあるファイルでだいたい構成が見えてくる。

/pds/compose.yaml
version: '3.9'
services:
  caddy:
    container_name: caddy
    image: caddy:2
    network_mode: host
    depends_on:
      - pds
    restart: unless-stopped
    volumes:
      - type: bind
        source: /pds/caddy/data
        target: /data
      - type: bind
        source: /pds/caddy/etc/caddy
        target: /etc/caddy
  pds:
    container_name: pds
    image: ghcr.io/bluesky-social/pds:0.4
    network_mode: host
    restart: unless-stopped
    volumes:
      - type: bind
        source: /pds
        target: /pds
    env_file:
      - /pds/pds.env
  watchtower:
    container_name: watchtower
    image: containrrr/watchtower:latest
    network_mode: host
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
    restart: unless-stopped
    environment:
      WATCHTOWER_CLEANUP: true
      WATCHTOWER_SCHEDULE: "@midnight"
#
/pds/caddy/etc/caddy/Caddyfile
{
	email bluesky@example.com
	on_demand_tls {
		ask http://localhost:3000/tls-check
	}
}

*.pds.example.com, pds.example.com {
	tls {
		on_demand
	}
	reverse_proxy http://localhost:3000
}

インストール時に入力したadmin mail addressはCaddyのOn-Demand TLSで利用される模様。デフォルトではLet's EncryptとZeroSSLのアカウントとして利用するのかな。

takiponetakipone
pds.env
PDS_HOSTNAME=pds.example.com
PDS_JWT_SECRET=b046edf971adb584cc1b8c3919f81667
PDS_ADMIN_PASSWORD=3c1ad73f77fc6d2c08b011acfae2f334
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=aca1094cce7a6a627bf2650c69ee792ca85ebfbabbb65e2aa5b538db1ada98cb
PDS_DATA_DIRECTORY=/pds
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
PDS_DID_PLC_URL=https://plc.directory
PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
PDS_REPORT_SERVICE_URL=https://mod.bsky.app
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
PDS_CRAWLERS=https://bsky.network
LOG_ENABLED=true

シークレットやパスワードのアルゴリズムは install.sh にある。

install.sh
     14 # Secure generator comands
     15 GENERATE_SECURE_SECRET_CMD="openssl rand --hex 16"
     16 GENERATE_K256_PRIVATE_KEY_CMD="openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32"
      : 
    328   PDS_ADMIN_PASSWORD=$(eval "${GENERATE_SECURE_SECRET_CMD}")
      : 
    331 PDS_JWT_SECRET=$(eval "${GENERATE_SECURE_SECRET_CMD}")
    332 PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
    333 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}")
takiponetakipone

以下のBrume 2の既存構成にpdsコンテナを追加することにした。

https://zenn.dev/takipone/articles/e514facc532263

compose.yaml抜粋
  pds:
    image: ghcr.io/bluesky-social/pds:0.4
    network_mode: host
    restart: unless-stopped
    volumes:
      - type: bind
        source: /opt/example-com/pds
        target: /pds
    env_file:
      - /opt/example-com/pds/pds.env

Nginxはブログ記事を参考にしつつ、以下を設定

  • 既存のGhostへのリバースプロキシと共存させるため /xrpc//.well-known/ のみPDSの http://localhost:3000/ に向ける。
  • /xrpc/ はWebsocket向けにパラメータを調整する

https://benharri.org/bluesky-pds-without-docker/

nginx.conf抜粋
http {
    :
    map $http_upgrade $connection_upgrade {
      default upgrade;
      ''      close;
    }
    server {
        :
        location /xrpc/ {
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection $connection_upgrade;
          proxy_pass http://localhost:3000;
          proxy_send_timeout      3600;
          proxy_read_timeout      3600;
        }
        location /.well-known/ {
          proxy_pass http://localhost:3000;
        }
    }
}

Brume 2(OpenWRT)はutil-linuxのコマンドが足りず、bashやjqなどを追加でインストールしたけど column コマンドが見つからず pdsadmin コマンドがエラーになる。一方で本コマンドは同一ホストで実行する必要はなく、PDSの認証情報があれば任意のホストから実行できることに気づいたので、Ubuntu 22.04マシンのpds.envをBrume 2のものに差し替えて代替できた。

takiponetakipone

CloudFront、ACMはPDSのドメインとサブドメインのワイルドカードをCloudFrontディストリビューションのCNAMESとACM証明書のSANsにセットした。

CloudFrontは既定でWebsocketをプロキシできるので、Websocketに関する特段の対応はなし。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.websockets.html

ビヘイビアはこんな感じで様子を見ている。

Tailscale FunnelのHTTPS終端のためにオリジンリクエストポリシーでHostヘッダを除外しているが、今のところPDSでホスト名を見るような処理はしていないようなので大丈夫そう。

takiponetakipone

一通り準備ができたらPDS AdminのDiscordの requests チャンネルにPDSのドメインを登録するとBlueskyのボットがクロールしに来てネットワークに参加できる。

あとはPDSのREADMEの通り、pdsadminで招待コードを発行してアカウント発行を進めればOK。ホスティングプロバイダーにPDSのドメインを指定する。

登録直後はプロフィールの挙動が不安定なので、Blueskyでハンドルを独自ドメインに設定するのと同様、[ハンドルを変更]からハンドル名を入力して[No DNS Panel]を選択、[Verify Text File]ボタンをクリックして検証が成功すれば安定した。

今回試した限りでは、表示される /.well-known/400 User not found というレスポンスで動いていないっぽい。ログを見る限りではresolveHandle APIにフォールバックして、そちらで検証が成功しているようだった。フォールバックについては、AT Protocolで詳しい仕様が定義されている。

https://atproto.com/specs/handle#handle-resolution

takiponetakipone

最初に作成したアカウントが、アカウント作成時のなんらか初期設定に失敗したようで、getProfile APIが 400 User not found になってしまったため、別のアカウントを作り直して回避できた。

一方で、一度作ったアカウントはPLC DID Directoryに登録されると pdsadmin account delete でPDS上で削除しても残ってしまう模様。 takedown で無効化するのが正しい手順だったのかもしれない。誰か消し方教えてください。↓にヒントがありそうな気はしている。

https://web.plc.directory/spec/v0.1/did-plc