diff --git a/ct/bluesky-pds.sh b/ct/bluesky-pds.sh new file mode 100755 index 0000000000..719db0c44f --- /dev/null +++ b/ct/bluesky-pds.sh @@ -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:-}${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:-} to HTTP://:3000" +echo -e "${INFO}${YW}No Caddy or public inbound ports are required on this LXC when using Cloudflare Tunnel." diff --git a/install/blueskypds-install.sh b/install/blueskypds-install.sh new file mode 100755 index 0000000000..04c6b23fd2 --- /dev/null +++ b/install/blueskypds-install.sh @@ -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 </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}" diff --git a/json/bluesky-pds.json b/json/bluesky-pds.json new file mode 100644 index 0000000000..71f1e1397f --- /dev/null +++ b/json/bluesky-pds.json @@ -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" + } + ] +}