Bluesky PDS
Overview
This image runs Bluesky PDS, the reference Personal Data Server for the AT Protocol (atproto). atproto is the open, federated social networking protocol that powers Bluesky. A PDS hosts user accounts, their signed data repositories and their uploaded media (blobs). It speaks the XRPC API that atproto clients and the wider network use, so you can run your own independent node in the network instead of depending on a single provider.
Bluesky PDS is an API server, not a web application. It answers the com.atproto XRPC surface over HTTP and exposes a health endpoint. There is no browser dashboard to sign in to.
What you get:
- A containerised server behind nginx. The server runs as a Docker container bound to
127.0.0.1:3000on the loopback interface. nginx fronts it as a reverse proxy on port 80, and on port 443 once you add TLS. The WebSocket upgrade that the atproto firehose (com.atproto.sync.subscribeRepos) needs is already configured. - A frozen container version. The pds container image is pinned to an image digest, so the bytes never change under you.
- A dedicated data volume. The account databases, repository data and the on-disk blobstore all live on a dedicated EBS data volume mounted at
/pds. The volume is independently resizable and separate from the operating system disk. - Per-instance secrets. On the first boot of every deployed instance, a one-shot service generates a JWT signing secret, an admin password and a secp256k1 PLC rotation key unique to that instance. It binds the server to the instance's own address, verifies the admin round-trip, and records the admin password in
/root/bluesky-pds-credentials.txtwith mode 0600. No shared or default secrets ship in the image. - 24/7 SecureAnyCloud support
Prerequisites
Before you deploy this image, make sure you have:
- An Amazon Web Services account where you can launch EC2 instances
- IAM permissions to launch instances, create security groups and subscribe to AWS Marketplace products
- An EC2 key pair in the target Region for SSH access to the instance
- A VPC and subnet in the target Region, with a security group allowing inbound port 22 from your management network and inbound ports 80 and 443 from the networks that will reach the server
- The AWS CLI (version 2) installed locally, if you plan to deploy from the command line
- A DNS domain you control, if you intend to federate the server with the wider Bluesky network (covered in Step 7). You can run and verify the server on its own AWS address without a domain first.
Launch the instance from the AWS Marketplace
Sign in to the AWS Management Console, open the EC2 service, and select Launch instance. Under Application and OS Images, choose AWS Marketplace AMIs and search for Bluesky PDS. Select the SecureAnyCloud listing, choose Select, then choose Continue on the subscription summary.
Pick an instance type of m5.large or larger. Choose your EC2 key pair under Key pair (login). Leave the root volume at the default size or larger. The image automatically adds a separate 20 GiB data volume for the account databases and blobstore.
Under Network settings, select your VPC and subnet. Create or select a security group that allows inbound port 22 from your management network and inbound ports 80 and 443 from the networks that will reach the server.
Select Launch instance. First-boot initialisation takes under a minute after the instance state becomes Running and the status checks pass.
Launch the instance from the AWS CLI
Run the following command to launch an instance from the SecureAnyCloud Bluesky PDS Marketplace AMI into an existing subnet and security group.
Replace <ami-id> with the AMI ID shown on the Marketplace listing and <key-name> with your EC2 key pair name. Replace <subnet-id> with your subnet ID and <security-group-id> with a security group that opens ports 22, 80 and 443 as described above.
aws ec2 run-instances \
--image-id <ami-id> \
--instance-type m5.large \
--key-name <key-name> \
--subnet-id <subnet-id> \
--security-group-ids <security-group-id> \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":30,"VolumeType":"gp3"}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=bluesky-pds-01}]'On success, the command prints a JSON document. Note the instance ID. Once the instance is running, retrieve its public address with aws ec2 describe-instances --instance-ids <instance-id> --query "Reservations[].Instances[].PublicIpAddress" --output text.
Connect and retrieve the admin password
Connect over SSH with the key pair you selected and the public IP address from Step 2. The SSH login user depends on the operating system of the AMI variant you launched. For Bluesky PDS on Ubuntu 24.04, the login user is ubuntu:
ssh -i /path/to/your-key.pem ubuntu@<public-ip>First boot generates the per-instance admin password and writes it to a root-only file. Read it with:
sudo cat /root/bluesky-pds-credentials.txtThe file records PDS_HOSTNAME (initially the instance's own AWS address) and PDS_ADMIN_PASSWORD. The JWT signing secret and the secp256k1 PLC rotation key are also generated per instance. They live in /pds/pds.env (mode 0600).
Admin XRPC calls authenticate as the user admin with PDS_ADMIN_PASSWORD, using HTTP Basic auth.
Verify the server is healthy
The PDS answers a health endpoint and a server-description endpoint through nginx on port 80. Check the health endpoint, which returns the running server version:
curl -s http://127.0.0.1/xrpc/_healthThe response is a JSON version string like this:
{"version":"0.4.5009"}Next, describe the server. The response reports the server DID, the user-handle domains it accepts, and whether an invite code is required to create an account:
curl -s http://127.0.0.1/xrpc/com.atproto.server.describeServer | jq .On a freshly launched instance, before you configure your own domain, the response looks like this. The server DID and the available user domain are derived from the instance's own AWS address:
{ "did": "did:web:ec2-203-0-113-10.compute-1.amazonaws.com", "availableUserDomains": [ ".ec2-203-0-113-10.compute-1.amazonaws.com" ], "inviteCodeRequired": true, "links": {}, "contact": {} }From outside the instance, substitute your instance's public IP or DNS name for 127.0.0.1. For example, run curl -s http://<public-ip>/xrpc/_health.
Check the services and the container
The Docker engine, the reverse proxy and the PDS run under systemd. The PDS itself runs as a Docker container bound to the loopback interface. Confirm that all four are active:
systemctl is-active docker nginx bluesky-pds bluesky-pds-firstbootAll four report active. Next, inspect the running container. It is published only on 127.0.0.1:3000, so it is never directly exposed to the network. nginx is the only public entry point:
sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'NAMES IMAGE STATUS PORTS bluesky-pds 1fa8bbceabb6 Up About a minute 127.0.0.1:3000->3000/tcpnginx also serves an unauthenticated static health endpoint at /healthz, which returns ok. Use it for load balancer and uptime checks.
Mint an invite code with the admin API
Account creation is invite-gated by default (inviteCodeRequired is true). The admin API mints invite codes, and it authenticates as admin with the per-instance password. This block reads the password from the credentials file and calls com.atproto.server.createInviteCode:
PW=$(sudo grep '^PDS_ADMIN_PASSWORD=' /root/bluesky-pds-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" -X POST http://127.0.0.1/xrpc/com.atproto.server.createInviteCode \
-H 'Content-Type: application/json' -d '{"useCount":1}' | jq .The server returns a single-use invite code:
{ "code": "ec2-203-0-113-10-compute-1-amazonaws-com-tiiwk-jaxlk" }A wrong admin password is rejected with HTTP 401. To register a new account on your server, hand an invite code to a client or use it with com.atproto.server.createAccount.
Point your domain at the server and federate
To join the wider Bluesky network, the server needs a real public DNS hostname rather than the instance's AWS address, because handles and DIDs are derived from it. The image ships domain-free, so you can set this once you have DNS in place.
- Create two DNS records pointing at the instance's public IP. The first is an A record for your chosen hostname, for example
social.example.com. The second is a wildcard A record (*.social.example.com), so user handles likealice.social.example.comresolve. You need the wildcard only if you want handles under your domain. - Set the hostname in the server's environment file. Edit
/pds/pds.envand change thePDS_HOSTNAMEline to your domain:
sudo sed -i 's/^PDS_HOSTNAME=.*/PDS_HOSTNAME=your-pds-host.your-domain.example/' /pds/pds.env- Restart the server so it picks up the new hostname:
sudo systemctl restart bluesky-pdsRe-run the describeServer check from Step 4 and confirm that the did and availableUserDomains now reflect your domain.
Changing PDS_HOSTNAME after accounts exist is disruptive, so set your final domain before onboarding users.
Terminate TLS
In this image, the server speaks HTTP on port 80. The wider atproto network and clients expect HTTPS on port 443. Terminate TLS in front of the instance using either of these approaches:
- AWS Application Load Balancer (recommended). Create an ALB with an ACM certificate for your domain, an HTTPS:443 listener, and a target group forwarding to the instance on port 80. Point your DNS records at the ALB instead of the instance. This keeps certificate renewal fully managed.
- Your own certificate on the instance. Obtain a certificate for your domain and add a
listen 443 ssl;server block to the nginx site at/etc/nginx/sites-available/sac-bluesky-pds. Then reload nginx. If you use a tool like certbot, keep the port 80 server block for the ACME HTTP-01 challenge.
Whichever you choose, make sure the security group allows inbound 443 from your users and that clients reach the server over https://your-domain.
Create and manage accounts
With an invite code in hand, you can register accounts. atproto clients, including the Bluesky app pointed at a custom PDS, walk a user through account creation. The user supplies the invite code and a handle under one of the server's availableUserDomains.
Programmatically, com.atproto.server.createAccount takes an email, handle, password and inviteCode. Account creation publishes a DID to the public PLC directory, so create accounts against your final federated hostname (Step 7), not the temporary AWS address.
The admin API (Basic auth as admin) also exposes server-management endpoints under com.atproto.admin. Examples are disabling invite codes for an account and taking moderation actions. Keep the admin password secret, because anyone with it controls the server.
Backups and maintenance
All durable state lives on the dedicated EBS data volume mounted at /pds. That state is the account and actor SQLite databases, the repository data and the blobstore under /pds/blocks. The operating system, the Docker engine and the pinned container image live on the root volume.
- Snapshots. Take regular EBS snapshots of the
/pdsdata volume, for example with Amazon Data Lifecycle Manager. You can then restore accounts and blobs after a failure. - Encryption at rest. Enable EBS encryption on the data volume in the AWS console to encrypt the databases and blobs at rest.
- Resizing. When storage runs low, grow the
/pdsvolume in the EC2 console and extend the filesystem withsudo resize2fs $(findmnt -no SOURCE /pds). - Updates. The container is pinned to a fixed image digest, so it never changes unexpectedly. To move to a newer PDS release, pull the new image and update the
image:line in/pds/compose.yaml. Then runsudo systemctl restart bluesky-pds. - Restart.
sudo systemctl restart bluesky-pdsrecreates the container from the compose file.sudo systemctl restart nginxreloads the proxy.
Troubleshooting
- The health check does not answer. Confirm that the container is running with
sudo docker ps, and inspect logs withsudo docker logs --tail 100 bluesky-pds. Confirm that the environment file exists withsudo test -f /pds/pds.env && echo ok. - First boot has not completed. Check the one-shot initialiser with
systemctl status bluesky-pds-firstbootandsudo journalctl -u bluesky-pds-firstboot. If you read the credentials file before first boot finished, the file explains this. - An admin call returns 401. Re-read the password from
/root/bluesky-pds-credentials.txt, and authenticate as useradminwith HTTP Basic auth. - Handles or federation are not working. Verify that your DNS A and wildcard records resolve to the instance, that
PDS_HOSTNAMEin/pds/pds.envmatches, and that the server is reachable over HTTPS.
Support
SecureAnyCloud publishes this image. For help with the image, deployment or configuration, contact SecureAnyCloud support through the channel listed on the AWS Marketplace listing. For questions about the AT Protocol and Bluesky PDS itself, see the upstream bluesky-social/pds project documentation.
Deploy on AWS
Launch Bluesky PDS on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need help?
Our support team is available 24/7.
sales@secureanycloud.com