C1: bake shower wheel into image; wire borgmatic; refine NFS docs

Three follow-ups on the shower deployment branch:

1. containers/shower/default.nix now uses buildPythonPackage to install
   the adelaide-baby-shower-app wheel + its deps at nix build time. The
   wheel comes from the forge PyPI index with a pinned SRI hash. The
   entrypoint no longer does pip-at-boot — it just runs migrations,
   collectstatic, and execs gunicorn.

2. ansible/roles/borgmatic/defaults/main.yml:
   - Adds shower to borgmatic_k8s_sqlite_dumps (context k3s-ringtail)
     so /app/data/db.sqlite3 is dumped via kubectl exec on every run.
   - Adds /Volumes/shower (sifaka SMB mount on indri) to
     borgmatic_source_directories so prize-photo media gets archived.

3. NFS share docs corrected to match the real on-sifaka pattern:
   exports allowlist 192.168.1.0/24 + 100.64.0.0/10 with all_squash to
   admin (matching frigate/paperless/etc.), not "Squash=No mapping".
   The pod's runAsUser doesn't need to match an on-disk uid because
   all_squash rewrites every write to admin:users.

Also adds a missing service-versions entry for the tailscale container
introduced in PR #347 — pre-existing gap surfaced by the
container-version-check hook on this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-05-11 08:37:12 -07:00
commit cb4f4085c2
6 changed files with 159 additions and 77 deletions

View file

@ -1,76 +1,79 @@
# Nix-built shower app container — Adelaide / Heidi / Addie baby shower.
#
# The app is published as a wheel to the Forgejo PyPI index at
# https://forge.eblu.me/api/packages/eblume/pypi/. Rather than pin and
# fetch the wheel + transitive deps at nix build time (which requires
# every wheel hash to be tracked here), this image ships a Python from
# nixpkgs and pip-installs the wheel into a venv on /app/data at first
# boot. Subsequent boots reuse the venv. This trades reproducibility for
# a much simpler nix file.
# https://forge.eblu.me/api/packages/eblume/pypi/. The wheel + its
# Python deps are baked in at build time via buildPythonPackage so the
# container boots cleanly with no pip-at-runtime. Build runs on the
# nix-container-builder runner (ringtail, amd64) so the image is native.
#
# Built on the nix-container-builder runner (ringtail, amd64) so the
# image runs natively on ringtail's k3s without QEMU emulation.
# To bump the version:
# 1. Update `version` below.
# 2. Update `wheelHash` — `nix-prefetch-url <url>` against the new wheel,
# or set it to `pkgs.lib.fakeHash` and let the build print the right one.
{ pkgs ? import <nixpkgs> { } }:
let
version = "1.0.0";
wheelHash = "sha256-9Xk3TCzl474As8n0RhLoy/QYw+K1DABBWEwLC8v1X0A=";
python = pkgs.python314;
appVersion = version;
entrypoint = pkgs.writeShellScript "shower-entrypoint" ''
set -eu
showerWheel = pkgs.fetchurl {
name = "adelaide_baby_shower_app-${version}-py3-none-any.whl";
url = "https://forge.eblu.me/api/packages/eblume/pypi/files/adelaide-baby-shower-app/${version}/adelaide_baby_shower_app-${version}-py3-none-any.whl";
hash = wheelHash;
};
APP_DIR=/app
DATA_DIR=/app/data
VENV_DIR=$DATA_DIR/.venv
INSTALLED_MARKER=$VENV_DIR/.installed-${appVersion}
shower = python.pkgs.buildPythonPackage {
pname = "adelaide-baby-shower-app";
inherit version;
format = "wheel";
src = showerWheel;
doCheck = false;
propagatedBuildInputs = with python.pkgs; [
django
django-axes
pillow
scipy
segno
];
};
export HOME=$DATA_DIR
export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_NO_CACHE_DIR=1
pyEnv = python.withPackages (ps: [
shower
ps.gunicorn
]);
mkdir -p "$DATA_DIR" "$APP_DIR/media"
# First boot (or version change): create venv and install the app + deps.
# The wheel comes from the internal devpi mirror (default index), with
# forge.eblu.me as the extra index for the adelaide-baby-shower-app wheel.
if [ ! -f "$INSTALLED_MARKER" ]; then
echo "shower: installing adelaide-baby-shower-app==${appVersion} into $VENV_DIR"
rm -rf "$VENV_DIR"
${python}/bin/python -m venv "$VENV_DIR"
"$VENV_DIR/bin/pip" install --upgrade pip
"$VENV_DIR/bin/pip" install \
--index-url=https://pypi.ops.eblu.me/root/pypi/+simple/ \
--extra-index-url=https://forge.eblu.me/api/packages/eblume/pypi/simple/ \
"adelaide-baby-shower-app==${appVersion}" gunicorn
touch "$INSTALLED_MARKER"
fi
# The wheel's config/settings.py uses BASE_DIR = parent.parent of its
# own __file__, so MEDIA_ROOT and DATABASES.NAME resolve relative to
# site-packages. Override with a thin shim placed in $APP_DIR.
cat > "$APP_DIR/local_settings.py" <<'PY'
# Settings shim — config/settings.py's `BASE_DIR = parent.parent` would
# otherwise resolve to site-packages, scattering db.sqlite3 / media /
# staticfiles into the venv. Pin them to /app/{data,media,data/static}.
localSettings = pkgs.writeText "local_settings.py" ''
from config.settings import * # noqa: F401,F403
DATABASES["default"]["NAME"] = "/app/data/db.sqlite3"
MEDIA_ROOT = "/app/media"
STATIC_ROOT = "/app/data/staticfiles"
PY
'';
export PYTHONPATH=$APP_DIR
entrypoint = pkgs.writeShellScript "shower-entrypoint" ''
set -eu
export HOME=/app/data
export PYTHONPATH=/app
export DJANGO_SETTINGS_MODULE=local_settings
cd "$APP_DIR"
cd /app
mkdir -p /app/data /app/media
echo "shower: running migrations"
"$VENV_DIR/bin/python" -m django migrate --noinput
${pyEnv}/bin/python -m django migrate --noinput
echo "shower: collecting static files"
"$VENV_DIR/bin/python" -m django collectstatic --noinput --clear
${pyEnv}/bin/python -m django collectstatic --noinput --clear
echo "shower: starting gunicorn"
exec "$VENV_DIR/bin/gunicorn" \
exec ${pyEnv}/bin/gunicorn \
--bind 0.0.0.0:8000 \
--workers 2 \
--forwarded-allow-ips='*' \
@ -81,19 +84,20 @@ in
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/shower";
contents = [
python
pyEnv
pkgs.cacert
pkgs.tzdata
pkgs.bashInteractive
pkgs.coreutils
pkgs.gnused
pkgs.gnugrep
];
# /app is writable by uid 1000 (matches deployment.yaml runAsUser).
fakeRootCommands = ''
extraCommands = ''
mkdir -p app/data app/media tmp
chmod 1777 tmp
cp ${localSettings} app/local_settings.py
'';
fakeRootCommands = ''
chown -R 1000:1000 app
'';
enableFakechroot = true;