Setting up a Bluesky PDS with Podman on CentOS Stream 10
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.targetCreate 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=pdsThis 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.containeris like a template for systemd to generate a corresponding.servicefile with the defined settings.pds.volumeis a template for systemd to generate a Podman volume with the defined settings.pds.podis 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.