# 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/. The wheel + its # 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 # nix-container-builder runner (ringtail, amd64) so the image is native. # # 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. # # To bump the version: # 1. Update `version` below. # 2. Set `outputHash` to `pkgs.lib.fakeHash`, run the build, copy the # real hash out of the error, and commit it. { pkgs ? import { } }: let version = "1.0.1"; python = pkgs.python314; # 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. pyDepsFOD = pkgs.stdenv.mkDerivation { pname = "shower-python-deps-fod"; inherit version; dontUnpack = true; nativeBuildInputs = [ python pkgs.cacert pkgs.removeReferencesTo ]; 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 cp "$script" "$out/bin/$name" chmod +x "$out/bin/$name" done # --- 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. # Strip bytecode entirely — pyc files embed compile-time paths. find $out -type f -name '*.pyc' -delete find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true # 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 ''; outputHashMode = "recursive"; outputHashAlgo = "sha256"; # Pinned dep closure — reproducible until version bumps. To recompute, # set to pkgs.lib.fakeHash and read the failure. outputHash = "sha256-9J2cxNxFSkSWLy2nwPtqV0Qtn54YVJ3LiijsqKQT+uc="; dontFixup = true; }; # 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 ''; }; sitePackages = "${pyDeps}/lib/python3.14/site-packages"; # 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/staticfiles}. 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" ''; entrypoint = pkgs.writeShellScript "shower-entrypoint" '' set -eu export HOME=/app/data export PATH=${pyDeps}/bin:${python}/bin:/bin export PYTHONPATH=/app:${sitePackages} export DJANGO_SETTINGS_MODULE=local_settings cd /app mkdir -p /app/data /app/media echo "shower: running migrations" ${python}/bin/python -m django migrate --noinput echo "shower: collecting static files" ${python}/bin/python -m django collectstatic --noinput --clear echo "shower: starting gunicorn" exec ${pyDeps}/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 pyDeps pkgs.cacert pkgs.tzdata pkgs.bashInteractive pkgs.coreutils ]; 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; 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" "PYTHONDONTWRITEBYTECODE=1" ]; ExposedPorts = { "8000/tcp" = { }; }; User = "1000"; WorkingDir = "/app"; }; }