Skip to content

Instantly share code, notes, and snippets.

@sgpthomas
Last active November 26, 2024 16:24
Show Gist options
  • Select an option

  • Save sgpthomas/a709db720f2e1ef4860c79354af71b80 to your computer and use it in GitHub Desktop.

Select an option

Save sgpthomas/a709db720f2e1ef4860c79354af71b80 to your computer and use it in GitHub Desktop.
version: '3.9'
services:
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:0.4
network_mode: host
restart: unless-stopped
volumes:
- type: bind
source: /pds
target: /pds
env_file:
- /pds/pds.env
watchtower:
container_name: watchtower
image: containrrr/watchtower:latest
network_mode: host
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
restart: unless-stopped
environment:
WATCHTOWER_CLEANUP: true
WATCHTOWER_SCHEDULE: "@midnight"
#!/bin/bash
set -o errexit
set -o nounset
set -o pipefail
# 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"
COMPOSE_NO_CADDY_URL="https://gist.githubusercontent.com/sgpthomas/a709db720f2e1ef4860c79354af71b80/raw/78da6f9e10837c41ea82c815ff5f1904d95c1de1/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
# 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. you@example.com): " 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. you@example.com): " PDS_ADMIN_EMAIL
fi
if [[ -z "${PDS_ADMIN_EMAIL}" ]]; then
usage "No admin email specified"
fi
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
#
# ask if you want to configure caddy
CONFIGURE_CADDY_PROMPT=""
read -p "Configure Caddy automatically? (y/N): " CONFIGURE_CADDY_PROMPT
if [[ "${CONFIGURE_CADDY_PROMPT}" =~ ^[Yy] ]]; then
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
fi
#
# 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.
#
if [[ "${CONFIGURE_CADDY_PROMPT}" =~ ^[Yy] ]]; then
echo "* Downloading PDS compose file with caddy service"
curl \
--silent \
--show-error \
--fail \
--output "${PDS_DATADIR}/compose.yaml" \
"${COMPOSE_URL}"
else
echo "* Downloading PDS compose file without caddy service"
curl \
--silent \
--show-error \
--fail \
--output "${PDS_DATADIR}/compose.yaml" \
"${COMPOSE_NO_CADDY_URL}"
fi
# 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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment