bonfirestarter/bonfire_turnkey.sh

538 lines
18 KiB
Bash

#!/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:-<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
\$\$;
PSQL
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
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}'
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}
SECRET_KEY_BASE=${SECRET_KEY_BASE}
SIGNING_SALT=${SIGNING_SALT}
ENCRYPTION_SALT=${ENCRYPTION_SALT}
MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
WITH_PATHEX=1
# PORT=4000
EOF_ENV
# 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
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}" <<NGINXCONF
server {
listen 80;
server_name ${DOMAIN};
# 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;
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\";
}
}
NGINXCONF
ln -sf "${SITE_AVAIL}" "${SITE_LINK}"
nginx -t || die "nginx config test failed"
systemctl restart nginx
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
### ---------------------------
### 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 postgresql.service
Wants=network-online.target postgresql.service
[Service]
Type=simple
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
systemctl daemon-reload
systemctl enable --now bonfire || warn "systemd enable/start returned non-zero; check journalctl -u bonfire"
### ---------------------------
### 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.'
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 <<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