FOSS geek, privacy advocate, digital archivist, mental illness advocate, gamer

Setting up a Bluesky PDS with Podman on CentOS Stream 10

Published on: by hyperreal

6 min read

This is based on the README.md at https://github.com/bluesky-social/pds, as well as the README.md at https://github.com/bluesky-social/deploy-recipes/tree/main/podman. I’ll keep SELinux in enforcing mode and provide a policy module to compile and install to allow the PDS to work. CentOS Stream is not an officially supported distribution by the upstream PDS maintainers – this is my own working setup – so please do not bother them with support questions for a CentOS Stream host. In lieu of that, you’re welcome to direct any questions or issues with this setup to me, at @hyperreal@tilde.zone in the fediverse, or submit an issue at bluesky-social/deploy-recipes.

Minimum server requirements

Component Requirement
OS CentOS Stream 10
RAM 1 GB
CPU cores 1
Storage 20 GB SSD
Architectures amd64, arm64
Number of PDS users 1-20

Ensure you have a firewall installed along with fail2ban, and that proper security precautions are taken for your server, such as SSH hardening.

Other requirements:

  • Public IPv4 address
  • Public DNS name
  • Public inbound internet access permitted on port 80/tcp and 443/tcp
  • A reverse-proxy server, such as Caddy. This guide assumes you have Caddy installed.

Refer to https://github.com/bluesky-social/pds for more information on server setup, but adapt it to CentOS.

Preliminary actions

Install Podman and epel-release:

sudo dnf install -y '@container-management' epel-release

SELinux policy module

SELinux is not required, but recommended for RHEL-like distributions. To set SELinux in enforcing mode, run the following command as root:

setenforce 1

To make this persist across reboots, you may need to edit /etc/sysconfig/selinux. Change the SELinux variable to the value enforcing. You can do this with the following command, which is idempotent in the event it is already set to enforcing.

sudo sed -i 's/SELINUX=permissive/SELINUX=enforcing/' /etc/sysconfig/selinux

You also need to install an SELinux policy module so that SELinux doesn’t deny the PDS processes.

Create the file pds.te:

module pds 1.0;

require {
        type container_runtime_t;
        type var_run_t;
        type container_t;
        type default_t;
        class file { create lock map open read setattr unlink write };
        class dir { add_name remove_name write };
        class unix_stream_socket connectto;
        class sock_file write;
}

#============= container_t ==============
allow container_t container_runtime_t:unix_stream_socket connectto;
allow container_t default_t:dir { add_name remove_name write };
allow container_t default_t:file { create lock map open read setattr unlink write };
allow container_t var_run_t:sock_file write;

Compile and install the module.

checkmodule -M -m -o pds.mod pds.te
semodule_package -o pds.pp -m pds.mod
sudo semodule -i pds.pp

If you receive any errors, you can check if there are SELinux denials with the following command:

sudo ausearch -m avc -ts recent | sudo audit2allow

Installating the PDS Podman quadlet

The systemd quadlet files are taken from bluesky-social/deploy-recipes and slightly modified.

The directory under which you should place these files is /etc/containers/systemd.

Create the file /etc/containers/systemd/pds.container with the following contents:

[Unit]
Description=Bluesky Personal Data Server service
Before=caddy.service

[Container]
Label=app=pds
Image=ghcr.io/bluesky-social/pds:0.4
AutoUpdate=registry
Pod=pds.pod
EnvironmentFile=/etc/pds.env

[Install]
WantedBy=multi-user.target default.target

Create the file /etc/containers/systemd/pds.pod with the following contents:

[Pod]
Volume=pds.volume:/pds
PublishPort=127.0.0.1:3000:3000 
# if you map 3000:3000 instead of 127.0.0.1:3000:3000
# the PDS will be accessible without the reverse proxy. You probably don't want that!

Create the file /etc/containers/systemd/pds.volume with the following contents:

[Unit]
Description=Bluesky PDS Volume

[Volume]
Label=app=pds

This set of files comprise a systemd quadlet. Quadlets enable Podman containers to run as systemd services. It’s an alternative to using podman-compose that fits in with the systemd ecosystem.

  • pds.container is like a template for systemd to generate a corresponding .service file with the defined settings.
  • pds.volume is a template for systemd to generate a Podman volume with the defined settings.
  • pds.pod is like a meta file with additional configuration that applies to the other files for the Podman services.

It is necessary to have all of these files together, as they depend on each other.

Setting up pds.env

Here is the default pds.env. You should edit it to your specific needs.

