Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# What makes a Fedora machine able to run rootless compose services. Server
|
||||
# role only -- ./install never runs this on a desktop. Idempotent throughout:
|
||||
# every step checks the machine before touching it, so a re-run on a machine
|
||||
# that already has all of this changes nothing and says so.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
# ── Linger ───────────────────────────────────────────────────────────────────
|
||||
# Without it every user unit -- which is every service -- stops at logout and
|
||||
# starts only at login, which on a server means "runs while somebody is SSHed
|
||||
# in". Linger is what makes the user session a real init.
|
||||
if loginctl show-user "$USER" 2>/dev/null | grep -q '^Linger=yes'; then
|
||||
log "Linger already enabled for $USER"
|
||||
else
|
||||
log "Enabling linger for $USER"
|
||||
sudo loginctl enable-linger "$USER"
|
||||
fi
|
||||
|
||||
# ── Unprivileged ports from 80 ───────────────────────────────────────────────
|
||||
# Rootless containers cannot bind 80/443 while the kernel reserves everything
|
||||
# below 1024 for root. Lowering the floor to 80 is what lets the reverse proxy
|
||||
# be a rootless container like everything else. A file in /etc/sysctl.d so it
|
||||
# survives reboots; sysctl --system so it applies now.
|
||||
SYSCTL_FILE=/etc/sysctl.d/99-rootless-ports.conf
|
||||
SYSCTL_WANT='net.ipv4.ip_unprivileged_port_start=80'
|
||||
if [[ -r "$SYSCTL_FILE" ]] && grep -qx "$SYSCTL_WANT" "$SYSCTL_FILE"; then
|
||||
log "Unprivileged ports already start at 80 ($SYSCTL_FILE)"
|
||||
else
|
||||
log "Allowing unprivileged binds from port 80"
|
||||
printf '%s\n' "$SYSCTL_WANT" | sudo tee "$SYSCTL_FILE" >/dev/null
|
||||
sudo sysctl --system >/dev/null
|
||||
fi
|
||||
|
||||
# ── Firewall ─────────────────────────────────────────────────────────────────
|
||||
# 80 and 443 because everything is reverse-proxied; 81 for the proxy's own
|
||||
# admin portal. Deliberately nothing else: a service needing another port open
|
||||
# documents that in its own folder and it is opened by hand, because a list of
|
||||
# per-service firewall holes maintained by an installer is a list nobody
|
||||
# audits.
|
||||
if systemctl is-active firewalld >/dev/null 2>&1; then
|
||||
reload_needed=0
|
||||
for port in 80 443 81; do
|
||||
if sudo firewall-cmd --permanent --query-port="${port}/tcp" >/dev/null 2>&1; then
|
||||
log "Port ${port}/tcp already open"
|
||||
else
|
||||
log "Opening port ${port}/tcp"
|
||||
sudo firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null
|
||||
reload_needed=1
|
||||
fi
|
||||
done
|
||||
(( reload_needed )) && sudo firewall-cmd --reload >/dev/null
|
||||
else
|
||||
log "firewalld is not active; no ports to open"
|
||||
fi
|
||||
|
||||
# ── The shared container network ─────────────────────────────────────────────
|
||||
# Every compose file expects nginx-bridge as an external network: the reverse
|
||||
# proxy reaches each service by container name across it, and no service needs
|
||||
# a published port of its own. External means compose will not create it, so
|
||||
# somebody has to -- this is that somebody.
|
||||
if podman network exists nginx-bridge 2>/dev/null; then
|
||||
log "podman network nginx-bridge already exists"
|
||||
else
|
||||
log "Creating podman network nginx-bridge"
|
||||
podman network create nginx-bridge >/dev/null
|
||||
fi
|
||||
|
||||
# ── Nightly image updates ────────────────────────────────────────────────────
|
||||
# server/scripts/update-containers, on a midnight timer. Linked rather than
|
||||
# copied so a pull updates the machinery with everything else; see the script
|
||||
# header for why this replaced watchtower.
|
||||
UNIT_SRC="$PANAMA_PATH/server/systemd"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
mkdir -p "$UNIT_DIR" "$HOME/Server/logs"
|
||||
for unit in podman-update.service podman-update.timer; do
|
||||
src="$UNIT_SRC/$unit"
|
||||
dst="$UNIT_DIR/$unit"
|
||||
[[ -e "$src" ]] || { log "Missing $src; skipping the update timer"; continue; }
|
||||
if [[ -L "$dst" ]]; then
|
||||
rm "$dst"
|
||||
elif [[ -e "$dst" ]]; then
|
||||
mv "$dst" "$dst.pre-panama"
|
||||
log "Kept the existing $unit as $unit.pre-panama"
|
||||
fi
|
||||
ln -s "$src" "$dst"
|
||||
log "Linked $unit"
|
||||
done
|
||||
systemctl --user daemon-reload
|
||||
if systemctl --user is-enabled podman-update.timer >/dev/null 2>&1; then
|
||||
log "podman-update.timer already enabled"
|
||||
else
|
||||
log "Enabling podman-update.timer (nightly image updates)"
|
||||
systemctl --user enable --now podman-update.timer
|
||||
fi
|
||||
Reference in New Issue
Block a user