{"id":12,"date":"2025-01-15T00:38:37","date_gmt":"2025-01-15T00:38:37","guid":{"rendered":"https:\/\/wp.lamp.wtf\/?p=12"},"modified":"2025-01-15T00:38:37","modified_gmt":"2025-01-15T00:38:37","slug":"how-to-manually-install-bluesky-pds-without-docker","status":"publish","type":"post","link":"https:\/\/wp.lamp.wtf\/?p=12","title":{"rendered":"How to manually install Bluesky PDS without Docker"},"content":{"rendered":"\n<p>The Bluesky PDS can be installed on most systems with <a href=\"https:\/\/nodejs.org\/\">Node.js<\/a> 18 or newer. This guide will focus on installation on a typical Linux system.<\/p>\n\n\n\n<p>Install node.js with your distro&#8217;s package manager. If your distro&#8217;s version is too old, you can install the latest LTS version from the <a href=\"https:\/\/github.com\/nodesource\/distributions\">Nodesource<\/a> repository.<\/p>\n\n\n\n<p>After installing Node.js, you also need to install pnpm:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;sudo] corepack enable pnpm<\/code><\/pre>\n\n\n\n<p>Create a user and home directory for the PDS server:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;sudo] useradd -r -m -s \/bin\/bash pds<\/code><\/pre>\n\n\n\n<p>A folder will be created at \/home\/pds where everything can be stored.<\/p>\n\n\n\n<p>Log in to the PDS user:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;sudo] su -l pds<\/code><\/pre>\n\n\n\n<p>Clone the PDS repository:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git clone https:\/\/github.com\/bluesky-social\/pds repo<\/code><\/pre>\n\n\n\n<p>This command will clone the repo to a folder named &#8220;repo&#8221; to avoid confusion.<\/p>\n\n\n\n<p>Enter the repository and install the node modules:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd repo\/service\npnpm install --production --frozen-lockfile<\/code><\/pre>\n\n\n\n<p>Go back home and create the configuration file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd; nano pds.env<\/code><\/pre>\n\n\n\n<p>Paste the following contents:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PDS_HOSTNAME=\nPDS_JWT_SECRET=\nPDS_ADMIN_PASSWORD=\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=\nPDS_DATA_DIRECTORY=\/home\/pds\/data\nPDS_BLOBSTORE_DISK_LOCATION=\/home\/pds\/data\/blocks\nPDS_BLOB_UPLOAD_LIMIT=52428800\nPDS_DID_PLC_URL=https:\/\/plc.directory\nPDS_BSKY_APP_VIEW_URL=https:\/\/api.bsky.app\nPDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app\nPDS_REPORT_SERVICE_URL=https:\/\/mod.bsky.app\nPDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac\nPDS_CRAWLERS=https:\/\/bsky.network\nLOG_ENABLED=true\nPDS_PORT=3000\nNODE_ENV=production<\/code><\/pre>\n\n\n\n<p>Create the missing directories:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p \/home\/pds\/data\/blocks<\/code><\/pre>\n\n\n\n<p>You can use this command to generate a value for <code>PDS_JWT_SECRET<\/code> and <code>PDS_ADMIN_PASSWORD<\/code>. (don&#8217;t use the same value for both)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl rand --hex 16<\/code><\/pre>\n\n\n\n<p>Run this command to generate a value for <code>PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32<\/code><\/pre>\n\n\n\n<p>Set the <code>PDS_HOSTNAME<\/code> to the domain name you will use for the PDS.<\/p>\n\n\n\n<p>By default, the user handles will be subdomains of this domain name, but you can change this by adding the <code>PDS_SERVICE_HANDLE_DOMAINS<\/code> variable.<\/p>\n\n\n\n<p>For example. if <code>PDS_HOSTNAME=example.com<\/code>, then users on the PDS will have handles like john.example.com.<\/p>\n\n\n\n<p>But maybe you have a website on example.com so you want to set <code>PDS_HOSTNAME=pds.example.com<\/code>. Then your users will have handles like john.pds.example.com, and this will not work with Cloudflare.<\/p>\n\n\n\n<p>In this case, set <code>PDS_SERVICE_HANDLE_DOMAINS=.example.com<\/code>. Note that the domain must start with a dot.<\/p>\n\n\n\n<p>The service handle domain can be completely different than the PDS domain.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Create a systemd service file at <code>\/etc\/systemd\/system\/pds.service<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=Bluesky Personal Data Server\nAfter=network.target\n\n&#91;Service]\nUser=pds\nType=exec\nEnvironmentFile=\/home\/pds\/pds.env\nWorkingDirectory=\/home\/pds\/repo\/service\nExecStart=node --enable-source-maps index.js\n\n&#91;Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p>Enable the service<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl daemon-reload\nsystemctl enable --now pds<\/code><\/pre>\n\n\n\n<p>Check if it&#8217;s working<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl status pds\ncurl localhost:3000\/xrpc\/_health<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>After the PDS itself is running, you need to connect it to the web. You need an HTTPS server listening on port 443 to proxy to http:\/\/localhost:3000 (or whatever you set the PDS port to). The proxy server needs to support WebSockets, and it needs to provide the X-Forwarded-For header. Caddy does it perfectly with a single line config, but Nginx and Apache can do it too with more complicated configuration.<\/p>\n\n\n\n<p>Caddy is also easier because it can automatically generate certificates for user handles, without using a wildcard certificate. The PDS is designed to work with Caddy&#8217;s on-demand TLS feature, so the caddy configuration looks like this.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n\ton_demand_tls {\n\t\task http:\/\/localhost:3000\/tls-check\n\t}\n}\n\n*.example.com, example.com {\n\ttls {\n\t\ton_demand\n\t}\n\treverse_proxy http:\/\/localhost:3000\n}<\/code><\/pre>\n\n\n\n<p>To use a different server, you have to use an ACME client like Certbot with a DNS plugin for your DNS provider (i.e. Cloudflare, Porkbun) to get a wildcard certificate.<\/p>\n\n\n\n<p>Todo: configuration examples for Apache, Nginx&#8230;<\/p>\n\n\n\n<p>Alternatively, you can use Cloudflare&#8217;s &#8220;cloudflared&#8221; daemon to connect the PDS directly to the Cloudflare network. This way you do not need to port forward, open firewall ports, or get TLS certificates. But you cannot use domain names with more than three levels, i.e john.pds.example.com.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>After the PDS is set up and available on the world wide web via an HTTPS reverse proxy server, it&#8217;s time to create the first account and request crawl from the Bluesky network.<\/p>\n\n\n\n<p>Use the pdsadmin.sh script from the PDS repo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/home\/pds\/repo\nexport PDS_ENV_FILE=\/home\/pds\/pds.env\n.\/pdsadmin.sh account create\n.\/pdsadmin.sh request-crawl<\/code><\/pre>\n\n\n\n<p>Tip: Add <code>export PDS_ENV_FILE=\/home\/pds\/pds.env<\/code> to <code>~\/.profile<\/code> so you don&#8217;t have to type it every time.<\/p>\n\n\n\n<p>Log in to your new PDS account on the Bluesky app and it should be working.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>To update, enter the repo folder and run <code>git pull<\/code>, then enter the source folder and run again:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pnpm install --production --frozen-lockfile<\/code><\/pre>\n\n\n\n<p>Then restart:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl restart pds<\/code><\/pre>\n\n\n\n<p>To view PDS logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>journalctl -fu pds<\/code><\/pre>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Bluesky PDS can be installed on most systems with Node.js 18 or newer. This guide will focus on installation on a typical Linux system. Install node.js with your distro&#8217;s package manager. If your distro&#8217;s version is too old, you can install the latest LTS version from the Nodesource repository. After installing Node.js, you also [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-12","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/posts\/12","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12"}],"version-history":[{"count":1,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/posts\/12\/revisions"}],"predecessor-version":[{"id":13,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=\/wp\/v2\/posts\/12\/revisions\/13"}],"wp:attachment":[{"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=12"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wp.lamp.wtf\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=12"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}