diff --git a/.gitignore b/.gitignore index 5c199eb..73cf2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ # ---> Ansible *.retry +.letta +# User-specific config +inventory.yml +vars/main.yml diff --git a/README.md b/README.md index 3c7b54f..35bf81f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,84 @@ # atproto-pds-ansible -Config for atproto PDS services using ansible \ No newline at end of file +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 your hostname and admin email +``` + +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 +``` + +## 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 templates `compose.yaml` and `Caddyfile` over the installer's defaults and runs `docker compose up -d`. + +**Subsequent runs:** the installer is skipped (guarded by `account.sqlite`). Ansible only applies changes if the templates have changed. `docker compose up -d` is a no-op when nothing 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 | + +## 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 +``` + +## Notes + +- `pds.env` (containing JWT secret, admin password, PLC rotation key) is generated by the installer and not managed by Ansible. Edit it directly on the server to add SMTP or change settings. +- 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. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..b0b314e --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +host_key_checking = False +remote_user = sysadmin diff --git a/inventory.yml.example b/inventory.yml.example new file mode 100644 index 0000000..f09dcb2 --- /dev/null +++ b/inventory.yml.example @@ -0,0 +1,4 @@ +all: + hosts: + pds: + ansible_host: your.server.ip diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..fe0a3de --- /dev/null +++ b/playbook.yml @@ -0,0 +1,38 @@ +--- +- name: Deploy PDS + hosts: pds + vars_files: + - vars/main.yml + + tasks: + - name: Run PDS installer (first time only) + shell: >- + echo "n" | bash <(curl -fsSL + https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh) + {{ pds_data_dir }} {{ pds_hostname }} {{ pds_admin_email }} + args: + creates: "{{ pds_data_dir }}/account.sqlite" + + - name: Deploy compose.yaml + template: + src: templates/compose.yaml.j2 + dest: "{{ pds_data_dir }}/compose.yaml" + mode: "0644" + notify: apply changes + + - name: Deploy Caddyfile + template: + src: templates/Caddyfile.j2 + dest: "{{ pds_data_dir }}/caddy/etc/caddy/Caddyfile" + mode: "0644" + notify: apply changes + + - name: Download pdsadmin + get_url: + url: https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh + dest: /usr/local/bin/pdsadmin + mode: "0755" + + handlers: + - name: apply changes + shell: cd {{ pds_data_dir }} && docker compose up -d && docker restart caddy diff --git a/templates/Caddyfile.j2 b/templates/Caddyfile.j2 new file mode 100644 index 0000000..3fdf99e --- /dev/null +++ b/templates/Caddyfile.j2 @@ -0,0 +1,17 @@ +{ + email {{ pds_admin_email }} + on_demand_tls { + ask http://localhost:3000/tls-check + } +} + +*.{{ pds_hostname }}, {{ pds_hostname }} { + tls { + on_demand + } + log { + output stdout + format json + } + reverse_proxy http://localhost:3000 +} diff --git a/templates/compose.yaml.j2 b/templates/compose.yaml.j2 new file mode 100644 index 0000000..714b1b2 --- /dev/null +++ b/templates/compose.yaml.j2 @@ -0,0 +1,38 @@ +services: + pds: + container_name: pds + image: ghcr.io/bluesky-social/pds:{{ pds_image_tag }} + network_mode: host + restart: unless-stopped + volumes: + - type: bind + source: {{ pds_data_dir }} + target: {{ pds_data_dir }} + env_file: + - {{ pds_data_dir }}/pds.env + + caddy: + container_name: caddy + image: caddy:2 + network_mode: host + restart: unless-stopped + volumes: + - type: bind + source: {{ pds_data_dir }}/caddy/data + target: /data + - type: bind + source: {{ pds_data_dir }}/caddy/etc/caddy + target: /etc/caddy + + watchtower: + container_name: watchtower + image: containrrr/watchtower:latest + network_mode: host + restart: unless-stopped + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_POLL_INTERVAL=3600 diff --git a/vars/main.yml.example b/vars/main.yml.example new file mode 100644 index 0000000..2d31663 --- /dev/null +++ b/vars/main.yml.example @@ -0,0 +1,8 @@ +# Required +pds_hostname: pds.example.com +pds_admin_email: admin@example.com + +# Optional (defaults shown) +# pds_data_dir must be /pds — the installer enforces this +pds_data_dir: /pds +pds_image_tag: "0.4"