Bluesky PDS on AWS User Guide
Overview
This image runs Bluesky PDS, the reference Personal Data Server for the AT Protocol (atproto) - the open, federated social networking protocol that powers Bluesky. A PDS hosts user accounts, their signed data repositories and their uploaded media (blobs), speaks the XRPC API that atproto clients and the wider network use, and lets you 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 into. In this image the server runs as a Docker container bound to 127.0.0.1:3000 on the loopback interface and is fronted by nginx as a reverse proxy on port 80 (and 443 once you add TLS), with the WebSocket upgrade the atproto firehose (com.atproto.sync.subscribeRepos) needs already configured. The pds container image is version-frozen to a pinned image digest so the bytes never change under you.
The account databases, repository data and the on-disk blobstore all live on a dedicated EBS data volume mounted at /pds, independently resizable and separate from the operating system disk. 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, binds the server to the instance's own address, verifies the admin round-trip, and records the admin password in /root/bluesky-pds-credentials.txt with mode 0600. No shared or default secrets ship in the image.
Prerequisites
Before you deploy this image you need:
- 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.
Step 1: 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 cloudimg listing and choose Select, then Continue on the subscription summary.
Pick an instance type of m5.large or larger. Choose your EC2 key pair under Key pair (login). Under Network settings select your VPC and subnet, and either 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. Leave the root volume at the default size or larger; the image adds a separate 20 GiB data volume for the account databases and blobstore automatically.
Select Launch instance. First boot initialisation takes under a minute after the instance state becomes Running and the status checks pass.
Step 2: Launch the Instance from the AWS CLI
The following block launches an instance from the cloudimg Bluesky PDS Marketplace AMI into an existing subnet and security group. Replace <ami-id> with the AMI ID shown on the Marketplace listing, <key-name> with your EC2 key pair name, <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}]'
The command prints a JSON document on success. Note the instance ID, then retrieve its public address once it is running with aws ec2 describe-instances --instance-ids <instance-id> --query "Reservations[].Instances[].PublicIpAddress" --output text.
Step 3: 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:
| AMI variant | SSH login user |
|---|---|
| Bluesky PDS on Ubuntu 24.04 | ubuntu |
ssh -i /path/to/your-key.pem ubuntu@<public-ip>
The per-instance admin password is generated on first boot and written to a root-only file. Read it with:
sudo cat /root/bluesky-pds-credentials.txt
The 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 generated per instance too and live in /pds/pds.env (mode 0600). Admin XRPC calls authenticate as the user admin with PDS_ADMIN_PASSWORD using HTTP Basic auth.
Step 4: 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 - it returns the running server version:
curl -s http://127.0.0.1/xrpc/_health
You should see a JSON version string like:
{"version":"0.4.5009"}
Now describe the server. This reports the server DID, the user-handle domains it will accept, 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 curl -s http://<public-ip>/xrpc/_health.
Step 5: Check the Services and the Container
The Docker engine, the reverse proxy and the PDS run under systemd, and the PDS itself runs as a Docker container bound to the loopback interface. Confirm they are all active:
systemctl is-active docker nginx bluesky-pds bluesky-pds-firstboot
All four report active. Inspect the running container - it is published only on 127.0.0.1:3000 so it is never directly exposed to the network, with nginx 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/tcp
nginx also serves an unauthenticated static health endpoint at /healthz (returns ok) for load balancer and uptime checks.
Step 6: Mint an Invite Code with the Admin API
Account creation is invite-gated by default (inviteCodeRequired is true). The admin API mints invite codes; 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. Hand an invite code to a client (or use it with com.atproto.server.createAccount) to register a new account on your server.
Step 7: 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: an A record for your chosen hostname (for example
social.example.com) and a wildcard A record (*.social.example.com) so user handles likealice.social.example.comresolve. A wildcard is only needed 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-pds
Re-run the describeServer check from Step 4 and confirm the did and availableUserDomains now reflect your domain. Changing PDS_HOSTNAME after accounts exist is disruptive, so set your final domain before onboarding users.
Step 8: Terminate TLS
The server speaks HTTP on port 80 in this image; 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/cloudimg-bluesky-pds, then reload nginx. Keep the port 80 server block for the ACME HTTP-01 challenge if you use a tool like certbot.
Whichever you choose, make sure the security group allows inbound 443 from your users and that clients reach the server over https://your-domain.
Step 9: 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 using 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. Because account creation publishes a DID to the public PLC directory, 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 - for example to disable invite codes for an account or take moderation actions. Keep the admin password secret; anyone with it controls the server.
Step 10: Backups and Maintenance
All durable state - the account and actor SQLite databases, repository data and the blobstore under /pds/blocks - lives on the dedicated EBS data volume mounted at /pds. 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) so you can restore accounts and blobs after a failure. - Encryption at rest: enable EBS encryption on the data volume via the AWS console for encryption of the databases and blobs at rest.
- Resizing: grow the
/pdsvolume in the EC2 console and extend the filesystem withsudo resize2fs $(findmnt -no SOURCE /pds)when storage runs low. - 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, update the
image:line in/pds/compose.yaml, and runsudo systemctl restart bluesky-pds. - Restart:
sudo systemctl restart bluesky-pdsrecreates the container from the compose file;sudo systemctl restart nginxreloads the proxy.
Troubleshooting
- Health check does not answer: confirm the container is running with
sudo docker ps; inspect logs withsudo docker logs --tail 100 bluesky-pds. Confirm 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. The credentials file explains this if you read it before first boot finished. - Admin call returns 401: re-read the password from
/root/bluesky-pds-credentials.txt; authenticate as useradminwith HTTP Basic auth. - Handles or federation not working: verify 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
This image is published by cloudimg. For help with the image, deployment, or configuration, contact cloudimg 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.