Bluesky PDS with Podman on CentOS Stream 10

This is based on the following documentation from the official Bluesky GitHub:

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

https://github.com/bluesky-social/deploy-recipes

We'll keep SELinux in enforcing mode and install a policy module 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 github.com/bluesky-social/deploy-recipes .

Minimum server requirements

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'll use Caddy and it is installed.

Preliminary actions

Install Podman and epel-release.

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

SELinux policy module

SELinux is not required, but it is 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 if 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 container processes used by the PDS.

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 SELinux still denies some processes, you can check for them with the following command.

sudo ausearch -m avc -ts recent | sudo audit2allow

Installing the PDS Podman quadlet

The systemd quadlet files are taken from github.com/bluesky-social/deploy-recipes . The only modification I made is the EnvironmentFile setting.

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

Create the file /etc/containers/systemd/pds.container.

[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

Next, create the file /etc/containers/systemd/pds.pod.

[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!

Finally, create the file /etc/containers/systemd/pds.volume.

[Unit]
Description=Bluesky PDS Volume

[Volume]
Label=app=pds

These files comprise a systemd quadlet. Quadlets enable Podman containers to run as systemd services. It's a declarative way to configure containers that fits in with the systemd ecosystem.

More information on how to configure quadlets can be found here: podman-systemd.unit .

It's necessary to have all of these files together in the same directory, as they depend on each other.

Setting up pds.env

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

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=

You should set PDS_JWT_SECRET to a pseudo-random value generated with the following command.

openssl rand --hex 16

Likewise for PDS_ADMIN_PASSWORD.

openssl rand --hex 16

You also need to set PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX to the value generated with this command.

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

As per the EnvironmentFile setting in pds.container, we should store pds.env at /etc/pds.env.

Starting the PDS

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

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 start the units.

sudo systemctl start pds.service

This should pull in the PDS container image and start it. You can check the status with 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 it 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 in your terminal.

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

You should receive a JSON response with a version, similar to this.

{"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 10. Additionally, this setup should also work on AlmaLinux 10, Rocky Linux 10, and Fedora 43, but will not work on any earlier versions of those distributions. I recommend reading the README.md at 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 github.com/bluesky-social/deploy-recipes , and either I or someone else will help troubleshoot the issue.