PDS_HOSTNAME=pds.example.com
PDS_JWT_SECRET=
PDS_ADMIN_PASSWORD=
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=
PDS_DATA_DIRECTORY=/pds #mapped to volume
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
PDS_BLOB_UPLOAD_LIMIT=104857600

# if you want to use s3 or compatible, use these variables and comment DISK_LOCATION
# Object Storage
PDS_BLOBSTORE_S3_BUCKET=your-bucket-name
PDS_BLOBSTORE_S3_ENDPOINT=https://s3.example.com
#PDS_BLOBSTORE_S3_FORCE_PATH_STYLE=true #depends on your provider
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=your-access-key-id
PDS_BLOBSTORE_S3_REGION=your-region
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=your-secret-key

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
PDS_EMAIL_SMTP_URL=
PDS_EMAIL_FROM_ADDRESS=

Be sure to set PDS_JWT_SECRET and PDS_ADMIN_PASSWORD to separate values generated from the following command. This means you should run it twice, using the output of each run as the value of PDS_JWT_SECRET and PDS_ADMIN_PASSWORD respectively.

openssl rand --hex 16

We also need to se PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX to the value produced by the following command:

openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32

You can keep pds.env at /etc/pds.env, as it is defined in the EnvironmentFile directive in pds.container.

Starting the PDS

The following systemd command will load the files we placed under /etc/containers/systemd/ and convert them to systemd unit files.

sudo systemctl daemon-reload

You should now be able to query pds.service by running the following command. Note that it will show the unit as inactive until it is started.

sudo systemctl status pds.service

Now we can activate the units:

sudo systemctl start pds.service

This should pull in the PDS container image and start it. You can check the status:

sudo systemctl status pds.service

Caddy configuration

A valid Caddy configuration should look like this:

{
 email myemail@example.com
 on_demand_tls {
  ask http://localhost:3000/tls-check
 }
}

# PDS
*.pds.example.com, pds.example.com {
 tls {
  on_demand
 }
 reverse_proxy http://localhost:3000
}
# Anything else for your server

pdsadmin.sh

You can create pdsadmin.sh and put in somewhere in your system’s binary PATH, such as /usr/local/bin/pdsadmin.sh.

#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail

PDSADMIN_BASE_URL="https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin"
export PDS_ENV_FILE="/etc/pds.env"
# Command to run.
COMMAND="${1:-help}"
shift || true

# we don't actually need root here since it only is required 

# Download the script, if it exists.
SCRIPT_URL="${PDSADMIN_BASE_URL}/${COMMAND}.sh"
SCRIPT_FILE="$(mktemp /tmp/pdsadmin.${COMMAND}.XXXXXX)"

if [[ "${COMMAND}" == "update" ]]; then
  echo "ERROR: self-update not supported via podman"
  exit 1
fi

if ! curl --fail --silent --show-error --location --output "${SCRIPT_FILE}" "${SCRIPT_URL}"; then
  echo "ERROR: ${COMMAND} not found"
  exit 2
fi

chmod +x "${SCRIPT_FILE}"
if "${SCRIPT_FILE}" "$@"; then
  rm -f "${SCRIPT_FILE}"
fi

Make the file executable:

sudo chmod +x /usr/local/bin/pdsadmin.sh

You can now run pdsadmin.sh with no arguments to see usage info. You’ll of course need to create an account on your PDS.

Verifying your PDS is online and accessible

Visit https://your-domain.net/xrpc/_health in your browser. Or run the following command from your terminal:

curl https://your-domain.net/xrpc/_health

You should receive a JSON response with a version:

{"version":"0.4.204"}

You’ll also need to check that WebSockets are working. You can do this with the wsdump tool. You’ll need the latest version of Golang to install it.

sudo dnf install -y golang

Now to install the wsdump tool:

go install github.com/nrxr/wsdump@latest

Then run it:

wsdump "wss://your-domain.net/xrpc/com.atproto.sync.subscribeRepos?cursor=0"

Note that there will be no events on the WebSocket until they are created in the PDS, so the above command may continue to run with no output. You’ll have to press CTRL-C to stop it.

Closing

That’s how to setup a Bluesky PDS on CentOS Stream. Additionally, this setup should also work on AlmaLinux 10, Rocky Linux 10, and Fedora 43, but will not work on any earlier verisons of those distributions. I recommend reading the README.md at https://github.com/bluesky-social/pds for more information on using the pdsadmin command, setting up SMTP, and troubleshooting.

If you have any questions or issues with this setup, feel free to reach out to me at @hyperreal@tilde.zone. You may also submit an issue at bluesky-social/deploy-recipes, and either I or someone else will help you troubleshoot the issue.