Make swapfile a default for every install, not just Asterisk droplets

Extracts the swapfile logic out of services/asterisk.sh (previously
DigitalOcean-droplet-gated) into lib/common.sh's ensure_swapfile() —
provider detection was never really the point, the actual condition
that matters is "modest RAM, no swap yet," which applies just as much
to a non-DO VPS running several Docker services at once as it did to a
single-purpose droplet.

- lib/common.sh: new ensure_swapfile(), same fallocate/mkswap/fstab/
  swappiness logic as before, threshold raised from 2048MB to 4096MB
  (a 4GB box running a full service stack is exactly the case that
  motivated this change — the old threshold would have skipped it).
- services/base.sh: calls it unconditionally so every install gets the
  same check regardless of which other services get chosen.
- services/asterisk.sh: swapfile call is no longer gated behind
  IS_DO — calls the shared helper directly. Kept a standalone-mode
  stub (same pattern as this file's other stubbed helpers) so
  `sudo bash asterisk.sh` with no base.sh in the picture still gets
  it. Idempotent either way: a box that already has swap, or already
  got it from base.sh earlier in the same run, no-ops immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBtExJcqxnokyZZKmphdug
This commit is contained in:
Claude
2026-08-09 18:13:37 +00:00
parent 7a66ef2e2f
commit ad38b96cfe
3 changed files with 102 additions and 51 deletions
+41
View File
@@ -268,6 +268,47 @@ ensure_ufw_enabled() {
log_success "UFW enabled (SSH on port ${_ssh_port} allowed first, so this won't lock you out)."
}
# Adds a swapfile on any box with modest RAM and no swap already active —
# no cloud-provider detection, just the actual condition that matters. Used
# to be DigitalOcean-droplet-gated logic living only in services/asterisk.sh;
# generalized here so every install gets the same safety net regardless of
# which services get chosen or which provider the box is on — a small VPS
# running several Docker services at once needs this just as much as a
# single-purpose Asterisk droplet did. Idempotent and safe to call from
# multiple places in the same run (services/base.sh calls it for every
# install; services/asterisk.sh also calls it directly so the standalone
# `sudo bash asterisk.sh` path — no base.sh involved — still gets it): a
# box that already has swap, or already got it from an earlier call in the
# same session, just returns immediately.
ensure_swapfile() {
local TOTAL_RAM_MB
TOTAL_RAM_MB="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
[[ "$TOTAL_RAM_MB" -gt 0 && "$TOTAL_RAM_MB" -le 4096 ]] || return 0
swapon --show | grep -q . && return 0
[ "$DRY_RUN" = true ] && { echo "[DRY-RUN] Would add a swapfile (${TOTAL_RAM_MB}MB RAM, no swap detected)"; return 0; }
local FREE_DISK_MB SWAP_MB=2048
FREE_DISK_MB="$(df -Pm / | awk 'NR==2 {print $4}')"
if [[ "$FREE_DISK_MB" -le $((SWAP_MB + 2048)) ]]; then
log_warning "Not enough free disk for a safe swapfile (${FREE_DISK_MB}MB free) — skipping."
log_warning "Consider a bigger box, or free up disk before installing."
return 0
fi
local ADD_SWAP=""
prompt_yn "No swap detected on this ${TOTAL_RAM_MB}MB-RAM box — add a ${SWAP_MB}MB swapfile? (recommended) (y/n):" "y" ADD_SWAP
[[ "$ADD_SWAP" =~ ^[Yy]$ ]] || return 0
fallocate -l "${SWAP_MB}M" /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_MB" status=none
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
grep -q '^vm.swappiness' /etc/sysctl.conf 2>/dev/null || echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -w vm.swappiness=10 >/dev/null 2>&1
log_success "Swapfile enabled (${SWAP_MB}MB, swappiness=10, persists across reboots)."
}
# Scopes a UFW allow rule to just the caddy_net bridge subnet instead of
# every interface. Needed for any port that only needs to be reachable from
# a *locally* Caddy-fronted service (via host.docker.internal) — a plain
+57 -51
View File
@@ -4,12 +4,13 @@
#
# One installer for both deployment shapes. It detects a DigitalOcean droplet
# (via the link-local metadata service, with a y/n fallback if that's blocked)
# and, in droplet mode, swaps in the public-cloud specifics: a swapfile for
# low-RAM plans, a public-FQDN-only flow with no LAN/VLAN prompts, a Caddy
# site block pinned to that one FQDN, an optional remote Authelia, and a
# DigitalOcean Cloud Firewall via doctl. Everything else — vendor files,
# compose, messaging dialplan, presence alerts, UFW, log rotation — is
# identical either way.
# and, in droplet mode, swaps in the public-cloud specifics: a public-FQDN-only
# flow with no LAN/VLAN prompts, a Caddy site block pinned to that one FQDN,
# an optional remote Authelia, and a DigitalOcean Cloud Firewall via doctl.
# The swapfile (lib/common.sh's ensure_swapfile) is NOT droplet-gatedevery
# box gets that same low-RAM safety net regardless of provider or deployment
# shape. Everything else — vendor files, compose, messaging dialplan,
# presence alerts, UFW, log rotation — is identical either way.
#
# This used to be two services (services/asterisk-digital-ocean.sh held a
# near-duplicate copy of the whole file). An existing droplet install at
@@ -92,6 +93,36 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
esac
}
# Standalone-mode copy of lib/common.sh's ensure_swapfile() — kept in
# sync by hand, same as every other helper stubbed in this block.
ensure_swapfile() {
local TOTAL_RAM_MB
TOTAL_RAM_MB="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
[[ "$TOTAL_RAM_MB" -gt 0 && "$TOTAL_RAM_MB" -le 4096 ]] || return 0
swapon --show | grep -q . && return 0
[ "${DRY_RUN:-false}" = true ] && { echo "[DRY-RUN] Would add a swapfile (${TOTAL_RAM_MB}MB RAM, no swap detected)"; return 0; }
local FREE_DISK_MB SWAP_MB=2048
FREE_DISK_MB="$(df -Pm / | awk 'NR==2 {print $4}')"
if [[ "$FREE_DISK_MB" -le $((SWAP_MB + 2048)) ]]; then
log_warning "Not enough free disk for a safe swapfile (${FREE_DISK_MB}MB free) — skipping."
return 0
fi
local ADD_SWAP=""
prompt_yn "No swap detected on this ${TOTAL_RAM_MB}MB-RAM box — add a ${SWAP_MB}MB swapfile? (recommended) (y/n):" "y" ADD_SWAP
[[ "$ADD_SWAP" =~ ^[Yy]$ ]] || return 0
fallocate -l "${SWAP_MB}M" /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_MB" status=none
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
grep -q '^vm.swappiness' /etc/sysctl.conf 2>/dev/null || echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -w vm.swappiness=10 >/dev/null 2>&1
log_success "Swapfile enabled (${SWAP_MB}MB, swappiness=10, persists across reboots)."
}
configure_caddy_for_service() {
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
local _caddy_dir="$DOCKER_DIR/caddy"
@@ -272,8 +303,9 @@ _asterisk_resolve_layout() {
# metadata service isn't always reachable (a container, a firewalled
# 169.254.0.0/16, a non-DO cloud that still wants the same public-IP
# treatment), so a miss falls back to asking rather than silently deciding
# for the user. Droplet mode is what gates the swapfile, the public-FQDN-only
# flow, and the Cloud Firewall step further down.
# for the user. Droplet mode is what gates the public-FQDN-only flow and the
# Cloud Firewall step further down (the swapfile is NOT droplet-gated — see
# ensure_swapfile in lib/common.sh, called unconditionally further down).
_asterisk_detect_digitalocean() {
local _meta="http://169.254.169.254/metadata/v1"
DROPLET_ID="$(curl -fsS --max-time 2 "$_meta/id" 2>/dev/null || true)"
@@ -284,8 +316,8 @@ _asterisk_detect_digitalocean() {
if [[ -n "$DROPLET_ID" ]]; then
[[ -z "$PUBLIC_IP" ]] && PUBLIC_IP="$(curl -fsS --max-time 3 https://ifconfig.me 2>/dev/null || true)"
log_success "DigitalOcean droplet detected (id $DROPLET_ID, public IP ${PUBLIC_IP:-unknown})."
log_info "Droplet mode adds: swapfile for low-RAM plans, public-FQDN-only setup (no"
log_info "LAN/VLAN prompts), a Cloud Firewall via doctl, and a remote-Authelia option."
log_info "Droplet mode adds: public-FQDN-only setup (no LAN/VLAN prompts), a Cloud"
log_info "Firewall via doctl, and a remote-Authelia option."
prompt_yn "Set this up as a public droplet? (n = treat it as a home/LAN box) (y/n):" "y" _answer
else
log_info "No DigitalOcean metadata service reachable — assuming a home/LAN box."
@@ -883,39 +915,6 @@ EOF
fi
}
# ── Shared: swapfile for low-RAM public cloud boxes ────────────────────────
# DigitalOcean doesn't provision swap by default. Docker + Asterisk + coturn
# fit in 512MB-1GB at idle with little headroom; a swapfile absorbs spikes
# (apt/image pulls, log bursts, a few concurrent calls) instead of the
# kernel OOM-killing a container or the box going unresponsive over SSH.
_asterisk_offer_swapfile() {
local TOTAL_RAM_MB
TOTAL_RAM_MB="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
[[ "$TOTAL_RAM_MB" -gt 0 && "$TOTAL_RAM_MB" -le 2048 ]] || return 0
swapon --show | grep -q . && return 0
local FREE_DISK_MB SWAP_MB=2048
FREE_DISK_MB="$(df -Pm / | awk 'NR==2 {print $4}')"
if [[ "$FREE_DISK_MB" -le $((SWAP_MB + 2048)) ]]; then
log_warning "Not enough free disk for a safe swapfile (${FREE_DISK_MB}MB free) — skipping."
log_warning "Consider a bigger box, or free up disk before installing."
return 0
fi
local ADD_SWAP=""
prompt_yn "No swap detected on this ${TOTAL_RAM_MB}MB-RAM box — add a ${SWAP_MB}MB swapfile? (y/n):" "y" ADD_SWAP
[[ "$ADD_SWAP" =~ ^[Yy]$ ]] || return 0
fallocate -l "${SWAP_MB}M" /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_MB" status=none
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
grep -q '^vm.swappiness' /etc/sysctl.conf 2>/dev/null || echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -w vm.swappiness=10 >/dev/null 2>&1
log_success "Swapfile enabled (${SWAP_MB}MB, swappiness=10, persists across reboots)."
}
# ── Droplet-mode Caddy: web admin on the SAME FQDN used for SIP ────────────
# Deliberately NOT using configure_caddy_for_service in this mode. Caddy only
# holds a cert for domains it's actively serving, and Asterisk never does ACME
@@ -1316,8 +1315,9 @@ MD
## DigitalOcean droplet notes
This install is in public-cloud mode: the installer read the droplet's public
IP from the metadata service, set up a swapfile, offered a Cloud Firewall, and
reverse-proxied the web admin on the same FQDN used for SIP.
IP from the metadata service, offered a Cloud Firewall, and reverse-proxied
the web admin on the same FQDN used for SIP. (The swapfile below isn't
droplet-specific — every install on this box gets the same check.)
### Droplet sizing
@@ -1336,10 +1336,13 @@ phones actually are is fine; SIP/RTP care about latency more than raw
bandwidth.
**Swap:** DigitalOcean doesn't provision swap by default, and Docker +
Asterisk + coturn leave little headroom at 512MB1GB RAM. The installer
detects RAM ≤2GB with no existing swap and offers to add a 2GB swapfile
(persisted in \`/etc/fstab\`) — it's what makes the \$4/mo plan viable instead
of risking an OOM kill under load.
Asterisk + coturn leave little headroom at 512MB1GB RAM. This isn't
Asterisk- or droplet-specific — \`base.sh\` (and this installer, for the
standalone-run case) checks RAM ≤4GB with no existing swap and offers a 2GB
swapfile (persisted in \`/etc/fstab\`) on every install, since a box running
several Docker services at once needs the same insurance a single-purpose
droplet does. It's what makes the \$4/mo plan viable instead of risking an
OOM kill under load — and it's why nothing above 4GB gets asked at all.
**OS image:** Ubuntu 24.04 LTS (supported through April 2029) is the safe,
battle-tested choice for Docker + coturn. Ubuntu 26.04 LTS is also available
@@ -1417,9 +1420,9 @@ install_asterisk() {
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk, patching Asterisk to"
echo "[DRY-RUN] log security events to logs/full (what CrowdSec + the Security Dashboard read)"
echo "[DRY-RUN] Would rotate logs/full at 100MB via /etc/logrotate.d/asterisk"
echo "[DRY-RUN] Would add a swapfile if RAM <= 4096MB and none exists (any deployment shape)"
echo "[DRY-RUN] Would detect a DigitalOcean droplet via its metadata service (asking either way)"
echo "[DRY-RUN] and, in droplet mode, additionally:"
echo "[DRY-RUN] - add a swapfile if RAM <= 2048MB and none exists"
echo "[DRY-RUN] - skip the LAN/VLAN prompts and set up one public FQDN for SIP + web admin"
echo "[DRY-RUN] - reverse-proxy the web admin on that SAME FQDN (needed for SIP cert sync)"
echo "[DRY-RUN] - offer local OR remote Authelia to protect the web admin"
@@ -1525,7 +1528,10 @@ install_asterisk() {
local IS_DO=false DROPLET_ID="" PUBLIC_IP=""
_asterisk_detect_digitalocean
[[ "$IS_DO" == true ]] && _asterisk_offer_swapfile
# Not droplet-gated — every box gets the same low-RAM safety net now
# (see lib/common.sh's ensure_swapfile). Idempotent: a no-op if base.sh
# already added swap earlier in this run, or if swap already exists.
ensure_swapfile
mkdir -p "$EA_DIR"
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
+4
View File
@@ -13,6 +13,7 @@ install_base() {
echo "[DRY-RUN] Would install Docker CE + Compose plugin"
echo "[DRY-RUN] Would detect an NVIDIA GPU and offer to install the driver"
echo " + NVIDIA Container Toolkit (for GPU-accelerated Docker services)"
echo "[DRY-RUN] Would add a swapfile if RAM <= 4096MB and none exists"
echo "[DRY-RUN] Would install/configure openssh-server"
echo "[DRY-RUN] Would offer SSH key import from GitHub/Launchpad"
echo "[DRY-RUN] Would offer to disable SSH password auth"
@@ -38,6 +39,9 @@ install_base() {
# ── Docker ───────────────────────────────────────────────────────────────
require_docker || log_warning "Docker install failed — will retry after base setup"
# ── Swapfile — default for every install, not just Asterisk droplets ────
ensure_swapfile
# ── NVIDIA GPU (driver + container toolkit) ─────────────────────────────
_base_setup_nvidia_gpu