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