Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions ct/bluesky-pds.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main"
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Community Scripts contributor
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
# Source: https://github.com/bluesky-social/pds
APP="Bluesky PDS"
var_tags="${var_tags:-social;atproto}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-25}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_arm64=1
export var_pds_hostname="${var_pds_hostname:-}"

header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if check_for_gh_branch "bluesky-pds" "bluesky-social/atproto" "main"; then
msg_info "Stopping Bluesky PDS"
systemctl stop pds
msg_ok "Stopped Bluesky PDS"
CLEAN_INSTALL=1 fetch_and_deploy_gh_branch "bluesky-pds" "bluesky-social/atproto" "main"
NODE_VERSION="24" setup_nodejs
$STD npm install --global pnpm@12.16.1
export PUPPETEER_SKIP_DOWNLOAD=true
cd /opt/bluesky-pds
$STD pnpm install --frozen-lockfile --filter 'pds-service...' --shamefully-hoist
$STD pnpm run --recursive --stream --workspace-concurrency 1 --filter 'pds-service...' build --force
$STD pnpm install --prod --filter 'pds-service...' --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false
msg_info "Starting Bluesky PDS"
systemctl start pds
msg_ok "Started Bluesky PDS"
msg_ok "Updated Bluesky PDS"
fi
if check_for_gh_release "goat" "bluesky-social/goat"; then
fetch_and_deploy_gh_release "goat" "bluesky-social/goat" "binary"
fi
exit
}
start
build_container

description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} has been successfully initialized!${CL}"
echo -e "${INFO}${YW}PDS URL:${CL} ${BGN}https://${var_pds_hostname:-<your-domain>}${CL}"
echo -e "${INFO}${YW}Administration:${CL} run ${GN}pdsadmin help${CL} inside the container"
echo -e "${INFO}${YW}Cloudflare Tunnel:${CL} route ${var_pds_hostname:-<your-domain>} to HTTP://<LXC-IP>:3000"
echo -e "${INFO}${YW}No Caddy or public inbound ports are required on this LXC when using Cloudflare Tunnel."
132 changes: 132 additions & 0 deletions install/blueskypds-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Community Scripts contributor
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
# Source: https://github.com/bluesky-social/pds

source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
if [[ -z "${var_pds_hostname:-}" ]]; then
read -r -p "${TAB3}Public PDS domain (for example pds.example.com): " var_pds_hostname
fi
var_pds_hostname="$(printf '%s' "$var_pds_hostname" | tr '[:upper:]' '[:lower:]')"
if [[ ! "$var_pds_hostname" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$ ]]; then
msg_error "Invalid Bluesky PDS domain. Use a public DNS name such as pds.example.com."
exit 1
fi
msg_info "Installing Bluesky PDS build dependencies"
$STD apt install -y build-essential ca-certificates curl git gpg apt-transport-https libsqlite3-dev libvips-dev openssl pkg-config python3 sqlite3 xxd
msg_ok "Installed Bluesky PDS build dependencies"

NODE_VERSION="24" setup_nodejs
$STD npm install --global pnpm@12.16.1
export PUPPETEER_SKIP_DOWNLOAD=true
fetch_and_deploy_gh_branch "bluesky-pds" "bluesky-social/atproto" "main"
cd /opt/bluesky-pds
msg_info "Building Bluesky PDS from source"
$STD pnpm install --frozen-lockfile --filter 'pds-service...' --shamefully-hoist
$STD pnpm run --recursive --stream --workspace-concurrency 1 --filter 'pds-service...' build --force
$STD pnpm install --prod --filter 'pds-service...' --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false
msg_ok "Built Bluesky PDS"

fetch_and_deploy_gh_release "goat" "bluesky-social/goat" "binary"
msg_info "Creating Bluesky PDS data directory"
install -d -m 0700 /opt/bluesky-pds_data
PDS_ADMIN_PASSWORD="$(openssl rand -hex 24)"
PDS_JWT_SECRET="$(openssl rand -hex 32)"
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="$(openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32)"
cat <<PDS_CONFIG >/opt/bluesky-pds_data/pds.env
PDS_HOSTNAME=${var_pds_hostname}
PDS_JWT_SECRET=${PDS_JWT_SECRET}
PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}
PDS_DATA_DIRECTORY=/opt/bluesky-pds_data
PDS_BLOBSTORE_DISK_LOCATION=/opt/bluesky-pds_data/blocks
PDS_BLOB_UPLOAD_LIMIT=314572800
PDS_DID_PLC_URL=https://plc.directory
PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
PDS_REPORT_SERVICE_URL=https://mod.bsky.app
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
PDS_CRAWLERS=https://bsky.network
PDS_SERVICE_HANDLE_DOMAINS=.${var_pds_hostname}
LOG_ENABLED=true
PDS_RATE_LIMITS_ENABLED=true
PDS_INVITE_REQUIRED=true
PDS_CONFIG
chmod 0600 /opt/bluesky-pds_data/pds.env
msg_ok "Created Bluesky PDS configuration and secrets"

