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# 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}" == "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
201INSTALLER_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}
280DOCKERD_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}
322CADDYFILE
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"
330PDS_HOSTNAME=${PDS_HOSTNAME}
331PDS_JWT_SECRET=$(eval "${GENERATE_SECURE_SECRET_CMD}")
332PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
333PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}")
334PDS_DATA_DIRECTORY=${PDS_DATADIR}
335PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR}/blocks
336PDS_BLOB_UPLOAD_LIMIT=52428800
337PDS_DID_PLC_URL=${PDS_DID_PLC_URL}
338PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL}
339PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID}
340PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL}
341PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID}
342PDS_CRAWLERS=${PDS_CRAWLERS}
343LOG_ENABLED=true
344PDS_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]
366Description=Bluesky PDS Service
367Documentation=https://github.com/bluesky-social/pds
368Requires=docker.service
369After=docker.service
370
371[Service]
372Type=oneshot
373RemainAfterExit=yes
374WorkingDirectory=${PDS_DATADIR}
375ExecStart=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml up --detach
376ExecStop=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml down
377
378[Install]
379WantedBy=default.target
380SYSTEMD_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========================================================================
412PDS installation successful!
413------------------------------------------------------------------------
414
415Check service status : sudo systemctl status pds
416Watch service logs : sudo docker logs -f pds
417Backup service data : ${PDS_DATADIR}
418PDS Admin command : pdsadmin
419
420Required Firewall Ports
421------------------------------------------------------------------------
422Service Direction Port Protocol Source
423------- --------- ---- -------- ----------------------
424HTTP TLS verification Inbound 80 TCP Any
425HTTP Control Panel Inbound 443 TCP Any
426
427Required DNS entries
428------------------------------------------------------------------------
429Name Type Value
430------- --------- ---------------
431${PDS_HOSTNAME} A ${PUBLIC_IP}
432*.${PDS_HOSTNAME} A ${PUBLIC_IP}
433
434Detected public IP of this server: ${PUBLIC_IP}
435
436To see pdsadmin commands, run "pdsadmin help"
437
438========================================================================
439INSTALLER_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.
451main