feat: pds 0.5 with telemetry and branding changes
Deploy PDS / deploy (push) Canceled after 0s

This commit is contained in:
gov committed 2026-08-09 23:15:35 -04:00
1 parent 82251b973f
commit be08225aaf
9 files changed
+323 -7

No files matched your search

+11
View File
@@ -45,6 +45,17 @@ jobs:
pds_backup_s3_access_key: ${{ secrets.OVH_S3_ACCESS_KEY }} pds_backup_s3_access_key: ${{ secrets.OVH_S3_ACCESS_KEY }}
pds_backup_s3_secret_key: ${{ secrets.OVH_S3_SECRET_KEY }} pds_backup_s3_secret_key: ${{ secrets.OVH_S3_SECRET_KEY }}
EOF EOF
# Optional: enable telemetry/monitoring
if [ -n "${{ vars.PDS_TELEMETRY_ENABLED }}" ]; then
echo "pds_telemetry_enabled: ${{ vars.PDS_TELEMETRY_ENABLED }}" >> vars/main.yml
fi
# Optional: branding
if [ -n "${{ vars.PDS_SERVICE_NAME }}" ]; then
echo "pds_service_name: \"${{ vars.PDS_SERVICE_NAME }}\"" >> vars/main.yml
fi
if [ -n "${{ vars.PDS_PRIMARY_COLOR }}" ]; then
echo "pds_primary_color: \"${{ vars.PDS_PRIMARY_COLOR }}\"" >> vars/main.yml
fi
- name: Run playbook - name: Run playbook
run: | run: |
+58 -3
View File
@@ -4,10 +4,11 @@ Ansible playbook for deploying a self-hosted [Bluesky PDS](https://github.com/bl
## What it deploys ## What it deploys
- **PDS** — Bluesky Personal Data Server (`ghcr.io/bluesky-social/pds:0.4`) - **PDS** — Bluesky Personal Data Server (`ghcr.io/bluesky-social/pds:0.5`)
- **Caddy** — Reverse proxy with automatic TLS via Let's Encrypt - **Caddy** — Reverse proxy with automatic TLS via Let's Encrypt
- **Watchtower** — Pulls new PDS image versions hourly and recreates containers - **Watchtower** — Pulls new PDS image versions hourly and recreates containers
- **PDS Backup Service** — rclone + systemd timer for periodic PDS backups to S3, plus Watchtower pre-update hook - **PDS Backup Service** — rclone + systemd timer for periodic PDS backups to S3, plus Watchtower pre-update hook
- **Monitoring** *(optional)* — Prometheus + Grafana + node_exporter stack with OpenTelemetry metrics and a pre-built PDS Overview dashboard
## Requirements ## Requirements
@@ -63,8 +64,8 @@ ansible-playbook playbook.yml
| `pds_hostname` | yes | — | Public DNS name for the PDS | | `pds_hostname` | yes | — | Public DNS name for the PDS |
| `pds_admin_email` | yes | — | Admin contact email (used by Caddy for Let's Encrypt) | | `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_data_dir` | no | `/pds` | Data directory (must be `/pds` — the installer enforces this) |
| `pds_image_tag` | no | `0.4` | PDS Docker image tag | | `pds_image_tag` | no | `0.5` | PDS Docker image tag |
| `pds_blob_upload_limit` | no | `104857600` | Max blob upload size in bytes | | `pds_blob_upload_limit` | no | `314572800` | Max blob upload size in bytes (300MB) |
| `pds_rate_limits_enabled` | no | `true` | Enable rate limiting | | `pds_rate_limits_enabled` | no | `true` | Enable rate limiting |
| `pds_invite_required` | no | `true` | Require invite codes to create accounts | | `pds_invite_required` | no | `true` | Require invite codes to create accounts |
| `pds_blobstore_type` | no | `disk` | Blob storage backend (`disk` or `s3`) | | `pds_blobstore_type` | no | `disk` | Blob storage backend (`disk` or `s3`) |
@@ -77,6 +78,35 @@ ansible-playbook playbook.yml
| `pds_email_smtp_url` | no | — | SMTP URL (with credentials) for email verification | | `pds_email_smtp_url` | no | — | SMTP URL (with credentials) for email verification |
| `pds_email_from_address` | no | — | From address for outgoing emails | | `pds_email_from_address` | no | — | From address for outgoing emails |
### Branding
All optional. Defaults to hostname + "PDS" with neutral colors. See the [PDS customization blog post](https://atproto.com/blog/pds-customization-and-metrics) for details.
| Variable | Default | Description |
|---|---|---|
| `pds_service_name` | hostname + " PDS" | Display name shown on auth screens and emails |
| `pds_logo_url` | — | Logo URL (SVG recommended) |
| `pds_primary_color` | — | Primary color for buttons, links, accents (hex, e.g. `#7c3aed`) |
| `pds_error_color` | — | Error state color |
| `pds_warning_color` | — | Warning state color |
| `pds_info_color` | — | Info state color |
| `pds_success_color` | — | Success state color |
| `pds_home_url` | — | Home page link in footer |
| `pds_terms_of_service_url` | — | Terms of service link in footer |
| `pds_privacy_policy_url` | — | Privacy policy link in footer |
| `pds_support_url` | — | Support link in footer |
| `pds_background_light_url` | — | Background image for auth screens (light mode) |
| `pds_background_dark_url` | — | Background image for auth screens (dark mode) |
### Telemetry & monitoring
Requires PDS image `0.5+` (telemetry added in `@atproto/pds` 0.5.26). When enabled, deploys Prometheus + Grafana + node_exporter alongside the PDS. All services bind to `127.0.0.1` — access via SSH tunnel.
| Variable | Default | Description |
|---|---|---|
| `pds_telemetry_enabled` | `false` | Enable OpenTelemetry + monitoring stack |
| `pds_grafana_image_tag` | `13.1.1` | Grafana Docker image tag (pinned — no floating major tag) |
### Backup service ### Backup service
| Variable | Required | Default | Description | | Variable | Required | Default | Description |
@@ -102,6 +132,28 @@ Three backup types, all uploaded to `s3://atprotopds-backup`:
Backups exclude `/pds/blocks` (blobs are already in S3). Each backup is a tarball named `{type}-YYYYMMDD-HHMMSS.tar.gz`. Backups exclude `/pds/blocks` (blobs are already in S3). Each backup is a tarball named `{type}-YYYYMMDD-HHMMSS.tar.gz`.
## Monitoring
Set `pds_telemetry_enabled: true` in `vars/main.yml` to enable OpenTelemetry metrics and deploy the monitoring stack (Prometheus + Grafana + node_exporter). The PDS pushes metrics over OTLP to Prometheus, which receives them directly (no collector needed).
**Access Grafana via SSH tunnel:**
```bash
ssh -L 3001:localhost:3001 sysadmin@your-server
```
Then open `http://localhost:3001` and log in with `admin`/`admin` (change on first login). The PDS Overview dashboard is provisioned automatically in a folder named "PDS".
**Ports (all bound to 127.0.0.1):**
| Service | Port | Purpose |
|---|---|---|
| Grafana | 3001 | Dashboard UI |
| Prometheus | 9090 | Metrics query API |
| node_exporter | 9100 | Host metrics |
Prometheus retains 15 days of data by default. Data is stored in Docker volumes, not under `/pds`, so it's not included in PDS backups.
## Post-deploy ## Post-deploy
Check that the PDS is healthy: Check that the PDS is healthy:
@@ -145,6 +197,9 @@ Auto-deploys on push to `main`. Set up these in Gitea repo settings (Settings >
| `SERVER_IP` | Target server IP address | | `SERVER_IP` | Target server IP address |
| `PDS_HOSTNAME` | Public DNS name for the PDS (e.g., `pds.example.com`) | | `PDS_HOSTNAME` | Public DNS name for the PDS (e.g., `pds.example.com`) |
| `PDS_ADMIN_EMAIL` | Admin contact email | | `PDS_ADMIN_EMAIL` | Admin contact email |
| `PDS_TELEMETRY_ENABLED` | *(optional)* Set to `true` to enable telemetry + monitoring stack |
| `PDS_SERVICE_NAME` | *(optional)* PDS display name for branding |
| `PDS_PRIMARY_COLOR` | *(optional)* Primary color (hex) for branding |
**Secrets** (sensitive credentials): **Secrets** (sensitive credentials):
+68 -2
View File
@@ -6,8 +6,8 @@
- vars/main.yml - vars/main.yml
vars: vars:
pds_data_dir: /pds pds_data_dir: /pds
pds_image_tag: "0.4" pds_image_tag: "0.5"
pds_blob_upload_limit: 104857600 pds_blob_upload_limit: 314572800
pds_rate_limits_enabled: true pds_rate_limits_enabled: true
pds_invite_required: true pds_invite_required: true
pds_backup_period: 24h pds_backup_period: 24h
@@ -16,6 +16,7 @@
pds_backup_s3_endpoint: https://s3.bhs.io.cloud.ovh.net pds_backup_s3_endpoint: https://s3.bhs.io.cloud.ovh.net
pds_backup_s3_access_key: "{{ pds_blobstore_s3_access_key }}" pds_backup_s3_access_key: "{{ pds_blobstore_s3_access_key }}"
pds_backup_s3_secret_key: "{{ pds_blobstore_s3_secret_key }}" pds_backup_s3_secret_key: "{{ pds_blobstore_s3_secret_key }}"
pds_grafana_image_tag: "13.1.1"
tasks: tasks:
- name: Run PDS installer (first time only) - name: Run PDS installer (first time only)
@@ -135,6 +136,71 @@
changed_when: false changed_when: false
failed_when: pds_backup_exit_code.stdout != "0" failed_when: pds_backup_exit_code.stdout != "0"
# ── Monitoring stack (telemetry) ──────────────────────────────
# Deploys Prometheus + Grafana + node_exporter when pds_telemetry_enabled is true.
# All services bind to 127.0.0.1 — access via SSH tunnel:
# ssh -L 3001:localhost:3001 sysadmin@<server>
# Then open http://localhost:3001 (admin/admin, change on first login)
- name: Create monitoring directories
file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ pds_data_dir }}/monitoring"
- "{{ pds_data_dir }}/monitoring/grafana/provisioning/datasources"
- "{{ pds_data_dir }}/monitoring/grafana/provisioning/dashboards"
- "{{ pds_data_dir }}/monitoring/dashboards"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
- name: Deploy monitoring compose
template:
src: templates/monitoring-compose.yaml.j2
dest: "{{ pds_data_dir }}/monitoring/compose.yaml"
mode: "0644"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
notify: restart monitoring
- name: Deploy Prometheus config
template:
src: templates/prometheus.yml.j2
dest: "{{ pds_data_dir }}/monitoring/prometheus.yml"
mode: "0644"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
notify: restart monitoring
- name: Deploy Grafana datasource provisioning
template:
src: templates/grafana-datasources.yml.j2
dest: "{{ pds_data_dir }}/monitoring/grafana/provisioning/datasources/prometheus.yml"
mode: "0644"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
notify: restart monitoring
- name: Deploy Grafana dashboard provisioning
template:
src: templates/grafana-dashboards.yml.j2
dest: "{{ pds_data_dir }}/monitoring/grafana/provisioning/dashboards/pds.yml"
mode: "0644"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
notify: restart monitoring
- name: Download PDS Overview dashboard
get_url:
url: https://raw.githubusercontent.com/bluesky-social/pds/main/monitoring/dashboards/pds-overview.json
dest: "{{ pds_data_dir }}/monitoring/dashboards/pds-overview.json"
mode: "0644"
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
notify: restart monitoring
- name: Start monitoring stack
shell: cd {{ pds_data_dir }}/monitoring && docker compose up -d
when: pds_telemetry_enabled is defined and pds_telemetry_enabled | bool
handlers: handlers:
- name: apply changes - name: apply changes
shell: cd {{ pds_data_dir }} && docker compose up -d && docker restart caddy shell: cd {{ pds_data_dir }} && docker compose up -d && docker restart caddy
- name: restart monitoring
shell: cd {{ pds_data_dir }}/monitoring && docker compose up -d --force-recreate
+11
View File
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: PDS
type: file
folder: PDS
updateIntervalSeconds: 30
allowUiUpdates: false
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
+12
View File
@@ -0,0 +1,12 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: pds-prometheus
type: prometheus
access: proxy
url: http://127.0.0.1:9090
isDefault: true
editable: false
jsonData:
timeInterval: 15s
+66
View File
@@ -0,0 +1,66 @@
name: pds-monitoring
# Self-contained monitoring stack for a self-hosted PDS.
# Deployed by Ansible to {{ pds_data_dir }}/monitoring/compose.yaml
#
# All services bind to 127.0.0.1 — nothing is exposed to the internet.
# Reach Grafana via SSH tunnel:
# ssh -L 3001:localhost:3001 you@your-pds-host
# Then open http://localhost:3001 (admin/admin, change on first login)
services:
node-exporter:
container_name: pds-node-exporter
image: prom/node-exporter:v1
network_mode: host
pid: host
restart: unless-stopped
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run|var/lib/docker/.+)($$|/)'
- '--web.listen-address=127.0.0.1:9100'
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
prometheus:
container_name: pds-prometheus
image: prom/prometheus:v3
network_mode: host
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.listen-address=127.0.0.1:9090'
# Lets the PDS push OTLP metrics directly — no collector needed
- '--web.enable-otlp-receiver'
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
grafana:
container_name: pds-grafana
# Grafana publishes no floating major tag — pinned to exact version
image: grafana/grafana:{{ pds_grafana_image_tag }}
network_mode: host
depends_on:
- prometheus
restart: unless-stopped
environment:
GF_SERVER_HTTP_ADDR: 127.0.0.1
GF_SERVER_HTTP_PORT: 3001
GF_ANALYTICS_REPORTING_ENABLED: false
GF_ANALYTICS_CHECK_FOR_UPDATES: false
GF_USERS_ALLOW_SIGN_UP: false
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
volumes:
prometheus-data:
grafana-data:
+48
View File
@@ -33,3 +33,51 @@ PDS_EMAIL_SMTP_URL={{ pds_email_smtp_url }}
{% if pds_email_from_address is defined %} {% if pds_email_from_address is defined %}
PDS_EMAIL_FROM_ADDRESS={{ pds_email_from_address }} PDS_EMAIL_FROM_ADDRESS={{ pds_email_from_address }}
{% endif %} {% endif %}
{% if pds_service_name is defined %}
PDS_SERVICE_NAME={{ pds_service_name }}
{% endif %}
{% if pds_logo_url is defined %}
PDS_LOGO_URL={{ pds_logo_url }}
{% endif %}
{% if pds_primary_color is defined %}
PDS_PRIMARY_COLOR={{ pds_primary_color }}
{% endif %}
{% if pds_error_color is defined %}
PDS_ERROR_COLOR={{ pds_error_color }}
{% endif %}
{% if pds_warning_color is defined %}
PDS_WARNING_COLOR={{ pds_warning_color }}
{% endif %}
{% if pds_info_color is defined %}
PDS_INFO_COLOR={{ pds_info_color }}
{% endif %}
{% if pds_success_color is defined %}
PDS_SUCCESS_COLOR={{ pds_success_color }}
{% endif %}
{% if pds_home_url is defined %}
PDS_HOME_URL={{ pds_home_url }}
{% endif %}
{% if pds_terms_of_service_url is defined %}
PDS_TERMS_OF_SERVICE_URL={{ pds_terms_of_service_url }}
{% endif %}
{% if pds_privacy_policy_url is defined %}
PDS_PRIVACY_POLICY_URL={{ pds_privacy_policy_url }}
{% endif %}
{% if pds_support_url is defined %}
PDS_SUPPORT_URL={{ pds_support_url }}
{% endif %}
{% if pds_background_light_url is defined %}
PDS_BACKGROUND_LIGHT_URL={{ pds_background_light_url }}
{% endif %}
{% if pds_background_dark_url is defined %}
PDS_BACKGROUND_DARK_URL={{ pds_background_dark_url }}
{% endif %}
{% if pds_telemetry_enabled is defined and pds_telemetry_enabled | bool %}
NODE_OPTIONS=--import=@atproto/pds/telemetry
OTEL_SERVICE_NAME=pds
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:9090/api/v1/otlp/v1/metrics
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
OTEL_METRIC_EXPORT_INTERVAL=15000
OTEL_SEMCONV_STABILITY_OPT_IN=http
OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process,serviceinstance,container
{% endif %}
+28
View File
@@ -0,0 +1,28 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
# Host metrics are scraped. PDS metrics are *pushed* by the PDS over OTLP to
# /api/v1/otlp/v1/metrics (enabled by --web.enable-otlp-receiver in
# compose.yaml), so the PDS needs no scrape job here.
scrape_configs:
- job_name: node
static_configs:
- targets: ['127.0.0.1:9100']
- job_name: prometheus
static_configs:
- targets: ['127.0.0.1:9090']
otlp:
# Keep OTel resource attributes that identify which PDS a series came from
promote_resource_attributes:
- service.name
- service.version
- service.instance.id
- host.name
storage:
tsdb:
# OTLP is a push protocol, so samples can arrive slightly out of order
out_of_order_time_window: 30m
+21 -2
View File
@@ -5,8 +5,8 @@ pds_admin_email: admin@example.com
# Optional (defaults shown) # Optional (defaults shown)
# pds_data_dir must be /pds — the installer enforces this # pds_data_dir must be /pds — the installer enforces this
pds_data_dir: /pds pds_data_dir: /pds
pds_image_tag: "0.4" pds_image_tag: "0.5"
pds_blob_upload_limit: 104857600 pds_blob_upload_limit: 314572800
pds_rate_limits_enabled: true pds_rate_limits_enabled: true
pds_invite_required: true pds_invite_required: true
@@ -30,3 +30,22 @@ pds_invite_required: true
# Email (uncomment to enable email verification) # Email (uncomment to enable email verification)
# pds_email_smtp_url: smtps://username:password@smtp.example.com/ # pds_email_smtp_url: smtps://username:password@smtp.example.com/
# pds_email_from_address: admin@example.com # pds_email_from_address: admin@example.com
# Branding (all optional — defaults to hostname + "PDS" with neutral colors)
# pds_service_name: "Example Social"
# pds_logo_url: "https://example.com/logo.svg"
# pds_primary_color: "#7c3aed"
# pds_error_color: "#dc2626"
# pds_warning_color: "#f59e0b"
# pds_info_color: "#3b82f6"
# pds_success_color: "#16a34a"
# pds_home_url: "https://example.com"
# pds_terms_of_service_url: "https://example.com/tos"
# pds_privacy_policy_url: "https://example.com/privacy"
# pds_support_url: "https://example.com/support"
# pds_background_light_url: "https://example.com/bg-light.png"
# pds_background_dark_url: "https://example.com/bg-dark.png"
# Telemetry & monitoring (OpenTelemetry + Prometheus + Grafana)
# Requires PDS image 0.5+ (telemetry added in @atproto/pds 0.5.26)
# pds_telemetry_enabled: true