port the swap section, and size it against the disk

Four things the original got wrong, all of which only show up on a machine that
is not this one:

  It detected swap with `swapon --show | grep -q '/'` — a test for a swap FILE.
  A machine using zram or a swap partition reports no swap at all, and the step
  would add a swapfile beside working swap. Reads SwapTotal from /proc/meminfo
  now, which covers every kind.

  It never looked at free disk. On a VPS with 4G free and 16G of RAM it would
  fallocate 8G, fail, and take the run down under `set -e`. The recommendation is
  now capped by what is actually there, keeping 5G back, and refuses rather than
  shrinking to something useless.

  fallocate was assumed to work. It produces a file that btrfs and zfs will not
  swap on, so dd is the fallback — slow, but it always works.

  swappiness was written by sed'ing /etc/sysctl.conf in place, tangling it with
  whatever else lives there. It is a drop-in at /etc/sysctl.d now, so what this
  script set is visible as its own file.

Role-dependent, which is the first use of MACHINE_ROLE: swappiness 10 on a
server, where swapping is the emergency valve and a page fault on a request path
is latency somebody is waiting for; the kernel default of 60 on dev, where
swapping out an application nobody has touched in an hour is exactly what you
want.

WSL is left alone entirely — WSL2 runs its own managed swap inside the VM, and a
swapfile written here is wasted disk the kernel will not use.

Sizes are reported rounded rather than floored. A 4 GiB swapfile is 4194300 kB,
which floors to 3 and reads as though a gigabyte went missing. Free disk stays
floored, deliberately: it decides how much to allocate, and rounding up invents
space.

