Changing approach

This commit is contained in:
CSnap 2025-08-24 19:40:09 -04:00
parent 7325976ebd
commit 4fa50fd49e
1 changed files with 463 additions and 242 deletions

View File

@ -1,207 +1,421 @@
#!/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
# ============================
# Bonfire Bare-Metal Installer
# Ubuntu 24.04 LTS (Noble)
# ============================
### ---------------------------
### 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
echo "=== Bonfire Bare-Metal Installer (Ubuntu 24.04) ==="
### ---------------------------
### 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; }
# ---------- 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
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
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
### ---------------------------
### 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 username (default: bonfire_user): " DB_USER
DB_USER=${DB_USER:-bonfire_user}
DB_NAME="bonfire"
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)"
# we'll generate a random DB password
DB_PASSWORD="$(openssl rand -base64 24)"
log "Inputs summary:"
info "Domain: ${DOMAIN}"
info "Email: ${EMAIL}"
info "Flavour: ${FLAVOUR}"
info "Postgres role: ${PG_USER} (password auto-generated)"
# ---------- FQDN validation ----------
### ---------------------------
### 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
echo "❌ Invalid FQDN: ${DOMAIN}"
exit 1
if [[ ! "$DOMAIN" =~ $FQDN_REGEX ]]; then
die "Invalid FQDN: $DOMAIN"
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)"
### ---------------------------
### 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
# 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
# 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
DOMAIN_A="$(dig +short A "${DOMAIN}" | head -n1 || true)"
DOMAIN_AAAA="$(dig +short AAAA "${DOMAIN}" | head -n1 || true)"
### ---------------------------
### 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"
echo "Server IPv4: ${SERVER_V4:-<none>}"
echo "Server IPv6: ${SERVER_V6:-<none>}"
echo "Domain A: ${DOMAIN_A:-<none>}"
echo "Domain AAAA: ${DOMAIN_AAAA:-<none>}"
# 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
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
# 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
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
# 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
echo "✅ DNS looks good."
# 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
# ---------- system prep ----------
echo "=== Updating system packages ==="
sudo apt update && sudo apt -y upgrade
# 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"
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
### ---------------------------
### 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
echo "=== Installing 'just' command runner ==="
sudo apt install -y just
### ---------------------------
### 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\"
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
asdf plugin add erlang || true
asdf plugin add elixir || true
asdf plugin add nodejs || true
# ---------- 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
# 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
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}';
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:-<none>} Domain A: ${DOMAIN_A:-<none>}"
echo " Server IPv6: ${SERVER_V6:-<none>} Domain AAAA: ${DOMAIN_AAAA:-<none>}"
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 <<PSQL || die "postgres setup failed"
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${PG_USER}') THEN
CREATE ROLE ${PG_USER} LOGIN PASSWORD '${PG_PASS}';
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;"
END
\$\$;
PSQL
# ---------- 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
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${PG_DB}'" | grep -q 1; then
sudo -u postgres createdb -O "${PG_USER}" "${PG_DB}"
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}"
sudo -u postgres psql -d "${PG_DB}" -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS postgis;" || die "Failed enabling postgis"
sudo -u postgres psql -d "${PG_DB}" -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS citext;" || warn "Could not enable citext (may require superuser privileges)."
### ---------------------------
### Build Bonfire (as bonfire user)
### ---------------------------
log "Writing .env and building Bonfire (this can take many minutes)"
# ensure webroot exists for ACME challenge
mkdir -p /var/www/html
chown -R "${APP_USER}:${APP_USER}" /var/www/html
sudo -u "${APP_USER}" bash -lc "
set -euo pipefail
export HOME='${APP_HOME}'
. \"\$HOME/.asdf/asdf.sh\" || true
export FLAVOUR='${FLAVOUR}'
export MIX_ENV=prod
export WITH_DOCKER=no
cd '${APP_DIR}'
echo "=== Initializing config with 'just config' ==="
just config
if command -v just >/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 <<EOF_ENV
FLAVOUR=${FLAVOUR}
HOSTNAME=${DOMAIN}
DATABASE_URL=ecto://${PG_USER}:${PG_PASS}@localhost/${PG_DB}
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)
SIGNING_SALT=${SIGNING_SALT}
ENCRYPTION_SALT=${ENCRYPTION_SALT}
MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
WITH_PATHEX=1
# PORT=4000
EOF
EOF_ENV
# ---------- build release ----------
echo "=== Running 'just setup-prod' and 'just rel-build' ==="
just setup-prod
just rel-build
# Disable Pathex if Elixir bug-window
if command -v elixir >/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
# ---------- Nginx + Let's Encrypt ----------
echo "=== Installing Nginx + Certbot ==="
sudo apt install -y nginx certbot python3-certbot-nginx
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:4000"
APP_UPSTREAM="http://127.0.0.1:${APP_PORT}"
echo "=== Writing Nginx site config ==="
sudo tee "${SITE_AVAIL}" >/dev/null <<NGINX
cat > "${SITE_AVAIL}" <<NGINXCONF
server {
listen 80;
server_name ${DOMAIN};
# Proxy all requests to Phoenix (serves static as well)
# ACME challenge path
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type text/plain;
}
location /live/websocket {
proxy_pass ${APP_UPSTREAM};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \"upgrade\";
proxy_set_header Host \$host;
}
location / {
proxy_pass ${APP_UPSTREAM};
proxy_http_version 1.1;
@ -209,108 +423,115 @@ server {
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";
proxy_set_header Connection \"upgrade\";
}
}
NGINX
NGINXCONF
sudo ln -sf "${SITE_AVAIL}" "${SITE_LINK}"
sudo nginx -t
sudo systemctl restart nginx
ln -sf "${SITE_AVAIL}" "${SITE_LINK}"
nginx -t || die "nginx config test failed"
systemctl restart nginx
echo "=== Obtaining Let's Encrypt certificate ==="
sudo certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${EMAIL}"
log "Attempting to obtain Let's Encrypt certificate via certbot"
if ! certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${EMAIL}"; then
warn "Certbot failed to obtain certificate. Check DNS/ports and run: certbot --nginx -d ${DOMAIN} manually."
fi
systemctl enable --now certbot.timer || true
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
### ---------------------------
### systemd unit
### ---------------------------
log "Writing systemd service unit for bonfire"
SERVICE_PATH="/etc/systemd/system/bonfire.service"
cat > "${SERVICE_PATH}" <<SYSTEMD
[Unit]
Description=Bonfire App
After=network-online.target
Wants=network-online.target
After=network-online.target postgresql.service
Wants=network-online.target postgresql.service
[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
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${APP_DIR}/.env
Environment=LANG=en_US.UTF-8
Environment=LC_ALL=en_US.UTF-8
Environment=ELIXIR_ERL_OPTIONS=+fnu
ExecStart=${APP_DIR}/_build/prod/rel/bonfire/bin/bonfire daemon
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
SYSTEMD
sudo systemctl daemon-reload
sudo systemctl enable --now bonfire
systemctl daemon-reload
systemctl enable --now bonfire || warn "systemd enable/start returned non-zero; check journalctl -u bonfire"
# ---------- helper scripts ----------
echo "=== Creating helper scripts ==="
cat > "${HOME}/bonfire/reload-nginx.sh" <<'EOS'
### ---------------------------
### Helper scripts (bonfire user)
### ---------------------------
log "Installing helper scripts into ${APP_DIR}"
sudo -u "${APP_USER}" bash -lc "cat > '${APP_DIR}/reload-nginx.sh' <<'EOS'
#!/usr/bin/env bash
set -e
sudo nginx -t
sudo systemctl reload nginx
echo "Nginx reloaded."
echo 'Nginx reloaded.'
EOS
chmod +x "${HOME}/bonfire/reload-nginx.sh"
chmod +x '${APP_DIR}/reload-nginx.sh' || true"
cat > "${HOME}/bonfire/bonfire-deploy.sh" <<'EOS'
sudo -u "${APP_USER}" bash -lc "cat > '${APP_DIR}/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."
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 "${HOME}/bonfire/bonfire-deploy.sh"
chmod +x '${APP_DIR}/bonfire-deploy.sh' || true"
# ---------- 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"
### ---------------------------
### Final summary
### ---------------------------
log "Done (or attempted). Summary:"
cat <<EOF
Bonfire URL: https://${DOMAIN}
Systemd:
sudo systemctl status bonfire
sudo journalctl -u bonfire -f
Database:
DB_NAME: ${PG_DB}
DB_USER: ${PG_USER}
DB_PASSWORD: ${PG_PASS}
To run CLI as bonfire user:
sudo -u ${APP_USER} -H bash -c "cd ${APP_DIR} && bash"
Useful commands:
sudo -u ${APP_USER} -H bash -c 'cd ${APP_DIR} && just rel-build'
sudo systemctl restart bonfire
sudo systemctl status bonfire
journalctl -u bonfire -b --no-pager
If something failed: inspect the build output above and the journal:
journalctl -u bonfire -b --no-pager
tail -n 200 /var/log/syslog
EOF
exit 0