#!/usr/bin/env bash set -euo pipefail # ============================ # Bonfire Bare-Metal Installer # Ubuntu 24.04 LTS (Noble) # ============================ echo "=== Bonfire Bare-Metal Installer (Ubuntu 24.04) ===" # ---------- prompts ---------- read -rp "Enter your domain name (FQDN, e.g., example.com): " DOMAIN while [[ -z "${DOMAIN}" ]]; do echo "Domain cannot be empty." read -rp "Enter your domain name: " DOMAIN done read -rp "Contact email for Let's Encrypt (e.g., admin@${DOMAIN}): " EMAIL while [[ -z "${EMAIL}" ]]; do echo "Email cannot be empty." read -rp "Contact email for Let's Encrypt: " EMAIL done read -rp "Bonfire flavour [social|ember|community|cooperation] (default: social): " FLAVOUR FLAVOUR=${FLAVOUR:-social} read -rp "PostgreSQL username (default: bonfire_user): " DB_USER DB_USER=${DB_USER:-bonfire_user} DB_NAME="bonfire" # we'll generate a random DB password DB_PASSWORD="$(openssl rand -base64 24)" # ---------- FQDN validation ---------- FQDN_REGEX='^([a-zA-Z0-9](-*[a-zA-Z0-9])*)(\.([a-zA-Z0-9](-*[a-zA-Z0-9])*))*\.[a-zA-Z]{2,}$' if [[ ! "${DOMAIN}" =~ ${FQDN_REGEX} ]]; then echo "❌ Invalid FQDN: ${DOMAIN}" exit 1 fi # ---------- DNS checks (A/AAAA must match this server) ---------- echo "=== Checking DNS for ${DOMAIN} ===" # public IPv4 SERVER_V4="$(curl -4 -fsS https://api.ipify.org || true)" # public IPv6 (may be empty if host lacks IPv6) SERVER_V6="$(curl -6 -fsS https://api64.ipify.org || true)" # need dig if ! command -v dig >/dev/null 2>&1; then echo "Installing dnsutils for DNS checks..." sudo apt update && sudo apt install -y dnsutils fi DOMAIN_A="$(dig +short A "${DOMAIN}" | head -n1 || true)" DOMAIN_AAAA="$(dig +short AAAA "${DOMAIN}" | head -n1 || true)" echo "Server IPv4: ${SERVER_V4:-}" echo "Server IPv6: ${SERVER_V6:-}" echo "Domain A: ${DOMAIN_A:-}" echo "Domain AAAA: ${DOMAIN_AAAA:-}" MISMATCH=true if [[ -n "${SERVER_V4}" && -n "${DOMAIN_A}" && "${SERVER_V4}" == "${DOMAIN_A}" ]]; then MISMATCH=false fi if [[ -n "${SERVER_V6}" && -n "${DOMAIN_AAAA}" && "${SERVER_V6,,}" == "${DOMAIN_AAAA,,}" ]]; then MISMATCH=false fi if [[ "${MISMATCH}" == "true" ]]; then echo "❌ DNS for ${DOMAIN} does not resolve to this server." echo " Server IPv4: ${SERVER_V4:-}, Domain A: ${DOMAIN_A:-}" echo " Server IPv6: ${SERVER_V6:-}, Domain AAAA: ${DOMAIN_AAAA:-}" echo "➡️ Fix your DNS A/AAAA records, wait for propagation, then re-run." exit 1 fi echo "✅ DNS looks good." # ---------- system prep ---------- echo "=== Updating system packages ===" sudo apt update && sudo apt -y upgrade echo "=== Installing base tools ===" sudo apt install -y \ curl git build-essential ca-certificates jq \ libssl-dev libffi-dev libncurses5-dev \ libwxgtk3.0-gtk3-dev libgl1-mesa-dev libglu1-mesa-dev \ libpng-dev libssh-dev unixodbc-dev pkg-config echo "=== Installing 'just' command runner ===" sudo apt install -y just echo "=== Installing Node.js 20 + Yarn ===" curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs npm install -g yarn # ---------- PostgreSQL 17 + PostGIS ---------- echo "=== Installing PostgreSQL 17 + PostGIS ===" echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt/ noble-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list >/dev/null curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /usr/share/keyrings/pgdg.gpg sudo apt update sudo apt install -y postgresql-17 postgresql-17-postgis-3 echo "=== Creating PostgreSQL role & database ===" sudo -u postgres psql -tAc "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASSWORD}'; END IF; END \$\$;" sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1 || sudo -u postgres createdb -O "${DB_USER}" "${DB_NAME}" sudo -u postgres psql -d "${DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS postgis;" # ---------- asdf + mise (match .tool-versions) ---------- echo "=== Installing asdf (plugins: erlang, elixir) ===" if [[ ! -d "$HOME/.asdf" ]]; then git clone https://github.com/asdf-vm/asdf.git "$HOME/.asdf" --branch v0.14.0 fi grep -q 'asdf.sh' "$HOME/.bashrc" || echo '. "$HOME/.asdf/asdf.sh"' >> "$HOME/.bashrc" # shellcheck source=/dev/null source "$HOME/.asdf/asdf.sh" || true asdf plugin-add erlang || true asdf plugin-add elixir || true echo "=== Installing mise ===" curl -fsSL https://mise.jdx.dev/install.sh | sh grep -q 'mise activate' "$HOME/.bashrc" || echo 'eval "$("$HOME/.local/bin/mise" activate bash)"' >> "$HOME/.bashrc" eval "$("$HOME/.local/bin/mise" activate bash)" || true # ---------- Bonfire clone & tool install ---------- echo "=== Cloning Bonfire repository ===" cd "$HOME" if [[ ! -d "bonfire" ]]; then git clone --depth 1 https://github.com/bonfire-networks/bonfire-app.git bonfire fi cd bonfire echo "=== Installing Erlang/Elixir via mise (from .tool-versions) ===" "$HOME/.local/bin/mise" install # ---------- Elixir bug-window check for Pathex ---------- WITH_PATHEX=1 if [[ -f ".tool-versions" ]]; then ELIXIR_LINE="$(grep -E '^elixir[[:space:]]' .tool-versions || true)" if [[ -n "${ELIXIR_LINE}" ]]; then ELIXIR_VER="$(echo "${ELIXIR_LINE}" | awk '{print $2}')" # strip possible 'ref:' etc; keep x.y.z ELIXIR_VER_CLEAN="$(echo "${ELIXIR_VER}" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' || true)" if [[ -n "${ELIXIR_VER_CLEAN}" ]]; then # compare versions: if >=1.17 and <1.17.3 => WITH_PATHEX=0 vcmp () { printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1; } if [[ "$(vcmp "1.17.0" "${ELIXIR_VER_CLEAN}")" == "1.17.0" && "$(vcmp "${ELIXIR_VER_CLEAN}" "1.17.3")" == "${ELIXIR_VER_CLEAN}" && "${ELIXIR_VER_CLEAN}" != "1.17.3" ]]; then WITH_PATHEX=0 echo "⚠️ Elixir ${ELIXIR_VER_CLEAN} falls in bug window (>=1.17,<1.17.3). Disabling Pathex (WITH_PATHEX=0)." fi fi fi fi # ---------- environment ---------- echo "=== Exporting environment ===" grep -q 'FLAVOUR=' "$HOME/.bashrc" || { echo "export FLAVOUR=${FLAVOUR}" >> "$HOME/.bashrc" echo "export MIX_ENV=prod" >> "$HOME/.bashrc" echo "export WITH_DOCKER=no" >> "$HOME/.bashrc" } export FLAVOUR="${FLAVOUR}" export MIX_ENV=prod export WITH_DOCKER=no echo "=== Initializing config with 'just config' ===" just config echo "=== Writing .env (DB + SECRET_KEY_BASE + WITH_PATHEX=${WITH_PATHEX}) ===" SECRET_KEY_BASE="$(openssl rand -hex 64)" cat > .env </dev/null </dev/null < "${HOME}/bonfire/reload-nginx.sh" <<'EOS' #!/usr/bin/env bash set -e sudo nginx -t sudo systemctl reload nginx echo "Nginx reloaded." EOS chmod +x "${HOME}/bonfire/reload-nginx.sh" cat > "${HOME}/bonfire/bonfire-deploy.sh" <<'EOS' #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" echo "Pulling latest code..." git pull --ff-only echo "Ensuring tool versions are installed (mise)..." ~/.local/bin/mise install echo "Updating prod config..." just setup-prod echo "Building release..." just rel-build echo "Restarting Bonfire..." sudo systemctl restart bonfire echo "Reloading Nginx..." ./reload-nginx.sh echo "Deploy complete." EOS chmod +x "${HOME}/bonfire/bonfire-deploy.sh" # ---------- done ---------- echo echo "=== Bonfire setup complete! 🎉 ===" echo "Site: https://${DOMAIN}" echo echo "Systemd service controls:" echo " sudo systemctl status bonfire" echo " sudo systemctl restart bonfire" echo " sudo systemctl stop bonfire" echo echo "Database credentials:" echo " DB_USER: ${DB_USER}" echo " DB_PASSWORD: ${DB_PASSWORD}" echo " DB_NAME: ${DB_NAME}" echo echo "Deploy updates with:" echo " ${HOME}/bonfire/bonfire-deploy.sh" echo "Reload Nginx with:" echo " ${HOME}/bonfire/reload-nginx.sh" echo echo "Certbot auto-renewal is enabled. To force renew:" echo " sudo certbot renew && sudo systemctl reload nginx"