--- permalink: /posts/writeups/1 title: Hosting Bluesky PDS on my Server Using Docker categories: - Write Up tags: - write up - bluesky - docker published_date: 2025-01-27 10:37:07.201403915 +0000 layout: default.liquid is_draft: false --- ## Self-hosting Bluesky PDS Using Docker I'm not usually a big user of social networks, but with my new blog, I wanted a way to share updates on social media. I went with Bluesky for its decentralized nature, which aligns with my interest in the growing trend of decentralized social platforms. In this write-up, I will document the steps I took to install [Bluesky’s PDS (Personal Data Server)](https://github.com/bluesky-social/pds) on my setup, an Ubuntu server running Docker. Thanks to this [write-up](https://mattdyson.org/blog/2024/11/self-hosting-bluesky-pds), which was a huge help to do mine. While most of it was very useful, some parts differed since my Docker setup is different. Also, I encountered difficulties when trying to change my handle to the root domain, so I used a different method to achieve this. As a result, I hope this guide will provide additional value and be helpful to others. ### Installing the PDS Using Docker The starting point is the official bluesky [compose.yaml](https://github.com/bluesky-social/pds/blob/main/compose.yaml). However, my environment already include other services, including an [NGINX proxy with ACME Companion](https://github.com/nginx-proxy/acme-companion) to automatically generate and renew TLS certificates for my http services (see [Basic usage](https://github.com/nginx-proxy/acme-companion/wiki/Basic-usage)). Therefore, I modify this ```compose.yaml``` to reuse my proxy setup to provide a reverse proxy for the PDS container, eliminating the need for the reverse proxy (Caddy) provided in the initial file. I also remove Watchtower, as I use my own scripts to handle image updates. Finally, my ```compose.yaml``` is pretty simple file looks like this: ```yaml version: '3.9' services: pds: container_name: pds image: ghcr.io/bluesky-social/pds:0.4 network_mode: host restart: unless-stopped volumes: - type: bind source: /pds # Change this for target: /pds env_file: - pds.env ``` Most of the setup is to adapt with environment variables in the environment file ```pds.env```: ```ini # PDS_HOSTNAME: The domain name of your PDS service (e.g., domain.com) PDS_HOSTNAME= # PDS_SERVICE_HANDLE_DOMAINS: A suffix for the domain to be used with the service (e.g., .domain.com) PDS_SERVICE_HANDLE_DOMAINS=. # PDS_JWT_SECRET: A secret key for signing JWT tokens, needed for secure authentication PDS_JWT_SECRET= # PDS_ADMIN_PASSWORD: The admin password for accessing the PDS service (use a strong password generated by a password manager) PDS_ADMIN_PASSWORD= # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: A private key for cryptographic operations in hex format PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX= # PDS_DATA_DIRECTORY: Directory where data will be stored (e.g., /pds) PDS_DATA_DIRECTORY=/pds # PDS_BLOBSTORE_DISK_LOCATION: Location for storing blob data (e.g., /pds/blocks) PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks # PDS_BLOB_UPLOAD_LIMIT: The maximum upload size for blobs in bytes (e.g., 50 MB = 52428800 bytes) PDS_BLOB_UPLOAD_LIMIT=52428800 # PDS_EMAIL_SMTP_URL: The URL for the SMTP server used to send emails (e.g., for email verification) PDS_EMAIL_SMTP_URL="smtp://:@" # PDS_EMAIL_FROM_ADDRESS: The email address used to send emails (e.g., noreply@domain.com) PDS_EMAIL_FROM_ADDRESS= # LOG_ENABLED: Set to 'true' to enable logging LOG_ENABLED=true # PDS_DID_PLC_URL: URL for the DID (Decentralized Identifier) PLC service PDS_DID_PLC_URL=https://plc.directory # PDS_BSKY_APP_VIEW_URL: URL for the BlueSky app view service PDS_BSKY_APP_VIEW_URL=https://api.bsky.app # PDS_BSKY_APP_VIEW_DID: DID for the BlueSky app view service PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app # PDS_REPORT_SERVICE_URL: URL for the report service PDS_REPORT_SERVICE_URL=https://mod.bsky.app # PDS_REPORT_SERVICE_DID: DID for the report service PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac # PDS_CRAWLERS: URL for the crawlers to scrape data from (e.g., bsky.network) PDS_CRAWLERS=https://bsky.network # VIRTUAL_HOST: The virtual host for your PDS service (e.g., pds.domain.com) VIRTUAL_HOST= # VIRTUAL_PORT: The port number for the virtual host (e.g., 3000) VIRTUAL_PORT=3000 # LETSENCRYPT_HOST: The host for generating a Let's Encrypt SSL certificate (e.g., pds.domain.com) LETSENCRYPT_HOST= # LETSENCRYPT_EMAIL: The email used for Let's Encrypt certificate registration (e.g., admin@domain.com) LETSENCRYPT_EMAIL= ``` Most variable comments are self-explanatory, but here are some additional hints for a few of them. To generate the ```PDS_JWT_SECRET``` , you can use the following command: ```sh $ openssl rand --hex 16 ``` For ```PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX```, you can use ```sh $ openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 ``` Since I run my own mail server on my domain, I use my SMTP server by specifying the information in the format ```smtp://:@```. If you don't have your own mail server, as mentioned in the official documentation, you can use services like [resend](https://resend.com/) as an alternative. As I mention before, as I have already have the [NGINX proxy with ACME Companion set up](https://github.com/nginx-proxy/acme-companion), I only need to add the following lines to make the PDS container's port 3000 accessible on `````` over HTTPS (e.g., https://pds.domain.com) through the reverse proxy: ```ini VIRTUAL_HOST= VIRTUAL_PORT=3000 LETSENCRYPT_HOST= LETSENCRYPT_EMAIL= ``` The container is now ready to run. To start everything, use the following command to verify is everything looks find: ```sh $ docker-compose up ``` #### Ensuring Everything the PDS Running as Expected As directed in the documentation [https://atproto.com/guides/self-hosting](https://atproto.com/guides/self-hosting), to check that everything is online and working, you can visit `https://>/xrpc/_health` in your browser. You should see a JSON response with the version, like: ```json {"version":"0.2.2-beta.2"} ``` Additionally, you can use an online WebSocket tester (e.g. [piehost.com](https://piehost.com/websocket-tester)) and entered the following URL: `wss:///xrpc/com.atproto.sync.subscribeRepos?cursor=0` If everything is configured correctly, the test should indicate that the connection has been successfully established. ### Get your Root Domain (e.g domain.com) as your Bluesky Handle You cannot directly create a handle for the root domain (e.g., domain.com). Therefore, we need to create an account with a different handle (e.g., handle.domain.com) and then verify the root domain handle ownership using [Decentralised Identifier (DID)](https://atproto.com/specs/did) . We can verify using DNS or HTTP server verification. It seems you can use either, but I did both to be sure and can't confirm if only one is enough. #### Create account Since the administrative tools are included in the Docker image, they do not need to be installed separately, as follows: ```sh $ git clone https://github.com/bluesky-social/pds/ bluesky-pds $ cd bluesky-pds/pds-admin ``` Then, create an account using a temporary handle (e.g. myhandle.) ```sh $ PDS_ENV_FILE=/path/to/pds.env ./account.sh create myhandle. Account created successfully! ----------------------------- Handle : myhandle. DID : Password : ----------------------------- ``` Make sure to record the handle, DID, and password for future use. I should be able, at this step, to connect from the Bluesky client (e.g., [https://bsky.app/](https://bsky.app/)) using your custom domain and verify your email address. Go to [https://bsky.app/](https://bsky.app/) > **Sign In** >In **Hosting Provider**, select **Custom** > enter your `` (e.g., pds.domain.com) and then use your credentials (email/password) to log in. Then, you should be connected and able to verify your email address in the account settings by receiving a code, if your SMTP setup is correct. Now, to validate our root domain, we first need to set up a Domain Ownership Verification method to confirm that we own the domain for the handle we want (e.g., domain.com). #### Domain Ownership Verification using DNS To do the verification of the ownership of your domain using DNS add a DNS TXT record ```_atproto.``` with ```did=```. #### Domain Ownership Verification using HTTP To do the verification of the ownership of your domain using HTTP you need make accecible a file on http server at ```/.well-known/atproto-did``` (e.g. https://domain.com/.well-known/atproto-did) that contain your ``````. As I already have an Nginx server running to serve your homepage, I simply added a file at the path ```/.well-known/atproto-did``` in the root directory of my site, containing my `````` value running the following : ```sh $ mkdir /.well-known $ echo > /.well-known/atproto-did ``` For reference, here’s a sample Docker configuration for serving my homepage: ```yaml version: '2' services: web: image: nginx container_name: nginx restart: always expose: - 80 volumes: - :/usr/share/nginx/html:ro environment: - VIRTUAL_HOST= - LETSENCRYPT_HOST= - LETSENCRYPT_EMAIL= ``` #### Validation To verify if the DNS or HTTP Domain Ownership Verification is set up correctly, you can check on [https://bsky-debug.app/handle](https://bsky-debug.app/handle) by entering your handle. #### Update Handle We can now update your handle to your root domain ! However, I encountered an issue when trying to verify my handle directly in the Bluesky app ([https://bsky.app/](https://bsky.app/)). After logging into my account, I was unable to update my handle to the root domain due to errors. In **Settings > Account > Change Handle > I have my own domain**, selecting **Verify DNS Record** or **Verify Text File** would return an error. However, I was able to use the [Go AT protocol CLI tool (goat)](https://github.com/bluesky-social/indigo/tree/main/cmd/goat) to successfully update my handle as follow. Installing ```goat``` (require the Go toolchain): ```sh $ go install github.com/bluesky-social/indigo/cmd/goat@latest ``` Then, update your handle using the following commands: ```sh $ goat account login -u -p --pds-host $ goat account update-handle ``` Once connected to your Bluesky account, your handle should now display as your root domain. ### Finally Once the handle update is complete, you can restart the server in detached mode with the command below. ```sh $ docker-compose up -d ``` ### References * [Bluesky PDS](https://github.com/bluesky-social/pds/) * [Atproto Self Hosting Documentation](https://atproto.com/guides/self-hosting) * [Matt Dysion Write Up](https://mattdyson.org/blog/2024/11/self-hosting-bluesky-pds) * [https://bsky-debug.app/handle](https://bsky-debug.app/handle) * [Go AT protocol CLI tool (goat)](https://github.com/bluesky-social/indigo/tree/main/cmd/goat)