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