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 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
160INSTALLER_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}
262DOCKERD_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}
301CADDYFILE
302
303 #
304 # Create the PDS env config
305 #
306 cat <<PDS_CONFIG >"${PDS_DATADIR}/pds.env"
307PDS_HOSTNAME=${PDS_HOSTNAME}
308PDS_JWT_SECRET=$(eval "${GENERATE_SECURE_SECRET_CMD}")
309PDS_ADMIN_PASSWORD=$(eval "${GENERATE_SECURE_SECRET_CMD}")
310PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}")
311PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}")
312PDS_DB_SQLITE_LOCATION=${PDS_DATADIR}/pds.sqlite
313PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR}/blocks
314PDS_DID_PLC_URL=${PDS_DID_PLC_URL}
315PDS_BSKY_APP_VIEW_ENDPOINT=${PDS_BSKY_APP_VIEW_ENDPOINT}
316PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID}
317PDS_CRAWLERS=${PDS_CRAWLERS}
318PDS_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]
340Description=Bluesky PDS Service
341Documentation=https://github.com/bluesky-social/pds
342Requires=docker.service
343After=docker.service
344
345[Service]
346Type=oneshot
347RemainAfterExit=yes
348WorkingDirectory=${PDS_DATADIR}
349ExecStart=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml up --detach
350ExecStop=/usr/bin/docker compose --file ${PDS_DATADIR}/compose.yaml down
351
352[Install]
353WantedBy=default.target
354SYSTEMD_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========================================================================
374PDS installation successful!
375------------------------------------------------------------------------
376
377Check service status : sudo systemctl status pds
378Watch service logs : sudo docker logs -f pds
379Backup service data : ${PDS_DATADIR}
380
381Required Firewall Ports
382------------------------------------------------------------------------
383Service Direction Port Protocol Source
384------- --------- ---- -------- ----------------------
385HTTP TLS verification Inbound 80 TCP Any
386HTTP Control Panel Inbound 443 TCP Any
387
388Required DNS entries
389------------------------------------------------------------------------
390Name Type Value
391------- --------- ---------------
392${PDS_HOSTNAME} A ${PUBLIC_IP}
393*.${PDS_HOSTNAME} A ${PUBLIC_IP}
394
395Detected public IP of this server: ${PUBLIC_IP}
396
397========================================================================
398INSTALLER_MESSAGE
399}
400
401# Run main function.
402main