#!/usr/bin/env bash # bonfire_turnkey_asdf_fixed.sh # All-in-one Bonfire installer using ASDF-managed Erlang/Elixir/Node (fixed: early curl/gnupg + pgdg key) # Target: Ubuntu 24.04 / Debian 12+ # # Run as root: # sudo bash ./bonfire_turnkey_asdf_fixed.sh # # Notes: # - Building Erlang/Elixir with asdf may take a long time on first run. # - By default the script checks DNS A/AAAA for the provided domain; use --skip-dns-check to bypass. # - This script attempts to be idempotent where reasonable. set -euo pipefail ### --------------------------- ### Configuration defaults ### --------------------------- APP_USER="bonfire" APP_HOME="/opt/${APP_USER}" APP_DIR="${APP_HOME}/app" APP_PORT="4000" PG_DB="bonfire" PG_USER_DEFAULT="bonfire_user" PG_VER_PREFERRED="17" ASDF_VERSION="v0.14.0" KERL_CONFIGURE_OPTIONS="--without-javac --without-wx" SKIP_DNS_CHECK=0 ### --------------------------- ### Helpers ### --------------------------- log(){ printf "\n\033[1;36m==> %s\033[0m\n" "$*"; } info(){ printf "\033[1;32m[info]\033[0m %s\n" "$*"; } warn(){ printf "\033[1;33m[warn]\033[0m %s\n" "$*"; } err(){ printf "\033[1;31m[error]\033[0m %s\n" "$*"; } die(){ err "$*"; exit 1; } require_root(){ if [[ $EUID -ne 0 ]]; then die "This script must be run as root (sudo)." fi } require_root ### --------------------------- ### CLI args ### --------------------------- while [[ $# -gt 0 ]]; do case "$1" in --skip-dns-check) SKIP_DNS_CHECK=1; shift;; *) die "Unknown argument: $1";; esac done ### --------------------------- ### Interactive prompts ### --------------------------- read -rp "Enter your domain (FQDN, e.g. bonfire.example.com): " DOMAIN while [[ -z "$DOMAIN" ]]; do read -rp "Domain cannot be empty. Enter domain: " DOMAIN; done read -rp "Contact email for Let's Encrypt (admin@${DOMAIN}): " EMAIL while [[ -z "$EMAIL" ]]; do read -rp "Email cannot be empty. Enter contact email: " EMAIL; done read -rp "Bonfire flavour [social|ember|community|cooperation] (default: social): " FLAVOUR FLAVOUR=${FLAVOUR:-social} read -rp "PostgreSQL role to create (default: ${PG_USER_DEFAULT}): " PG_USER PG_USER=${PG_USER:-$PG_USER_DEFAULT} PG_PASS="$(openssl rand -base64 24)" log "Inputs summary:" info "Domain: ${DOMAIN}" info "Email: ${EMAIL}" info "Flavour: ${FLAVOUR}" info "Postgres role: ${PG_USER} (password auto-generated)" ### --------------------------- ### Validate FQDN (basic) ### --------------------------- 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 die "Invalid FQDN: $DOMAIN" fi ### --------------------------- ### System update & ensure curl/gnupg early ### --------------------------- log "APT update & ensure curl/gnupg are present (required early for repos)" export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get upgrade -y # Ensure curl / gnupg are present early for repo keys apt-get install -y --no-install-recommends curl wget gnupg gpg-agent dirmngr lsb-release apt-transport-https ca-certificates || true ### --------------------------- ### Install build deps (may include some heavy packages needed for asdf builds) ### --------------------------- log "Installing build dependencies (Erlang/Elixir builds need several packages)" apt-get install -y --no-install-recommends \ build-essential automake autoconf m4 libncurses5-dev libncursesw5-dev \ libssl-dev libreadline-dev zlib1g-dev pkg-config unzip \ libxml2-utils xsltproc fop libxslt1.1 libxslt1-dev \ libwxgtk-webview3.2-dev libglu1-mesa-dev libgl1-mesa-dev \ libpng-dev libssh-dev unixodbc-dev default-jdk rsync jq dnsutils git || true # optional: try to install just if in apt repos if ! command -v just >/dev/null 2>&1; then if apt-cache show just >/dev/null 2>&1; then apt-get install -y just || true fi fi ### --------------------------- ### PostgreSQL repository key setup (PGDG) - idempotent ### --------------------------- # ------------------------- # PGDG key + repo (idempotent, modern Ubuntu/Debian) # ------------------------- PG_KEY_URL="https://www.postgresql.org/media/keys/ACCC4CF8.asc" TRUSTED_KEY="/etc/apt/trusted.gpg.d/postgresql.gpg" CODENAME="$(lsb_release -cs || echo noble)" LIST_FILE="/etc/apt/sources.list.d/pgdg.list" # ensure curl & gpg are present apt-get update -y apt-get install -y --no-install-recommends curl gnupg lsb-release apt-transport-https ca-certificates || true # import/dearmor the key if missing if [[ ! -f "${TRUSTED_KEY}" ]]; then info "Importing PostgreSQL signing key to ${TRUSTED_KEY}..." curl -fsSL "${PG_KEY_URL}" | gpg --dearmor -o "${TRUSTED_KEY}" \ || { rm -f "${TRUSTED_KEY}"; die "Failed to fetch/dearmor PGDG key"; } chmod 644 "${TRUSTED_KEY}" chown root:root "${TRUSTED_KEY}" else info "PGDG key already present at ${TRUSTED_KEY}; skipping import." fi # write the repo list (idempotent) echo "deb http://apt.postgresql.org/pub/repos/apt ${CODENAME}-pgdg main" > "${LIST_FILE}" chmod 644 "${LIST_FILE}" info "PGDG APT entry written to ${LIST_FILE}" # update package lists apt-get update -y || die "apt-get update failed after adding PGDG repo" # ------------------------- ### --------------------------- ### Install PostgreSQL + PostGIS ### --------------------------- log "Installing PostgreSQL and PostGIS (attempting preferred ${PG_VER_PREFERRED})" if apt-cache policy "postgresql-${PG_VER_PREFERRED}" | grep -q Candidate; then apt-get install -y "postgresql-${PG_VER_PREFERRED}" "postgresql-${PG_VER_PREFERRED}-postgis-3" postgis || true else apt-get install -y postgresql postgis postgresql-contrib || true fi systemctl enable --now postgresql || true ### --------------------------- ### Set locale to UTF-8 (avoid Elixir latin1 warning) ### --------------------------- log "Ensuring UTF-8 locale" apt-get install -y locales || true if ! grep -q "en_US.UTF-8 UTF-8" /etc/locale.gen 2>/dev/null; then echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen fi locale-gen en_US.UTF-8 >/dev/null || true update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 export ELIXIR_ERL_OPTIONS="+fnu" ### --------------------------- ### Create bonfire user and set ownership ### --------------------------- log "Creating system user '${APP_USER}' and preparing ${APP_HOME}" if id -u "${APP_USER}" >/dev/null 2>&1; then info "User ${APP_USER} already exists" else adduser --system --group --home "${APP_HOME}" --shell /bin/bash "${APP_USER}" || true fi mkdir -p "${APP_HOME}" chown -R "${APP_USER}:${APP_USER}" "${APP_HOME}" chmod 750 "${APP_HOME}" # limited sudo for helper scripts (reload nginx, systemctl) echo "${APP_USER} ALL=(ALL) NOPASSWD:/usr/sbin/nginx,/bin/systemctl" > "/etc/sudoers.d/${APP_USER}" chmod 0440 "/etc/sudoers.d/${APP_USER}" ### --------------------------- ### Install ASDF for bonfire user ### --------------------------- log "Installing asdf for ${APP_USER} if missing" if [[ ! -d "${APP_HOME}/.asdf" ]]; then sudo -u "${APP_USER}" bash -lc "git clone https://github.com/asdf-vm/asdf.git ${APP_HOME}/.asdf --branch ${ASDF_VERSION}" fi # ensure .bashrc loads asdf if ! sudo -u "${APP_USER}" bash -lc "grep -q 'asdf.sh' ${APP_HOME}/.bashrc" >/dev/null 2>&1; then cat >> "${APP_HOME}/.bashrc" <<'ASDFRC' # ASDF . "$HOME/.asdf/asdf.sh" . "$HOME/.asdf/completions/asdf.bash" ASDFRC chown "${APP_USER}:${APP_USER}" "${APP_HOME}/.bashrc" fi # create local bin dir (nodejs plugin helper uses it) sudo -u "${APP_USER}" bash -lc "mkdir -p ${APP_HOME}/.local/bin && chmod 755 ${APP_HOME}/.local/bin" ### --------------------------- ### Clone Bonfire repository ### --------------------------- log "Cloning Bonfire repository into ${APP_DIR}" if [[ ! -d "${APP_DIR}" ]]; then sudo -u "${APP_USER}" git clone --depth 1 https://github.com/bonfire-networks/bonfire-app.git "${APP_DIR}" else info "Repository already present; updating..." sudo -u "${APP_USER}" bash -lc "cd '${APP_DIR}' && git fetch --depth 1 origin && git reset --hard origin/HEAD" fi ### --------------------------- ### Install ASDF plugins & runtimes as bonfire user ### --------------------------- log "Installing ASDF plugins (erlang, elixir, nodejs) and runtimes (may be slow)" sudo -u "${APP_USER}" bash -lc " set -euo pipefail export HOME='${APP_HOME}' . \"\$HOME/.asdf/asdf.sh\" asdf plugin add erlang || true asdf plugin add elixir || true asdf plugin add nodejs || true # nodejs key import helper (if present) if [[ -f \"\$HOME/.asdf/plugins/nodejs/bin/import-release-team-keyring\" ]]; then bash \"\$HOME/.asdf/plugins/nodejs/bin/import-release-team-keyring\" || true fi cd '${APP_DIR}' if [[ -f .tool-versions ]]; then export KERL_CONFIGURE_OPTIONS=\"${KERL_CONFIGURE_OPTIONS}\" asdf install || true else export KERL_CONFIGURE_OPTIONS=\"${KERL_CONFIGURE_OPTIONS}\" EL_VER=\$(asdf list-all elixir | tail -n1 || true) ERL_VER=\$(asdf list-all erlang | tail -n1 || true) NODE_VER=\$(asdf list-all nodejs | tail -n1 || true) if [[ -n \"\$ERL_VER\" ]]; then asdf install erlang \"\$ERL_VER\" || true; asdf global erlang \"\$ERL_VER\"; fi if [[ -n \"\$EL_VER\" ]]; then asdf install elixir \"\$EL_VER\" || true; asdf global elixir \"\$EL_VER\"; fi if [[ -n \"\$NODE_VER\" ]]; then asdf install nodejs \"\$NODE_VER\" || true; asdf global nodejs \"\$NODE_VER\"; fi fi mix local.hex --force || true mix local.rebar --force || true echo '[info] ASDF runtimes installed (or attempted).' " ### --------------------------- ### Verify mix is available ### --------------------------- if ! sudo -u "${APP_USER}" bash -lc ". ${APP_HOME}/.asdf/asdf.sh && command -v mix >/dev/null 2>&1"; then die "mix not available for ${APP_USER}. Check ASDF install logs above." fi ### --------------------------- ### DNS validation (unless skipped) ### --------------------------- if [[ "${SKIP_DNS_CHECK}" -eq 0 ]]; then log "Checking DNS A/AAAA records for ${DOMAIN}" SERVER_V4="$(curl -4 -fsS https://api.ipify.org || true)" SERVER_V6="$(curl -6 -fsS https://api64.ipify.org || true)" DOMAIN_A="$(dig +short A "${DOMAIN}" | head -n1 || true)" DOMAIN_AAAA="$(dig +short AAAA "${DOMAIN}" | head -n1 || true)" echo " Server IPv4: ${SERVER_V4:-} Domain A: ${DOMAIN_A:-}" echo " Server IPv6: ${SERVER_V6:-} Domain AAAA: ${DOMAIN_AAAA:-}" DNS_OK=false if [[ -n "${SERVER_V4}" && "${SERVER_V4}" == "${DOMAIN_A}" ]]; then DNS_OK=true; fi if [[ -n "${SERVER_V6}" && -n "${DOMAIN_AAAA}" && "${SERVER_V6,,}" == "${DOMAIN_AAAA,,}" ]]; then DNS_OK=true; fi if [[ "${DNS_OK}" != "true" ]]; then die "DNS for ${DOMAIN} does not point to this server's public IP. Fix DNS or re-run with --skip-dns-check." fi fi ### --------------------------- ### PostgreSQL role / DB / extensions ### --------------------------- log "Configuring PostgreSQL role/database and enabling PostGIS & citext" sudo -u postgres psql -v ON_ERROR_STOP=1 </dev/null 2>&1; then just config || true fi SECRET_KEY_BASE=\$(openssl rand -hex 64) SIGNING_SALT=\$(openssl rand -hex 16) ENCRYPTION_SALT=\$(openssl rand -hex 16) MEILI_MASTER_KEY=\$(openssl rand -hex 32) cat > .env </dev/null 2>&1; then ELV=\$(elixir -v | sed -n 's/.*Elixir \\([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\).*/\\1/p' || true) if printf '%s\n' 1.17.0 1.17.1 1.17.2 | grep -qx \"\$ELV\"; then sed -i 's/^WITH_PATHEX=.*/WITH_PATHEX=0/' .env || true echo '[warn] Elixir in bug-window; disabled WITH_PATHEX in .env' fi fi mix local.hex --force || true mix local.rebar --force || true mix deps.get --only prod || mix deps.get || true # Assets if [[ -d assets ]]; then cd assets if command -v yarn >/dev/null 2>&1; then yarn install --frozen-lockfile || yarn install || true NODE_ENV=production yarn build || true else npm ci || npm install || true NODE_ENV=production npm run build || true fi cd - fi if command -v just >/dev/null 2>&1; then just setup-prod || true just rel-build || true else MIX_ENV=prod mix compile MIX_ENV=prod mix assets.deploy || true MIX_ENV=prod mix ecto.setup || true MIX_ENV=prod mix release fi echo '[info] Bonfire build attempted (see output above).' " # verify release binary if [[ ! -x "${APP_DIR}/_build/prod/rel/bonfire/bin/bonfire" ]]; then die "Release binary missing at ${APP_DIR}/_build/prod/rel/bonfire/bin/bonfire — check the build output above." fi ### --------------------------- ### Nginx config + certbot ### --------------------------- log "Writing nginx site config and reloading" SITE_AVAIL="/etc/nginx/sites-available/bonfire" SITE_LINK="/etc/nginx/sites-enabled/bonfire" APP_UPSTREAM="http://127.0.0.1:${APP_PORT}" cat > "${SITE_AVAIL}" < "${SERVICE_PATH}" < '${APP_DIR}/reload-nginx.sh' <<'EOS' #!/usr/bin/env bash set -e sudo nginx -t sudo systemctl reload nginx echo 'Nginx reloaded.' EOS chmod +x '${APP_DIR}/reload-nginx.sh' || true" sudo -u "${APP_USER}" bash -lc "cat > '${APP_DIR}/bonfire-deploy.sh' <<'EOS' #!/usr/bin/env bash set -euo pipefail cd \"\$(dirname \"\$0\")\" . \"\$HOME/.asdf/asdf.sh\" 2>/dev/null || true echo 'Pulling latest code...' git pull --ff-only || true echo 'Running production setup and rebuild...' just setup-prod || true just rel-build || true echo 'Restarting service...' sudo systemctl restart bonfire || true echo 'Reloading nginx...' ./reload-nginx.sh || true echo 'Deploy complete.' EOS chmod +x '${APP_DIR}/bonfire-deploy.sh' || true" ### --------------------------- ### Final summary ### --------------------------- log "Done (or attempted). Summary:" cat <