C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
# Nix-built shower app container — Adelaide / Heidi / Addie baby shower.
|
|
|
|
|
#
|
|
|
|
|
# The app is published as a wheel to the Forgejo PyPI index at
|
2026-05-11 08:37:12 -07:00
|
|
|
# https://forge.eblu.me/api/packages/eblume/pypi/. The wheel + its
|
2026-05-11 09:00:26 -07:00
|
|
|
# transitive Python deps are baked in at build time via a fixed-output
|
|
|
|
|
# derivation that runs `pip install --target` against forge PyPI (proxied
|
|
|
|
|
# through pypi.ops.eblu.me for upstream packages). Build runs on the
|
2026-05-11 08:37:12 -07:00
|
|
|
# nix-container-builder runner (ringtail, amd64) so the image is native.
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
#
|
2026-05-11 09:00:26 -07:00
|
|
|
# Going through pip-install-target rather than nixpkgs Python packages
|
|
|
|
|
# sidesteps two issues we hit going through `python.pkgs.buildPythonPackage`:
|
|
|
|
|
# 1. python314Packages.django still aliases to Django 4.2 LTS, which
|
|
|
|
|
# doesn't support Python 3.14 at all.
|
|
|
|
|
# 2. django-axes pulls selenium + browser fonts into its check phase
|
|
|
|
|
# and the nix sandbox can't provide those.
|
|
|
|
|
#
|
2026-05-11 08:37:12 -07:00
|
|
|
# To bump the version:
|
|
|
|
|
# 1. Update `version` below.
|
2026-05-11 09:00:26 -07:00
|
|
|
# 2. Set `outputHash` to `pkgs.lib.fakeHash`, run the build, copy the
|
|
|
|
|
# real hash out of the error, and commit it.
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
{ pkgs ? import <nixpkgs> { } }:
|
|
|
|
|
|
|
|
|
|
let
|
2026-05-11 10:23:40 -07:00
|
|
|
version = "1.0.1";
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
|
|
|
|
python = pkgs.python314;
|
|
|
|
|
|
2026-05-11 09:00:26 -07:00
|
|
|
# Fixed-output derivation: pip-installs the app wheel + every transitive
|
|
|
|
|
# dep into a single target dir. FODs get network access in exchange for
|
|
|
|
|
# a pinned output hash, which means the whole dependency closure is
|
|
|
|
|
# immutable across rebuilds.
|
2026-05-11 09:06:44 -07:00
|
|
|
pyDepsFOD = pkgs.stdenv.mkDerivation {
|
|
|
|
|
pname = "shower-python-deps-fod";
|
2026-05-11 08:37:12 -07:00
|
|
|
inherit version;
|
2026-05-11 09:00:26 -07:00
|
|
|
|
|
|
|
|
dontUnpack = true;
|
|
|
|
|
|
2026-05-11 09:06:44 -07:00
|
|
|
nativeBuildInputs = [ python pkgs.cacert pkgs.removeReferencesTo ];
|
2026-05-11 09:00:26 -07:00
|
|
|
|
|
|
|
|
buildPhase = ''
|
|
|
|
|
runHook preBuild
|
|
|
|
|
|
|
|
|
|
export HOME=$TMPDIR
|
|
|
|
|
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
|
|
|
|
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
|
|
|
|
|
|
|
|
${python}/bin/python -m venv "$TMPDIR/venv"
|
|
|
|
|
"$TMPDIR/venv/bin/pip" install --upgrade pip
|
|
|
|
|
"$TMPDIR/venv/bin/pip" install \
|
|
|
|
|
--no-cache-dir \
|
|
|
|
|
--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==${version}" \
|
|
|
|
|
gunicorn
|
|
|
|
|
|
|
|
|
|
runHook postBuild
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
installPhase = ''
|
|
|
|
|
runHook preInstall
|
|
|
|
|
|
|
|
|
|
mkdir -p $out/lib/python3.14 $out/bin
|
|
|
|
|
cp -r "$TMPDIR/venv/lib/python3.14/site-packages" $out/lib/python3.14/site-packages
|
|
|
|
|
|
|
|
|
|
for script in "$TMPDIR/venv/bin/"*; do
|
|
|
|
|
[ -f "$script" ] || continue
|
|
|
|
|
name=$(basename "$script")
|
|
|
|
|
case "$name" in
|
|
|
|
|
python*|pip*|activate*) continue ;;
|
|
|
|
|
esac
|
2026-05-11 09:06:44 -07:00
|
|
|
cp "$script" "$out/bin/$name"
|
2026-05-11 09:00:26 -07:00
|
|
|
chmod +x "$out/bin/$name"
|
|
|
|
|
done
|
|
|
|
|
|
2026-05-11 09:06:44 -07:00
|
|
|
# --- Strip Nix store references (FOD outputs must be self-contained) ---
|
|
|
|
|
# The wrapper derivation below restores them via autoPatchelfHook + a
|
|
|
|
|
# python wrapper that points pyc-less imports at the on-image python.
|
2026-05-11 09:00:26 -07:00
|
|
|
|
2026-05-11 09:06:44 -07:00
|
|
|
# Strip bytecode entirely — pyc files embed compile-time paths.
|
2026-05-11 09:00:26 -07:00
|
|
|
find $out -type f -name '*.pyc' -delete
|
|
|
|
|
find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
|
2026-05-11 09:06:44 -07:00
|
|
|
|
|
|
|
|
# Dynamically discover all nix store references and strip them. We
|
|
|
|
|
# don't have a static list because pip pulls in stdenv via Python's
|
|
|
|
|
# build env (gcc-lib, libstdc++, etc.) and the closure is opaque.
|
|
|
|
|
{ find $out -type f -print0 \
|
|
|
|
|
| xargs -0 grep -aohE '/nix/store/[a-z0-9]{32}-[^/"[:space:]]+' 2>/dev/null \
|
|
|
|
|
|| true; } | sort -u > $TMPDIR/store-refs.txt
|
|
|
|
|
echo "Found $(wc -l < $TMPDIR/store-refs.txt) unique store path references to strip"
|
|
|
|
|
|
|
|
|
|
refs_args=""
|
|
|
|
|
while IFS= read -r ref; do
|
|
|
|
|
refs_args="$refs_args -t $ref"
|
|
|
|
|
done < $TMPDIR/store-refs.txt
|
|
|
|
|
|
|
|
|
|
if [ -n "$refs_args" ]; then
|
|
|
|
|
find $out -type f -exec remove-references-to $refs_args {} + 2>/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
remaining=$({ find $out -type f -print0 | xargs -0 grep -cl '/nix/store/' 2>/dev/null || true; } | wc -l)
|
|
|
|
|
echo "Files with remaining store references: $remaining"
|
|
|
|
|
|
|
|
|
|
runHook postInstall
|
2026-05-11 09:00:26 -07:00
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
outputHashMode = "recursive";
|
|
|
|
|
outputHashAlgo = "sha256";
|
2026-05-11 09:13:53 -07:00
|
|
|
# Pinned dep closure — reproducible until version bumps. To recompute,
|
|
|
|
|
# set to pkgs.lib.fakeHash and read the failure.
|
2026-05-11 10:23:40 -07:00
|
|
|
outputHash = pkgs.lib.fakeHash;
|
2026-05-11 09:00:26 -07:00
|
|
|
|
|
|
|
|
dontFixup = true;
|
2026-05-11 08:37:12 -07:00
|
|
|
};
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
2026-05-11 09:06:44 -07:00
|
|
|
# Non-FOD wrapper: re-applies RPATHs to pre-built .so files (pillow,
|
|
|
|
|
# scipy) so they find libstdc++ / libz / etc. at runtime. autoPatchelfHook
|
|
|
|
|
# discovers needed libraries from buildInputs.
|
|
|
|
|
pyDeps = pkgs.stdenv.mkDerivation {
|
|
|
|
|
pname = "shower-python-deps";
|
|
|
|
|
inherit version;
|
|
|
|
|
|
|
|
|
|
dontUnpack = true;
|
|
|
|
|
|
|
|
|
|
nativeBuildInputs = [ pkgs.autoPatchelfHook ];
|
|
|
|
|
|
|
|
|
|
buildInputs = with pkgs; [
|
|
|
|
|
python
|
|
|
|
|
stdenv.cc.cc.lib # libstdc++, libgcc_s
|
|
|
|
|
zlib
|
|
|
|
|
libjpeg
|
|
|
|
|
libwebp
|
|
|
|
|
libtiff
|
|
|
|
|
openjpeg
|
|
|
|
|
lcms2
|
|
|
|
|
freetype
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
installPhase = ''
|
|
|
|
|
cp -r ${pyDepsFOD} $out
|
|
|
|
|
chmod -R u+w $out
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-11 09:00:26 -07:00
|
|
|
sitePackages = "${pyDeps}/lib/python3.14/site-packages";
|
2026-05-11 08:37:12 -07:00
|
|
|
|
|
|
|
|
# Settings shim — config/settings.py's `BASE_DIR = parent.parent` would
|
|
|
|
|
# otherwise resolve to site-packages, scattering db.sqlite3 / media /
|
2026-05-11 09:00:26 -07:00
|
|
|
# staticfiles into the venv. Pin them to /app/{data,media,data/staticfiles}.
|
2026-05-11 08:37:12 -07:00
|
|
|
localSettings = pkgs.writeText "local_settings.py" ''
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
from config.settings import * # noqa: F401,F403
|
|
|
|
|
|
|
|
|
|
DATABASES["default"]["NAME"] = "/app/data/db.sqlite3"
|
|
|
|
|
MEDIA_ROOT = "/app/media"
|
|
|
|
|
STATIC_ROOT = "/app/data/staticfiles"
|
2026-05-11 08:37:12 -07:00
|
|
|
'';
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
2026-05-11 08:37:12 -07:00
|
|
|
entrypoint = pkgs.writeShellScript "shower-entrypoint" ''
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
export HOME=/app/data
|
2026-05-11 09:00:26 -07:00
|
|
|
export PATH=${pyDeps}/bin:${python}/bin:/bin
|
|
|
|
|
export PYTHONPATH=/app:${sitePackages}
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
export DJANGO_SETTINGS_MODULE=local_settings
|
|
|
|
|
|
2026-05-11 08:37:12 -07:00
|
|
|
cd /app
|
|
|
|
|
|
|
|
|
|
mkdir -p /app/data /app/media
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
|
|
|
|
echo "shower: running migrations"
|
2026-05-11 09:00:26 -07:00
|
|
|
${python}/bin/python -m django migrate --noinput
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
|
|
|
|
echo "shower: collecting static files"
|
2026-05-11 09:00:26 -07:00
|
|
|
${python}/bin/python -m django collectstatic --noinput --clear
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
|
|
|
|
|
echo "shower: starting gunicorn"
|
2026-05-11 09:00:26 -07:00
|
|
|
exec ${pyDeps}/bin/gunicorn \
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
--bind 0.0.0.0:8000 \
|
|
|
|
|
--workers 2 \
|
|
|
|
|
--forwarded-allow-ips='*' \
|
|
|
|
|
config.wsgi:application
|
|
|
|
|
'';
|
|
|
|
|
in
|
|
|
|
|
|
|
|
|
|
pkgs.dockerTools.buildLayeredImage {
|
|
|
|
|
name = "blumeops/shower";
|
|
|
|
|
contents = [
|
2026-05-11 09:00:26 -07:00
|
|
|
python
|
|
|
|
|
pyDeps
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
pkgs.cacert
|
|
|
|
|
pkgs.tzdata
|
|
|
|
|
pkgs.bashInteractive
|
|
|
|
|
pkgs.coreutils
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-11 08:37:12 -07:00
|
|
|
extraCommands = ''
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
mkdir -p app/data app/media tmp
|
|
|
|
|
chmod 1777 tmp
|
2026-05-11 08:37:12 -07:00
|
|
|
cp ${localSettings} app/local_settings.py
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
fakeRootCommands = ''
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
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"
|
2026-05-11 09:00:26 -07:00
|
|
|
"PYTHONDONTWRITEBYTECODE=1"
|
C1: deploy adelaide-baby-shower-app to ringtail k3s
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>
2026-05-11 08:14:12 -07:00
|
|
|
];
|
|
|
|
|
ExposedPorts = {
|
|
|
|
|
"8000/tcp" = { };
|
|
|
|
|
};
|
|
|
|
|
User = "1000";
|
|
|
|
|
WorkingDir = "/app";
|
|
|
|
|
};
|
|
|
|
|
}
|