Adds the Adelaide / Heidi / Addie baby shower app — a Django guest
splash, raffle picker, and prize-assignment console — on ringtail k3s.
Public landing at shower.eblu.me (via fly proxy), tailnet admin at
shower.ops.eblu.me. App source: forge.eblu.me/eblume/adelaide-baby-shower-app,
wheel-published to the Forgejo Packages PyPI index.
Manifests under argocd/manifests/shower/: NFS-backed PVC for /app/media,
local-path PVC for SQLite, ExternalSecret pulling DJANGO_SECRET_KEY from
1Password (item "Shower (blumeops)"), Tailscale ProxyGroup ingress.
Defense-in-depth for the public surface:
- /admin/ blocked at the fly edge except /admin/login/ and /admin/logout/
- shower_auth rate limit on the login path
- new fail2ban filter+jail with a per-service shower-deny.conf
(nginx-deny action generalized to accept nginx_deny_file)
- django-axes (5 / 1h) keyed on (username, ip_address)
Plus: Caddy route on indri, Pulumi gandi CNAME, Grafana APM dashboard
mirroring docs-apm.json, runbook at how-to/operations/shower-app.md,
and a service-versions entry. X-Clacks-Overhead set on the new server
block — GNU Terry Pratchett.
Build: containers/shower/default.nix uses dockerTools to ship a
nixpkgs Python plus a startup wrapper that installs the wheel into
/app/data/.venv on first boot and execs gunicorn. Lets the wheel come
from forge PyPI without pinning hashes for every transitive dep.
Prerequisites tracked in the runbook (not yet executed):
- NFS share sifaka:/volume1/shower (manual Synology step)
- 1Password item "Shower (blumeops)" with secret-key field
- container build via `mise run container-build-and-release shower`
- Pulumi dns-up after merge
- fly certs add shower.eblu.me
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.5 KiB
Nix
117 lines
3.5 KiB
Nix
# 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.
|
|
#
|
|
# Built on the nix-container-builder runner (ringtail, amd64) so the
|
|
# image runs natively on ringtail's k3s without QEMU emulation.
|
|
{ pkgs ? import <nixpkgs> { } }:
|
|
|
|
let
|
|
version = "1.0.0";
|
|
|
|
python = pkgs.python314;
|
|
appVersion = version;
|
|
|
|
entrypoint = pkgs.writeShellScript "shower-entrypoint" ''
|
|
set -eu
|
|
|
|
APP_DIR=/app
|
|
DATA_DIR=/app/data
|
|
VENV_DIR=$DATA_DIR/.venv
|
|
INSTALLED_MARKER=$VENV_DIR/.installed-${appVersion}
|
|
|
|
export HOME=$DATA_DIR
|
|
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
export PIP_NO_CACHE_DIR=1
|
|
|
|
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'
|
|
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
|
|
export DJANGO_SETTINGS_MODULE=local_settings
|
|
|
|
cd "$APP_DIR"
|
|
|
|
echo "shower: running migrations"
|
|
"$VENV_DIR/bin/python" -m django migrate --noinput
|
|
|
|
echo "shower: collecting static files"
|
|
"$VENV_DIR/bin/python" -m django collectstatic --noinput --clear
|
|
|
|
echo "shower: starting gunicorn"
|
|
exec "$VENV_DIR/bin/gunicorn" \
|
|
--bind 0.0.0.0:8000 \
|
|
--workers 2 \
|
|
--forwarded-allow-ips='*' \
|
|
config.wsgi:application
|
|
'';
|
|
in
|
|
|
|
pkgs.dockerTools.buildLayeredImage {
|
|
name = "blumeops/shower";
|
|
contents = [
|
|
python
|
|
pkgs.cacert
|
|
pkgs.tzdata
|
|
pkgs.bashInteractive
|
|
pkgs.coreutils
|
|
pkgs.gnused
|
|
pkgs.gnugrep
|
|
];
|
|
|
|
# /app is writable by uid 1000 (matches deployment.yaml runAsUser).
|
|
fakeRootCommands = ''
|
|
mkdir -p app/data app/media tmp
|
|
chmod 1777 tmp
|
|
chown -R 1000:1000 app
|
|
'';
|
|
enableFakechroot = true;
|
|
|
|
config = {
|
|
Entrypoint = [ "${entrypoint}" ];
|
|
Env = [
|
|
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
|
"TZDIR=${pkgs.tzdata}/share/zoneinfo"
|
|
"TZ=America/Los_Angeles"
|
|
"TMPDIR=/tmp"
|
|
"LANG=C.UTF-8"
|
|
"LC_ALL=C.UTF-8"
|
|
];
|
|
ExposedPorts = {
|
|
"8000/tcp" = { };
|
|
};
|
|
User = "1000";
|
|
WorkingDir = "/app";
|
|
};
|
|
}
|