Fork of official Bluesky PDS (Personal Data Server).
at main 448 lines 13 kB view raw
1#!/usr/bin/env bash 2set -o errexit 3set -o nounset 4set -o pipefail 5 6# Disable prompts for apt-get. 7export DEBIAN_FRONTEND="noninteractive" 8 9# System info. 10PLATFORM="$(uname --hardware-platform || true)" 11DISTRIB_CODENAME="$(lsb_release --codename --short || true)" 12DISTRIB_ID="$(lsb_release --id --short | tr '[:upper:]' '[:lower:]' || true)" 13 14# Secure generator commands 15GENERATE_SECURE_SECRET_CMD="openssl rand --hex 16" 16GENERATE_K256_PRIVATE_KEY_CMD="openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32" 17 18# The Docker compose file. 19COMPOSE_URL="https://raw.githubusercontent.com/bluesky-social/pds/main/compose.yaml" 20 21# The pdsadmin script. 22PDSADMIN_URL="https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh" 23 24# System dependencies. 25REQUIRED_SYSTEM_PACKAGES=" 26 ca-certificates 27 curl 28 gnupg 29 jq 30 lsb-release 31 openssl 32 sqlite3 33 xxd 34" 35# Docker packages. 36REQUIRED_DOCKER_PACKAGES=" 37 containerd.io 38 docker-ce 39 docker-ce-cli 40 docker-compose-plugin 41" 42 43PUBLIC_IP="" 44METADATA_URLS=() 45METADATA_URLS+=("http://169.254.169.254/v1/interfaces/0/ipv4/address") # Vultr 46METADATA_URLS+=("http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address") # DigitalOcean 47METADATA_URLS+=("http://169.254.169.254/2021-03-23/meta-data/public-ipv4") # AWS 48METADATA_URLS+=("http://169.254.169.254/hetzner/v1/metadata/public-ipv4") # Hetzner 49 50PDS_DATADIR="${1:-/pds}" 51PDS_HOSTNAME="${2:-}" 52PDS_ADMIN_EMAIL="${3:-}" 53PDS_DID_PLC_URL="https://plc.directory" 54PDS_BSKY_APP_VIEW_URL="https://api.bsky.app" 55PDS_BSKY_APP_VIEW_DID="did:web:api.bsky.app" 56PDS_REPORT_SERVICE_URL="https://mod.bsky.app" 57PDS_REPORT_SERVICE_DID="did:plc:ar7c4by46qjdydhdevvrndac" 58PDS_CRAWLERS="https://bsky.network" 59 60function usage { 61 local error="${1}" 62 cat <<USAGE >&2 63ERROR: ${error} 64Usage: 65sudo bash $0 66 67Please try again. 68USAGE 69 exit 1 70} 71 72function main { 73 # Check that user is root. 74 if [[ "${EUID}" -ne 0 ]]; then 75 usage "This script must be run as root. (e.g. sudo $0)" 76 fi 77 78 # Check for a supported architecture. 79 # If the platform is unknown (not uncommon) then we assume x86_64 80 if [[ "${PLATFORM}" == "unknown" ]]; then 81 PLATFORM="x86_64" 82 fi 83 if [[ "${PLATFORM}" != "x86_64" ]] && [[ "${PLATFORM}" != "aarch64" ]] && [[ "${PLATFORM}" != "arm64" ]]; then 84 usage "Sorry, only x86_64 and aarch64/arm64 are supported. Exiting..." 85 fi 86 87 # Check for a supported distribution. 88 SUPPORTED_OS="false" 89 if [[ "${DISTRIB_ID}" == "ubuntu" ]]; then 90 if [[ "${DISTRIB_CODENAME}" == "focal" ]]; then 91 SUPPORTED_OS="true" 92 echo "* Detected supported distribution Ubuntu 20.04 LTS" 93 elif [[ "${DISTRIB_CODENAME}" == "jammy" ]]; then 94 SUPPORTED_OS="true" 95 echo "* Detected supported distribution Ubuntu 22.04 LTS" 96 elif [[ "${DISTRIB_CODENAME}" == "noble" ]]; then 97 SUPPORTED_OS="true" 98 echo "* Detected supported distribution Ubuntu 24.04 LTS" 99 fi 100 elif [[ "${DISTRIB_ID}" == "debian" ]]; then 101 if [[ "${DISTRIB_CODENAME}" == "bullseye" ]]; then 102 SUPPORTED_OS="true" 103 echo "* Detected supported distribution Debian 11" 104 elif [[ "${DISTRIB_CODENAME}" == "bookworm" ]]; then 105 SUPPORTED_OS="true" 106 echo "* Detected supported distribution Debian 12" 107 elif [[ "${DISTRIB_CODENAME}" == "trixie" ]]; then 108 SUPPORTED_OS="true" 109 echo "* Detected supported distribution Debian 13" 110 fi 111 fi 112 113 if [[ "${SUPPORTED_OS}" != "true" ]]; then 114 echo "Sorry, only Ubuntu 20.04, 22.04, 24.04, and Debian 11, 12, and 13 are supported by this installer. Exiting..." 115 exit 1 116 fi 117 118 # Enforce that the data directory is /pds since we're assuming it for now. 119 # Later we can make this actually configurable. 120 if [[ "${PDS_DATADIR}" != "/pds" ]]; then 121 usage "The data directory must be /pds. Exiting..." 122 fi 123 124 # Check if PDS is already installed. 125 if [[ -e "${PDS_DATADIR}/account.sqlite" ]]; then 126 echo 127 echo "ERROR: pds is already configured in ${PDS_DATADIR}" 128 echo 129 echo "To do a clean re-install:" 130 echo "------------------------------------" 131 echo "1. Stop the service" 132 echo 133 echo " sudo systemctl stop pds" 134 echo 135 echo "2. Delete the data directory" 136 echo 137 echo " sudo rm -rf ${PDS_DATADIR}" 138 echo 139 echo "3. Re-run this installation script" 140 echo 141 echo " sudo bash ${0}" 142 echo 143 echo "For assistance, check https://github.com/bluesky-social/pds" 144 exit 1 145 fi 146 147 # 148 # Attempt to determine server's public IP. 149 # 150 151 # First try using the hostname command, which usually works. 152 if [[ -z "${PUBLIC_IP}" ]]; then 153 PUBLIC_IP=$(hostname --all-ip-addresses | awk '{ print $1 }') 154 fi 155 156 # Prevent any private IP address from being used, since it won't work. 157 if [[ "${PUBLIC_IP}" =~ ^(127\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|192\.168\.) ]]; then 158 PUBLIC_IP="" 159 fi 160 161 # Check the various metadata URLs. 162 if [[ -z "${PUBLIC_IP}" ]]; then 163 for METADATA_URL in "${METADATA_URLS[@]}"; do 164 METADATA_IP="$(timeout 2 curl --silent --show-error "${METADATA_URL}" | head --lines=1 || true)" 165 if [[ "${METADATA_IP}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 166 PUBLIC_IP="${METADATA_IP}" 167 break 168 fi 169 done 170 fi 171 172 if [[ -z "${PUBLIC_IP}" ]]; then 173 PUBLIC_IP="Server's IP" 174 fi 175 176 # 177 # Prompt user for required variables. 178 # 179 if [[ -z "${PDS_HOSTNAME}" ]]; then 180 cat <<INSTALLER_MESSAGE 181--------------------------------------- 182 Add DNS Record for Public IP 183--------------------------------------- 184 185 From your DNS provider's control panel, create the required 186 DNS record with the value of your server's public IP address. 187 188 + Any DNS name that can be resolved on the public internet will work. 189 + Replace example.com below with any valid domain name you control. 190 + A TTL of 600 seconds (10 minutes) is recommended. 191 192 Example DNS record: 193 194 NAME TYPE VALUE 195 ---- ---- ----- 196 example.com A ${PUBLIC_IP:-Server public IP} 197 *.example.com A ${PUBLIC_IP:-Server public IP} 198 199 **IMPORTANT** 200 It's recommended to wait 3-5 minutes after creating a new DNS record 201 before attempting to use it. This will allow time for the DNS record 202 to be fully updated. 203 204INSTALLER_MESSAGE 205 206 if [[ -z "${PDS_HOSTNAME}" ]]; then 207 read -p "Enter your public DNS address (e.g. example.com): " PDS_HOSTNAME 208 fi 209 fi 210 211 if [[ -z "${PDS_HOSTNAME}" ]]; then 212 usage "No public DNS address specified" 213 fi 214 215 if [[ "${PDS_HOSTNAME}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 216 usage "Invalid public DNS address (must not be an IP address)" 217 fi 218 219 # Admin email 220 if [[ -z "${PDS_ADMIN_EMAIL}" ]]; then 221 read -p "Enter an admin email address (e.g. you@example.com): " PDS_ADMIN_EMAIL 222 fi 223 if [[ -z "${PDS_ADMIN_EMAIL}" ]]; then 224 usage "No admin email specified" 225 fi 226 227 # 228 # Install system packages. 229 # 230 if lsof -v >/dev/null 2>&1; then 231 while true; do 232 apt_process_count="$(lsof -n -t /var/cache/apt/archives/lock /var/lib/apt/lists/lock /var/lib/dpkg/lock | wc --lines || true)" 233 if (( apt_process_count == 0 )); then 234 break 235 fi 236 echo "* Waiting for other apt process to complete..." 237 sleep 2 238 done 239 fi 240 241 apt-get update 242 apt-get install --yes ${REQUIRED_SYSTEM_PACKAGES} 243 244 # 245 # Install Docker 246 # 247 if ! docker version >/dev/null 2>&1; then 248 echo "* Installing Docker" 249 mkdir --parents /etc/apt/keyrings 250 251 # Remove the existing file, if it exists, 252 # so there's no prompt on a second run. 253 rm --force /etc/apt/keyrings/docker.gpg 254 curl --fail --silent --show-error --location "https://download.docker.com/linux/${DISTRIB_ID}/gpg" | \ 255 gpg --dearmor --output /etc/apt/keyrings/docker.gpg 256 257 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${DISTRIB_ID} ${DISTRIB_CODENAME} stable" >/etc/apt/sources.list.d/docker.list 258 259 apt-get update 260 apt-get install --yes ${REQUIRED_DOCKER_PACKAGES} 261 fi 262 263 # 264 # Configure the Docker daemon so that logs don't fill up the disk. 265 # 266 if ! [[ -e /etc/docker/daemon.json ]]; then 267 echo "* Configuring Docker daemon" 268 cat <<'DOCKERD_CONFIG' >/etc/docker/daemon.json 269{ 270 "log-driver": "json-file", 271 "log-opts": { 272 "max-size": "500m", 273 "max-file": "4" 274 } 275} 276DOCKERD_CONFIG 277 systemctl restart docker 278 else 279 echo "* Docker daemon already configured! Ensure log rotation is enabled." 280 fi 281 282 # 283 # Create data directory. 284 # 285 if ! [[ -d "${PDS_DATADIR}" ]]; then 286 echo "* Creating data directory ${PDS_DATADIR}" 287 mkdir --parents "${PDS_DATADIR}" 288 fi 289 chmod 700 "${PDS_DATADIR}" 290 291 # 292 # Configure Caddy 293 # 294 if ! [[ -d "${PDS_DATADIR}/caddy/data" ]]; then 295 echo "* Creating Caddy data directory" 296 mkdir --parents "${PDS_DATADIR}/caddy/data" 297 fi 298 if ! [[ -d "${PDS_DATADIR}/caddy/etc/caddy" ]]; then 299 echo "* Creating Caddy config directory" 300 mkdir --parents "${PDS_DATADIR}/caddy/etc/caddy" 301 fi 302 303 echo "* Creating Caddy config file" 304 cat <<CADDYFILE >"${PDS_DATADIR}/caddy/etc/caddy/Caddyfile" 305{ 306 email ${PDS_ADMIN_EMAIL} 307 on_demand_tls { 308 ask http://localhost:3000/tls-check 309 } 310} 311 312*.${PDS_HOSTNAME}, ${PDS_HOSTNAME} { 313 tls { 314 on_demand 315 } 316 reverse_proxy http://localhost:3000 317} 318CADDYFILE 319 320 # 321 # Create the PDS env config 322 # 323 # Created here so that we can use it later in multiple places. 324 PDS_ADMIN_PASSWORD=$(eval "${GENERATE_SECURE_SECRET_CMD}") 325 cat <<PDS_CONFIG >"${PDS_DATADIR}/pds.env" 326PDS_HOSTNAME=${PDS_HOSTNAME} 327PDS_JWT_SECRET=$(eval "${GENERATE_SECURE_SECRET_CMD}") 328PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD} 329PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}") 330PDS_DATA_DIRECTORY=${PDS_DATADIR} 331PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR}/blocks 332PDS_BLOB_UPLOAD_LIMIT=104857600 333PDS_DID_PLC_URL=${PDS_DID_PLC_URL} 334PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL} 335PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID} 336PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL} 337PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID} 338PDS_CRAWLERS=${PDS_CRAWLERS} 339LOG_ENABLED=true 340PDS_RATE_LIMITS_ENABLED=true 341PDS_CONFIG 342 343 # 344 # Download and install pds launcher. 345 # 346 echo "* Downloading PDS compose file" 347 curl \ 348 --silent \ 349 --show-error \ 350 --fail \ 351 --output "${PDS_DATADIR}/compose.yaml" \ 352 "${COMPOSE_URL}" 353 354 # Replace the /pds paths with the ${PDS_DATADIR} path. 355 sed --in-place "s|/pds|${PDS_DATADIR}|g" "${PDS_DATADIR}/compose.yaml" 356 357 # 358 # Create the systemd service. 359 # 360 echo "* Starting the pds systemd service" 361 cat <<SYSTEMD_UNIT_FILE >/etc/systemd/system/pds.service 362[Unit] 363Description=Bluesky PDS Service 364Documentation=https://github.com/bluesky-social/pds 365Requires=docker.service 366After=docker.service 367 368[Service] 369Type=oneshot 370RemainAfterExit=yes 371WorkingDirectory=${PDS_DATADIR} 372ExecStart=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml up --detach 373ExecStop=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml down 374 375[Install] 376WantedBy=default.target 377SYSTEMD_UNIT_FILE 378 379 systemctl daemon-reload 380 systemctl enable pds 381 systemctl restart pds 382 383 # Enable firewall access if ufw is in use. 384 if ufw status >/dev/null 2>&1; then 385 if ! ufw status | grep --quiet '^80[/ ]'; then 386 echo "* Enabling access on TCP port 80 using ufw" 387 ufw allow 80/tcp >/dev/null 388 fi 389 if ! ufw status | grep --quiet '^443[/ ]'; then 390 echo "* Enabling access on TCP port 443 using ufw" 391 ufw allow 443/tcp >/dev/null 392 fi 393 fi 394 395 # 396 # Download and install pdsadmin. 397 # 398 echo "* Downloading pdsadmin" 399 curl \ 400 --silent \ 401 --show-error \ 402 --fail \ 403 --output "/usr/local/bin/pdsadmin" \ 404 "${PDSADMIN_URL}" 405 chmod +x /usr/local/bin/pdsadmin 406 407 cat <<INSTALLER_MESSAGE 408======================================================================== 409PDS installation successful! 410------------------------------------------------------------------------ 411 412Check service status : sudo systemctl status pds 413Watch service logs : sudo docker logs -f pds 414Backup service data : ${PDS_DATADIR} 415PDS Admin command : pdsadmin 416 417Required Firewall Ports 418------------------------------------------------------------------------ 419Service Direction Port Protocol Source 420------- --------- ---- -------- ---------------------- 421HTTP TLS verification Inbound 80 TCP Any 422HTTP Control Panel Inbound 443 TCP Any 423 424Required DNS entries 425------------------------------------------------------------------------ 426Name Type Value 427------- --------- --------------- 428${PDS_HOSTNAME} A ${PUBLIC_IP} 429*.${PDS_HOSTNAME} A ${PUBLIC_IP} 430 431Detected public IP of this server: ${PUBLIC_IP} 432 433To see pdsadmin commands, run "pdsadmin help" 434 435======================================================================== 436INSTALLER_MESSAGE 437 438 CREATE_ACCOUNT_PROMPT="" 439 read -p "Create a PDS user account? (y/N): " CREATE_ACCOUNT_PROMPT 440 441 if [[ "${CREATE_ACCOUNT_PROMPT}" =~ ^[Yy] ]]; then 442 pdsadmin account create 443 fi 444 445} 446 447# Run main function. 448main