Verified on this host — 4G RAM, 4G existing swap correctly detected and left
alone — and with the disk check stubbed at 6G free (caps to 1G) and 3G free
(refuses).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 17:48:45 +00:00
co-authored by Claude Opus 5
parent b5948f9673
commit 716a6e2750
2 changed files with 139 additions and 1 deletions
+84
View File
@@ -89,6 +89,90 @@ locale_set() {
esac esac
} }
# -----------------------------------------------------------------------------
# Swap
# -----------------------------------------------------------------------------
SWAPFILE=/swapfile
# Rounded to nearest, not floored: a 4 GiB swapfile is 4194300 kB, which floors
# to 3 and reads as though a gigabyte went missing. Same for RAM, where 3.7 GiB
# reporting as "3G" makes the sizing tiers look wrong.
kb_to_gb_rounded() { echo $((($1 + 524288) / 1048576)); }
# Total active swap in GiB, 0 if there is none.
#
# From /proc/meminfo rather than by grepping swapon's output for a slash, which
# is what the original did to spot a swap FILE — that test reports no swap at all
# on a machine using zram or a swap partition, and the step would then add a
# swapfile beside perfectly good swap.
swap_active_gb() { kb_to_gb_rounded "$(awk '/^SwapTotal:/ { print $2 }' /proc/meminfo)"; }
ram_gb() { kb_to_gb_rounded "$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)"; }
# Free space on the filesystem that would hold the swapfile, in GiB. Floored
# rather than rounded, deliberately: this one decides how much to allocate, and
# rounding up invents space that is not there.
disk_free_gb() { echo $(($(df -Pk "$(dirname "$SWAPFILE")" | awk 'NR == 2 { print $4 }') / 1024 / 1024)); }
# How much swap this machine should have.
#
# The tiers are the original's. What is new is that the answer is capped by what
# is actually on the disk — the original would try to fallocate 8G on a VPS with
# 4G free, fail, and take the run down with it.
swap_recommended_gb() {
local ram size
ram="$(ram_gb)"
if ((ram <= 2)); then
size=2
elif ((ram <= 8)); then
size=4
else
size=8
fi
# Leave a few gigabytes behind. A swapfile that fills the disk is a worse
# problem than no swapfile.
local room=$(($(disk_free_gb) - 5))
((room < size)) && size="$room"
((size < 1)) && size=0
echo "$size"
}
# How eagerly the kernel swaps, by role.
#
# 10 on a server: swapping is the emergency valve, not a routine, and the cost of
# a page fault on a request path is latency somebody is waiting for. A desktop is
# the opposite case — swapping out an application nobody has touched in an hour
# is exactly what you want — so dev keeps the kernel default of 60.
swappiness_for_role() { if is_server; then echo 10; else echo 60; fi; }
swap_create() {
local gb="$1"
# fallocate is instant but produces a file some filesystems refuse to swap on
# (btrfs without the right attributes, zfs at all). dd is slow and always
# works, so it is the fallback rather than the default.
if ! fallocate -l "${gb}G" "$SWAPFILE" 2>/dev/null; then
info " fallocate is not usable here — writing the file with dd, which is slower"
dd if=/dev/zero of="$SWAPFILE" bs=1M count=$((gb * 1024)) status=none
fi
chmod 600 "$SWAPFILE"
mkswap "$SWAPFILE" >/dev/null
swapon "$SWAPFILE"
grep -qs "^${SWAPFILE}[[:space:]]" /etc/fstab || echo "${SWAPFILE} none swap sw 0 0" >>/etc/fstab
}
# Written as a drop-in rather than by rewriting /etc/sysctl.conf in place. The
# original sed'd that file, which means the setting is tangled up with whatever
# else lives there and is invisible to anyone looking for what this script did.
swappiness_set() {
echo "vm.swappiness=$1" >/etc/sysctl.d/99-machine-setup-swappiness.conf
sysctl -q -w "vm.swappiness=$1"
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Timezone # Timezone
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+55 -1
View File
@@ -261,13 +261,67 @@ if ! skip; then
step_ok step_ok
fi fi
# =============================================================================
# 7. Swap
# =============================================================================
#
# Disk the kernel can park cold pages on when RAM fills, so a spike costs
# latency instead of a process. A `bun install` or a Docker build on a small
# machine is exactly the spike this is for.
step "Swap"
if ! skip; then
ACTIVE_SWAP_GB="$(swap_active_gb)"
WANT_SWAP_GB="$(swap_recommended_gb)"
SWAPPINESS="$(swappiness_for_role)"
CURRENT_SWAPPINESS="$(sysctl -n vm.swappiness 2>/dev/null || echo unknown)"
echo ""
info "Swap — overflow space so a memory spike costs speed rather than a process"
echo " RAM: $(ram_gb)G"
echo " active swap: ${ACTIVE_SWAP_GB}G"
echo " swappiness: ${CURRENT_SWAPPINESS} -> ${SWAPPINESS} (${MACHINE_ROLE})"
if [[ "$IS_WSL" == true ]]; then
# WSL2 runs its own managed swap inside the VM; a swapfile here is wasted
# disk and is not what the kernel would use anyway.
echo " WSL manages its own swap — leaving it alone"
SUMMARY+=("Swap: left to WSL")
elif ((ACTIVE_SWAP_GB > 0)); then
echo " already has ${ACTIVE_SWAP_GB}G of swap, leaving it alone"
if [[ "$CURRENT_SWAPPINESS" != "$SWAPPINESS" ]] && confirm "Set swappiness to ${SWAPPINESS}?"; then
swappiness_set "$SWAPPINESS"
ok "swappiness set to ${SWAPPINESS}"
SUMMARY+=("Swap: kept ${ACTIVE_SWAP_GB}G, swappiness ${SWAPPINESS}")
else
SUMMARY+=("Swap: kept ${ACTIVE_SWAP_GB}G")
fi
elif ((WANT_SWAP_GB == 0)); then
# Capped to nothing by the disk check rather than by choice.
warn "not enough free disk to add swap safely — $(disk_free_gb)G free"
SUMMARY+=("Swap: none added, disk too full")
else
echo " to create: ${WANT_SWAP_GB}G at ${SWAPFILE} ($(disk_free_gb)G free now)"
if confirm "Proceed?"; then
swap_create "$WANT_SWAP_GB"
swappiness_set "$SWAPPINESS"
ok "${WANT_SWAP_GB}G swap active, swappiness ${SWAPPINESS}"
SUMMARY+=("Swap: ${WANT_SWAP_GB}G created, swappiness ${SWAPPINESS}")
else
warn "skipped by request"
SUMMARY+=("Swap: SKIPPED by request")
fi
fi
step_ok
fi
# ============================================================================= # =============================================================================
# NOT PORTED YET # NOT PORTED YET
# ============================================================================= # =============================================================================
# #
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order: # Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
# #
# swap · auto-suspend · boot-hang fix · user creation · # auto-suspend · boot-hang fix · user creation ·
# ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades · # ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades ·
# git config · docker · zsh + prompt · tailscale · neovim · js runtimes · # git config · docker · zsh + prompt · tailscale · neovim · js runtimes ·
# dev tools · ufw · zshrc · disk ballast # dev tools · ufw · zshrc · disk ballast