msg_info "Creating Bluesky PDS systemd service"
cat <<'SYSTEMD_UNIT' >/etc/systemd/system/pds.service
[Unit]
Description=Bluesky Personal Data Server
Documentation=https://github.com/bluesky-social/pds
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/bluesky-pds/services/pds
Environment=NODE_ENV=production
Environment=PDS_PORT=3000
Environment=UV_USE_IO_URING=0
EnvironmentFile=/opt/bluesky-pds_data/pds.env
ExecStart=/usr/bin/node --heapsnapshot-signal=SIGUSR2 --enable-source-maps --import=@atproto/pds/telemetry index.ts
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/bluesky-pds_data
[Install]
WantedBy=multi-user.target
SYSTEMD_UNIT
systemctl daemon-reload
systemctl enable -q --now pds
msg_ok "Created Bluesky PDS systemd service"

msg_info "Installing native Bluesky PDS administration command"
cat <<'PDSADMIN' >/usr/local/bin/pdsadmin
#!/usr/bin/env bash
set -Eeuo pipefail
set -a
source /opt/bluesky-pds_data/pds.env
set +a
# Goat rejects loopback/private IPs as an SSRF safeguard; use the public PDS URL.
export PDS_HOST="https://${PDS_HOSTNAME}"
exec goat pds admin "$@"
PDSADMIN
chmod 0755 /usr/local/bin/pdsadmin
msg_ok "Installed pdsadmin command"

msg_info "Waiting for Bluesky PDS health check"
for attempt in {1..30}; do
if curl -fsS http://127.0.0.1:3000/xrpc/_health >/dev/null 2>&1; then
break
fi
sleep 2
done
if ! curl -fsS http://127.0.0.1:3000/xrpc/_health >/dev/null 2>&1; then
msg_error "Bluesky PDS did not become healthy. Check: journalctl -u pds -n 100 --no-pager"
exit 1
fi
msg_ok "Bluesky PDS health check passed"

motd_ssh
customize
cleanup_lxc
msg_ok "Bluesky PDS installation completed"
echo -e "${INFO}${YW}PDS URL:${CL} ${BGN}https://${var_pds_hostname}${CL}"
echo -e "${INFO}${YW}Health check:${CL} ${GN}https://${var_pds_hostname}/xrpc/_health${CL}"
echo -e "${INFO}${YW}PDS administration:${CL} ${GN}pdsadmin help${CL}"
echo -e "${INFO}${YW}Create an account:${CL} ${GN}pdsadmin account create --handle HANDLE.${var_pds_hostname} --email EMAIL --password PASSWORD${CL}"
echo -e "${INFO}${YW}Admin password is stored in:${CL} ${GN}/opt/bluesky-pds_data/pds.env${CL}"
echo -e "${INFO}${YW}Logs:${CL} ${GN}journalctl -u pds -f${CL}"
57 changes: 57 additions & 0 deletions json/bluesky-pds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "Bluesky PDS",
"slug": "bluesky-pds",
"categories": [0],
"date_created": "2026-11-07",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 3000,
"documentation": "https://github.com/bluesky-social/pds",
"website": "https://bsky.social/",
"repository": "https://github.com/bluesky-social/pds",
"architectures": ["amd64", "arm64"],
"platforms": ["pve"],
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/bluesky.webp",
"description": "Host a federated Bluesky Personal Data Server (PDS) and manage accounts on your own domain. This deployment builds and runs the official TypeScript PDS directly in the LXC without Docker or an in-container reverse proxy.",
"install_methods": [
{
"type": "default",
"script": "ct/bluesky-pds.sh",
"config_path": "/opt/bluesky-pds_data/pds.env",
"resources": {
"cpu": 2,
"ram": 4096,
"hdd": 25,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"app_vars": [
{
"name": "var_pds_hostname",
"label": "Public PDS domain",
"type": "text",
"required": true
}
],
"notes": [
{
"text": "Create A records for your PDS domain and its wildcard (*.your-domain) pointing to this LXC before installation.",
"type": "warning"
},
{
"text": "PDS running ob TCP port 3000.",
"type": "warning"
},
{
"text": "This is a source-built, community-maintained native deployment; it is not the upstream Docker installer and needs testing before production use.",
"type": "info"
}
]
}
Loading