feat(system-tuning): new package for swap + LXC cgroup memory caps (closes #391)

Captures the gk2 RAM optimization applied by hand on 2026-05-27.
Before: 117 MB free, 0 swap, no cgroup caps → 95+ packages thrashing.
After: 1.1 GB free, 4 GB swap, 12 LXC containers throttled at 2x current
RSS via cgroup MemoryHigh (soft cap = throttle, not OOM-kill).

Package contents:

  sbin/secubox-tuning-apply         idempotent CLI; reads two TOML
                                     configs and applies swap + cgroup
                                     caps. Supports --dry-run, swap-only,
                                     cgroup-only.
  etc/secubox/tuning/config.toml    swap path + size (default 4G) +
                                     minimum_data_free_gb safety.
  etc/secubox/tuning/lxc-memory-high.toml
                                     12 container → MemoryHigh mapping
                                     (mail=500M, matrix=400M, etc.) from
                                     gk2 baseline.
  debian/postinst                   runs secubox-tuning-apply with safe
                                     defaults.
  debian/prerm                      deliberately preserves swap and
                                     drop-ins on uninstall (manual
                                     cleanup documented inline).

Cgroup caps applied two ways:
  1. Direct write to /sys/fs/cgroup/lxc.payload.<name>/memory.high for
     immediate runtime effect on running containers (no restart).
  2. Edit of /data/lxc/<name>/config or /var/lib/lxc/<name>/config to
     add lxc.cgroup2.memory.high = <cap>, so lxc-autostart re-applies
     the cap on every container restart.

Initial attempt used systemd drop-ins under
/etc/systemd/system/lxc@<name>.service.d/ — turned out to be ineffective
on boards where containers are started by lxc-autostart (not by
systemctl start lxc@<name>.service). Refactored to the direct-cgroup +
LXC-config approach during gk2 verification.

Verified on gk2: all 12 containers show non-"max" memory.high in cgroup
+ matching lxc.cgroup2.memory.high in their LXC config files.

Distinct from secubox-hardening (kernel sysctl + AppArmor + module
blacklist for security) — this package is performance tuning.
This commit is contained in:
CyberMind-FR 2026-05-27 13:10:20 +02:00
parent d20aacf8cf
commit b54f9c4e96
8 changed files with 330 additions and 0 deletions

View File

@ -0,0 +1,19 @@
secubox-system-tuning (1.0.0-1~bookworm1) bookworm; urgency=medium
* Initial release — captures the gk2 RAM optimization applied by
hand on 2026-05-27 (closes #391).
* Creates /data/swapfile (default 4G) if /data has enough free
space and no swap is currently active; persists in /etc/fstab.
* Applies per-LXC-container cgroup MemoryHigh caps two ways:
- immediate cgroup write to /sys/fs/cgroup/lxc.payload.<name>/
memory.high for runtime effect, no restart needed;
- persistent edit of /data/lxc/<name>/config (or /var/lib/lxc/
<name>/config — falls back) adding
lxc.cgroup2.memory.high = <cap> so the cap is re-applied on
every lxc-start by lxc-autostart at boot.
* Idempotent secubox-tuning-apply script — re-run after editing
/etc/secubox/tuning/lxc-memory-high.toml to update caps.
* Default cap profile from gk2 (mail/matrix/mitmproxy/nextcloud/
lyrion/grafana/horde/authelia/mqtt/rustdesk/roundcube/zigbee).
-- Gerald KERMA <devel@cybermind.fr> Wed, 27 May 2026 19:45:00 +0000

View File

@ -0,0 +1,28 @@
Source: secubox-system-tuning
Section: admin
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Package: secubox-system-tuning
Architecture: all
Depends: ${misc:Depends}, systemd, util-linux, python3
Recommends: lxc
Description: SecuBox System Tuning — swap + cgroup MemoryHigh caps for LXC containers
Per-board memory tuning for SecuBox: configures a swap file in /data
(boards typically ship with 0 swap and exhaust real memory under LXC
load), and applies per-LXC-container cgroup MemoryHigh soft caps
to throttle (not OOM-kill) runaway containers.
.
Captures the by-hand fix applied on gk2 on 2026-05-27 (#391) so
other boards inherit it automatically. Idempotent: re-running the
tuning script after editing the TOML config only diffs the deltas.
.
Configuration lives at /etc/secubox/tuning/{config.toml,
lxc-memory-high.toml}. Apply with secubox-tuning-apply or wait
for the next package upgrade.
.
Distinct from secubox-hardening (kernel sysctl + AppArmor +
module blacklist for *security*) — this package is *performance*
tuning: swap and cgroup soft limits.

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
# Apply tuning with safe defaults. Idempotent; if swap is already
# active or caps are already applied, this is a no-op.
if [ -x /usr/sbin/secubox-tuning-apply ]; then
/usr/sbin/secubox-tuning-apply || \
echo "secubox-system-tuning: apply failed (non-fatal, edit /etc/secubox/tuning/ and re-run /usr/sbin/secubox-tuning-apply)" >&2
fi
fi
#DEBHELPER#
exit 0

View File

@ -0,0 +1,13 @@
#!/bin/sh
set -e
# On removal: deliberately leave /data/swapfile and the LXC drop-ins
# in place. Removing them mid-board-life could trigger OOM-killer on
# heavy LXC containers. Operator can remove manually:
# swapoff /data/swapfile && rm /data/swapfile
# rm -rf /etc/systemd/system/lxc@*.service.d/50-memory-high.conf
# systemctl daemon-reload
# <restart LXC containers to drop runtime caps>
#DEBHELPER#
exit 0

View File

@ -0,0 +1,10 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_install:
install -d debian/secubox-system-tuning/usr/sbin
install -m 755 sbin/secubox-tuning-apply debian/secubox-system-tuning/usr/sbin/
install -d debian/secubox-system-tuning/etc/secubox/tuning
install -m 644 etc/secubox/tuning/config.toml debian/secubox-system-tuning/etc/secubox/tuning/
install -m 644 etc/secubox/tuning/lxc-memory-high.toml debian/secubox-system-tuning/etc/secubox/tuning/

View File

@ -0,0 +1,16 @@
# /etc/secubox/tuning/config.toml — SecuBox memory tuning master config
# Modify and re-run /usr/sbin/secubox-tuning-apply to re-apply.
[swap]
# Path of the swap file. Lives on /data because boards typically have
# the bulk of their storage there (mSSD), not on / (microSD).
path = "/data/swapfile"
# Swap size. Use 'G' or 'M' suffix (passed to fallocate -l).
# Default 4G is sized for 8 GB boards with heavy LXC load (gk2 baseline).
size = "4G"
# Minimum free space on /data before secubox-tuning-apply will allocate
# the swap file. Safety: avoid filling up /data on a board with limited
# storage.
minimum_data_free_gb = 8

View File

@ -0,0 +1,28 @@
# /etc/secubox/tuning/lxc-memory-high.toml — per-LXC-container MemoryHigh caps.
#
# MemoryHigh is a SOFT cap: when the container exceeds it, the kernel
# throttles allocations rather than OOM-killing (which would happen on
# MemoryMax). Tune up if you see throttling in `journalctl -u lxc@<name>`
# or if a container becomes unresponsive under load.
#
# Each entry generates /etc/systemd/system/lxc@<container>.service.d/
# 50-memory-high.conf with [Service] MemoryHigh=<value>. Applied at
# runtime by secubox-tuning-apply via `systemctl set-property` (no
# container restart required for already-running containers).
#
# Defaults below are from gk2 baseline (2026-05-27, post-#388 deploy).
# They are roughly 2× current observed RSS for each container.
[caps]
mail = "500M" # postfix + dovecot + rspamd
mitmproxy = "400M" # WAF inspector
matrix = "400M" # synapse
nextcloud = "600M" # NC + bundled mariadb
lyrion = "500M" # Logitech Media Server
grafana = "400M" # grafana-server
horde = "300M" # horde webmail
authelia = "200M" # SSO IdP
mqtt = "150M" # mosquitto
rustdesk = "200M" # rustdesk relay
roundcube = "200M" # roundcube webmail (separate from horde)
zigbee = "200M" # zigbee2mqtt

View File

@ -0,0 +1,202 @@
#!/usr/bin/env bash
# /usr/sbin/secubox-tuning-apply — apply swap + cgroup MemoryHigh tuning.
#
# Idempotent. Reads config from /etc/secubox/tuning/.
#
# Usage:
# secubox-tuning-apply # apply both swap and cgroup caps
# secubox-tuning-apply swap # swap only
# secubox-tuning-apply cgroup # cgroup caps only
# secubox-tuning-apply --dry-run # show what would be done
set -euo pipefail
CONFIG_FILE="/etc/secubox/tuning/config.toml"
LXC_CAPS_FILE="/etc/secubox/tuning/lxc-memory-high.toml"
CYAN='\033[0;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${CYAN}[tuning]${NC} $*"; }
ok() { echo -e "${GREEN}[ ok ]${NC} $*"; }
warn() { echo -e "${YELLOW}[ warn ]${NC} $*"; }
err() { echo -e "${RED}[fail ]${NC} $*" >&2; }
DRY_RUN=0
DO_SWAP=1
DO_CGROUP=1
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
swap) DO_CGROUP=0 ;;
cgroup) DO_SWAP=0 ;;
-h|--help)
sed -n '1,/^set -/p' "$0" | grep '^#' | sed 's/^# \?//'
exit 0
;;
*) err "unknown arg: $arg"; exit 2 ;;
esac
done
run() {
if [[ $DRY_RUN -eq 1 ]]; then
log "DRY-RUN: $*"
else
eval "$@"
fi
}
# Minimal TOML reader using python3 (avoids depending on toml-cli).
toml_get() {
local file="$1" key="$2" default="${3:-}"
python3 -c "
import sys
try:
import tomllib
except ImportError:
import tomli as tomllib
try:
d = tomllib.loads(open('$file').read())
for k in '$key'.split('.'):
d = d[k]
print(d)
except (KeyError, FileNotFoundError):
print('$default')
" 2>/dev/null || echo "$default"
}
toml_table_keys() {
local file="$1" table="$2"
python3 -c "
import sys
try:
import tomllib
except ImportError:
import tomli as tomllib
d = tomllib.loads(open('$file').read())
for k in d.get('$table', {}):
print(k)
" 2>/dev/null
}
apply_swap() {
log "swap phase"
local swap_path size min_free_gb
swap_path=$(toml_get "$CONFIG_FILE" swap.path /data/swapfile)
size=$(toml_get "$CONFIG_FILE" swap.size 4G)
min_free_gb=$(toml_get "$CONFIG_FILE" swap.minimum_data_free_gb 8)
if swapon --show=NAME --noheadings | grep -q '.'; then
ok "swap already active: $(swapon --show=NAME --noheadings | tr '\n' ' ')"
return 0
fi
if [[ ! -d "$(dirname "$swap_path")" ]]; then
warn "swap parent dir $(dirname "$swap_path") missing, skipping"
return 0
fi
local free_gb
free_gb=$(df --output=avail -BG "$(dirname "$swap_path")" | tail -1 | tr -d 'G ')
if [[ "$free_gb" -lt "$min_free_gb" ]]; then
warn "/data free space ${free_gb}G < required ${min_free_gb}G; skipping swap creation"
return 0
fi
if [[ -f "$swap_path" ]]; then
log "$swap_path exists, activating"
run "swapon $swap_path"
else
log "creating $swap_path ($size)"
run "fallocate -l $size $swap_path"
run "chmod 600 $swap_path"
run "mkswap $swap_path"
run "swapon $swap_path"
fi
if ! grep -q "^$swap_path " /etc/fstab; then
log "persisting in /etc/fstab"
run "echo '$swap_path none swap sw 0 0' >> /etc/fstab"
fi
ok "swap: $(swapon --show)"
}
apply_cgroup() {
log "cgroup MemoryHigh phase"
if [[ ! -f "$LXC_CAPS_FILE" ]]; then
warn "$LXC_CAPS_FILE not found, skipping"
return 0
fi
local containers
containers=$(toml_table_keys "$LXC_CAPS_FILE" caps)
if [[ -z "$containers" ]]; then
warn "no caps defined in $LXC_CAPS_FILE"
return 0
fi
# On SecuBox boards LXC containers are started by lxc-autostart at boot,
# NOT by systemctl start lxc@<name>.service. So drop-ins under
# /etc/systemd/system/lxc@<name>.service.d/ are ineffective. Apply the
# cap two ways instead:
# 1. Direct write to the container's existing cgroup
# (/sys/fs/cgroup/lxc.payload.<name>/memory.high) for immediate
# effect, no restart.
# 2. Add lxc.cgroup2.memory.high=<cap> to the container's LXC config
# (/var/lib/lxc/<name>/config OR /data/lxc/<name>/config — try
# both, take the first that exists) so the cap is reapplied on
# every lxc-start.
local applied=0 persisted=0 missed=0
for ctn in $containers; do
local cap cgrp lxc_conf
cap=$(toml_get "$LXC_CAPS_FILE" "caps.$ctn")
# (1) immediate cgroup write
cgrp="/sys/fs/cgroup/lxc.payload.${ctn}"
if [[ -d "$cgrp" ]]; then
run "echo $cap > $cgrp/memory.high"
applied=$((applied+1))
printf " %-12s cgroup memory.high=%s (runtime)" "$ctn" "$cap"
else
missed=$((missed+1))
printf " %-12s container not running (no cgroup)" "$ctn"
fi
# (2) persistent LXC config edit. Look in both standard and /data paths.
lxc_conf=""
for path in "/data/lxc/${ctn}/config" "/var/lib/lxc/${ctn}/config"; do
if [[ -f "$path" ]]; then
lxc_conf="$path"
break
fi
done
if [[ -n "$lxc_conf" ]]; then
if [[ $DRY_RUN -eq 1 ]]; then
printf " + DRY-RUN: would set memory.high=%s in %s\n" "$cap" "$lxc_conf"
else
# Replace existing memory.high line if present, otherwise append.
if grep -q "^lxc\.cgroup2\.memory\.high\s*=" "$lxc_conf"; then
sed -i "s|^lxc\.cgroup2\.memory\.high\s*=.*|lxc.cgroup2.memory.high = ${cap}|" "$lxc_conf"
else
echo "lxc.cgroup2.memory.high = ${cap}" >> "$lxc_conf"
fi
printf " + persisted in %s\n" "$lxc_conf"
persisted=$((persisted+1))
fi
else
printf "\n"
fi
done
ok "cgroup caps: $applied runtime + $persisted persisted + $missed missed"
}
log "secubox-tuning-apply (dry_run=$DRY_RUN, swap=$DO_SWAP, cgroup=$DO_CGROUP)"
[[ $DO_SWAP -eq 1 ]] && apply_swap
[[ $DO_CGROUP -eq 1 ]] && apply_cgroup
ok "done"