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