Bp
Applications AWS

Bluesky PDS on AWS User Guide

| Product: Bluesky PDS on AWS

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.

  1. 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 like alice.social.example.com resolve. A wildcard is only needed if you want handles under your domain.
  2. Set the hostname in the server's environment file. Edit /pds/pds.env and change the PDS_HOSTNAME line to your domain:
sudo sed -i 's/^PDS_HOSTNAME=.*/PDS_HOSTNAME=your-pds-host.your-domain.example/' /pds/pds.env
  1. 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 /pds data 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 /pds volume in the EC2 console and extend the filesystem with sudo 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 run sudo systemctl restart bluesky-pds.
  • Restart: sudo systemctl restart bluesky-pds recreates the container from the compose file; sudo systemctl restart nginx reloads the proxy.

Troubleshooting

  • Health check does not answer: confirm the container is running with sudo docker ps; inspect logs with sudo docker logs --tail 100 bluesky-pds. Confirm the environment file exists with sudo test -f /pds/pds.env && echo ok.
  • First boot has not completed: check the one-shot initialiser with systemctl status bluesky-pds-firstboot and sudo 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 user admin with HTTP Basic auth.
  • Handles or federation not working: verify your DNS A and wildcard records resolve to the instance, that PDS_HOSTNAME in /pds/pds.env matches, 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.