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