Files
atproto-pds-ansible/README.md
T
2026-08-04 16:41:47 -04:00

130 lines
4.7 KiB
Markdown

# atproto-pds-ansible
Ansible playbook for deploying a self-hosted [Bluesky PDS](https://github.com/bluesky-social/pds) with Caddy (TLS) and Watchtower (auto-updates).
## What it deploys
- **PDS** — Bluesky Personal Data Server (`ghcr.io/bluesky-social/pds:0.4`)
- **Caddy** — Reverse proxy with automatic TLS via Let's Encrypt
- **Watchtower** — Pulls new PDS image versions hourly and recreates containers
## Requirements
- A server running Ubuntu 20.04/22.04/24.04 or Debian 11/12/13
- SSH access as the `sysadmin` user (with sudo privileges)
- A domain pointed at the server (A record for the hostname + wildcard `*.` subdomain for handles)
- Ports 80 and 443 open to the internet
- Ansible installed locally
## Setup
```bash
cp inventory.yml.example inventory.yml # set your server IP
cp vars/main.yml.example vars/main.yml # set hostname, admin email, S3/SMTP creds
```
Edit `inventory.yml`:
```yaml
all:
hosts:
pds:
ansible_host: 203.0.113.10
```
Edit `vars/main.yml`:
```yaml
pds_hostname: bsky.example.com
pds_admin_email: you@example.com
pds_blobstore_s3_access_key: your-access-key
pds_blobstore_s3_secret_key: your-secret-key
```
## Deploy
```bash
ansible-playbook playbook.yml
```
## What it does
**First run:** the official [PDS installer](https://github.com/bluesky-social/pds/blob/main/installer.sh) bootstraps Docker, generates secrets, and creates the initial database. Ansible then reads the generated secrets from `pds.env`, templates `pds.env`, `compose.yaml`, and `Caddyfile`, and runs `docker compose up -d`.
**Subsequent runs:** the installer is skipped (guarded by `account.sqlite`). Ansible reads the existing secrets from `pds.env` and re-templates all config files. Changes only trigger a restart if content actually changed.
## Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| `pds_hostname` | yes | — | Public DNS name for the PDS |
| `pds_admin_email` | yes | — | Admin contact email (used by Caddy for Let's Encrypt) |
| `pds_data_dir` | no | `/pds` | Data directory (must be `/pds` — the installer enforces this) |
| `pds_image_tag` | no | `0.4` | PDS Docker image tag |
| `pds_blob_upload_limit` | no | `104857600` | Max blob upload size in bytes |
| `pds_rate_limits_enabled` | no | `true` | Enable rate limiting |
| `pds_invite_required` | no | `true` | Require invite codes to create accounts |
| `pds_blobstore_type` | no | `disk` | Blob storage backend (`disk` or `s3`) |
| `pds_email_from_address` | no | — | From address for outgoing emails |
| `pds_blobstore_s3_access_key` | yes (if S3) | — | S3 access key for blob storage |
| `pds_blobstore_s3_secret_key` | yes (if S3) | — | S3 secret key for blob storage |
| `pds_email_smtp_url` | no | — | SMTP URL (with credentials) for email verification |
## Auto-updates
Watchtower checks every hour (`WATCHTOWER_POLL_INTERVAL=3600`) for new `0.4.x` images on `ghcr.io/bluesky-social/pds:0.4`. When a new image is found, it pulls it and recreates the container. Old images are cleaned up automatically.
## Post-deploy
Check that the PDS is healthy:
```bash
curl https://your-hostname/xrpc/_health
```
Create an account:
```bash
pdsadmin account create
```
## Teardown
Run on the server to remove everything, use with caution:
```bash
sudo docker compose -f /pds/compose.yaml down
sudo rm -rf /pds
sudo systemctl stop pds 2>/dev/null
sudo systemctl disable pds 2>/dev/null
sudo rm -f /etc/systemd/system/pds.service /usr/local/bin/pdsadmin
sudo systemctl daemon-reload
```
## Notes
- `pds.env` is Ansible-managed. Secrets (JWT, admin password, PLC rotation key) are generated by the installer on first run, then read and preserved by Ansible on subsequent runs. To change config like S3 or SMTP, update `vars/main.yml` and re-run the playbook.
- The installer creates a `pds.service` systemd unit. It's left in place — it just runs `docker compose up -d` on boot. Docker's `restart: unless-stopped` policy also handles this independently.
## CI/CD (Gitea Actions)
Auto-deploys on push to `main`. Set up these in Gitea repo settings (Settings > Actions):
**Variables** (non-sensitive config):
| Variable | Description |
|---|---|
| `SERVER_IP` | Target server IP address |
| `PDS_HOSTNAME` | Public DNS name for the PDS (e.g., `pds.example.com`) |
| `PDS_ADMIN_EMAIL` | Admin contact email |
**Secrets** (sensitive credentials):
| Secret | Description |
|---|---|
| `SSH_PRIVATE_KEY` | Private SSH key for the `sysadmin` user on the target server |
| `OVH_S3_ACCESS_KEY` | OVH S3 access key for blob storage |
| `OVH_S3_SECRET_KEY` | OVH S3 secret key for blob storage |
The workflow installs Ansible, writes the SSH key from a secret, generates `inventory.yml` and `vars/main.yml` from vars and secrets, and runs the playbook.