bonfirestarter/bonfire_turnkey.sh

317 lines
9.9 KiB
Bash

#!/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:-<none>}"
echo "Server IPv6: ${SERVER_V6:-<none>}"
echo "Domain A: ${DOMAIN_A:-<none>}"
echo "Domain AAAA: ${DOMAIN_AAAA:-<none>}"
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:-<none>}, Domain A: ${DOMAIN_A:-<none>}"
echo " Server IPv6: ${SERVER_V6:-<none>}, Domain AAAA: ${DOMAIN_AAAA:-<none>}"
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 <<EOF
DATABASE_URL=ecto://${DB_USER}:${DB_PASSWORD}@localhost/${DB_NAME}
SECRET_KEY_BASE=${SECRET_KEY_BASE}
WITH_PATHEX=${WITH_PATHEX}
# PORT can be set (defaults to 4000)
# PORT=4000
EOF
# ---------- build release ----------
echo "=== Running 'just setup-prod' and 'just rel-build' ==="
just setup-prod
just rel-build
# ---------- Nginx + Let's Encrypt ----------
echo "=== Installing Nginx + Certbot ==="
sudo apt install -y nginx certbot python3-certbot-nginx
SITE_AVAIL="/etc/nginx/sites-available/bonfire"
SITE_LINK="/etc/nginx/sites-enabled/bonfire"
APP_UPSTREAM="http://127.0.0.1:4000"
echo "=== Writing Nginx site config ==="
sudo tee "${SITE_AVAIL}" >/dev/null <<NGINX
server {
listen 80;
server_name ${DOMAIN};
# Proxy all requests to Phoenix (serves static as well)
location / {
proxy_pass ${APP_UPSTREAM};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}
NGINX
sudo ln -sf "${SITE_AVAIL}" "${SITE_LINK}"
sudo nginx -t
sudo systemctl restart nginx
echo "=== Obtaining Let's Encrypt certificate ==="
sudo certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${EMAIL}"
echo "=== Enabling certbot auto-renew (systemd timer) ==="
sudo systemctl enable --now certbot.timer
# ---------- systemd service ----------
echo "=== Creating systemd service 'bonfire.service' ==="
SERVICE_FILE="/etc/systemd/system/bonfire.service"
RUN_USER="$(id -un)"
RUN_GROUP="$(id -gn)"
sudo tee "${SERVICE_FILE}" >/dev/null <<SYSTEMD
[Unit]
Description=Bonfire App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${RUN_USER}
Group=${RUN_GROUP}
WorkingDirectory=${HOME}/bonfire
EnvironmentFile=${HOME}/bonfire/.env
ExecStart=${HOME}/bonfire/_build/prod/rel/bonfire/bin/bonfire start daemon
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SYSTEMD
sudo systemctl daemon-reload
sudo systemctl enable --now bonfire
# ---------- helper scripts ----------
echo "=== Creating helper scripts ==="
cat > "${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"