Mirror from bluesky-social/pds

add installer script

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