C1: deploy adelaide-baby-shower-app to ringtail k3s #349
1 changed files with 93 additions and 33 deletions
C1: switch shower container to pip-install FOD
The buildPythonPackage approach with `propagatedBuildInputs = [ python.pkgs.django ... ]` doesn't work:
1. nixpkgs python314Packages.django still aliases to Django 4.2 LTS,
which doesn't support Python 3.14.
2. django-axes from nixpkgs pulls selenium + browser fonts into its
check phase, and the nix sandbox can't provide those (fontconfig
errors, then build dep tree collapses).
Switching to authentik's FOD pattern instead: a single fixed-output
derivation that pip-installs the adelaide-baby-shower-app wheel + every
transitive dep from forge PyPI into a target dir. FODs get network
access in exchange for a pinned output hash, so the closure stays
reproducible.
outputHash is set to fakeHash for the first build — the runner will
print the real hash on failure; a follow-up commit will pin it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
ba4c1e8953
|
|
@ -2,51 +2,108 @@
|
||||||
#
|
#
|
||||||
# The app is published as a wheel to the Forgejo PyPI index at
|
# The app is published as a wheel to the Forgejo PyPI index at
|
||||||
# https://forge.eblu.me/api/packages/eblume/pypi/. The wheel + its
|
# https://forge.eblu.me/api/packages/eblume/pypi/. The wheel + its
|
||||||
# Python deps are baked in at build time via buildPythonPackage so the
|
# transitive Python deps are baked in at build time via a fixed-output
|
||||||
# container boots cleanly with no pip-at-runtime. Build runs on the
|
# 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.
|
# 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:
|
# To bump the version:
|
||||||
# 1. Update `version` below.
|
# 1. Update `version` below.
|
||||||
# 2. Update `wheelHash` — `nix-prefetch-url <url>` against the new wheel,
|
# 2. Set `outputHash` to `pkgs.lib.fakeHash`, run the build, copy the
|
||||||
# or set it to `pkgs.lib.fakeHash` and let the build print the right one.
|
# real hash out of the error, and commit it.
|
||||||
{ pkgs ? import <nixpkgs> { } }:
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
let
|
let
|
||||||
version = "1.0.0";
|
version = "1.0.0";
|
||||||
wheelHash = "sha256-9Xk3TCzl474As8n0RhLoy/QYw+K1DABBWEwLC8v1X0A=";
|
|
||||||
|
|
||||||
python = pkgs.python314;
|
python = pkgs.python314;
|
||||||
|
|
||||||
showerWheel = pkgs.fetchurl {
|
# Fixed-output derivation: pip-installs the app wheel + every transitive
|
||||||
name = "adelaide_baby_shower_app-${version}-py3-none-any.whl";
|
# dep into a single target dir. FODs get network access in exchange for
|
||||||
url = "https://forge.eblu.me/api/packages/eblume/pypi/files/adelaide-baby-shower-app/${version}/adelaide_baby_shower_app-${version}-py3-none-any.whl";
|
# a pinned output hash, which means the whole dependency closure is
|
||||||
hash = wheelHash;
|
# immutable across rebuilds.
|
||||||
};
|
pyDeps = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "shower-python-deps";
|
||||||
shower = python.pkgs.buildPythonPackage {
|
|
||||||
pname = "adelaide-baby-shower-app";
|
|
||||||
inherit version;
|
inherit version;
|
||||||
format = "wheel";
|
|
||||||
src = showerWheel;
|
dontUnpack = true;
|
||||||
doCheck = false;
|
|
||||||
propagatedBuildInputs = with python.pkgs; [
|
nativeBuildInputs = [ python pkgs.cacert ];
|
||||||
django
|
|
||||||
django-axes
|
buildPhase = ''
|
||||||
pillow
|
runHook preBuild
|
||||||
scipy
|
|
||||||
segno
|
export HOME=$TMPDIR
|
||||||
];
|
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||||
|
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
# Install into a venv first so pip's bytecode-compile + entry-point
|
||||||
|
# generation pick up the right interpreter, then copy site-packages
|
||||||
|
# + bin into $out at a stable layout.
|
||||||
|
${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
|
||||||
|
|
||||||
|
# Copy console scripts (gunicorn, django-admin, etc.) but drop the
|
||||||
|
# venv-specific shebang prefix that points at $TMPDIR/venv/bin/python.
|
||||||
|
# Rewrite shebangs to the eventual on-image python path.
|
||||||
|
for script in "$TMPDIR/venv/bin/"*; do
|
||||||
|
[ -f "$script" ] || continue
|
||||||
|
name=$(basename "$script")
|
||||||
|
case "$name" in
|
||||||
|
python*|pip*|activate*) continue ;;
|
||||||
|
esac
|
||||||
|
# Replace the venv python shebang with a path that resolves inside
|
||||||
|
# the docker image (where ${python} ends up in /nix/store).
|
||||||
|
sed -e "1 s|^#!.*python.*|#!${python}/bin/python3.14|" "$script" > "$out/bin/$name"
|
||||||
|
chmod +x "$out/bin/$name"
|
||||||
|
done
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Bytecode files embed absolute paths; deletion forces re-compile inside
|
||||||
|
# the image at first run, with paths matching the image filesystem.
|
||||||
|
postInstall = ''
|
||||||
|
find $out -type f -name '*.pyc' -delete
|
||||||
|
find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
'';
|
||||||
|
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
# Computed by setting to pkgs.lib.fakeHash and reading the failure.
|
||||||
|
# Pin the dep closure — rebuilds are reproducible until the version bumps.
|
||||||
|
outputHash = pkgs.lib.fakeHash;
|
||||||
|
|
||||||
|
dontFixup = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
pyEnv = python.withPackages (ps: [
|
sitePackages = "${pyDeps}/lib/python3.14/site-packages";
|
||||||
shower
|
|
||||||
ps.gunicorn
|
|
||||||
]);
|
|
||||||
|
|
||||||
# Settings shim — config/settings.py's `BASE_DIR = parent.parent` would
|
# Settings shim — config/settings.py's `BASE_DIR = parent.parent` would
|
||||||
# otherwise resolve to site-packages, scattering db.sqlite3 / media /
|
# otherwise resolve to site-packages, scattering db.sqlite3 / media /
|
||||||
# staticfiles into the venv. Pin them to /app/{data,media,data/static}.
|
# staticfiles into the venv. Pin them to /app/{data,media,data/staticfiles}.
|
||||||
localSettings = pkgs.writeText "local_settings.py" ''
|
localSettings = pkgs.writeText "local_settings.py" ''
|
||||||
from config.settings import * # noqa: F401,F403
|
from config.settings import * # noqa: F401,F403
|
||||||
|
|
||||||
|
|
@ -59,7 +116,8 @@ let
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
export HOME=/app/data
|
export HOME=/app/data
|
||||||
export PYTHONPATH=/app
|
export PATH=${pyDeps}/bin:${python}/bin:/bin
|
||||||
|
export PYTHONPATH=/app:${sitePackages}
|
||||||
export DJANGO_SETTINGS_MODULE=local_settings
|
export DJANGO_SETTINGS_MODULE=local_settings
|
||||||
|
|
||||||
cd /app
|
cd /app
|
||||||
|
|
@ -67,13 +125,13 @@ let
|
||||||
mkdir -p /app/data /app/media
|
mkdir -p /app/data /app/media
|
||||||
|
|
||||||
echo "shower: running migrations"
|
echo "shower: running migrations"
|
||||||
${pyEnv}/bin/python -m django migrate --noinput
|
${python}/bin/python -m django migrate --noinput
|
||||||
|
|
||||||
echo "shower: collecting static files"
|
echo "shower: collecting static files"
|
||||||
${pyEnv}/bin/python -m django collectstatic --noinput --clear
|
${python}/bin/python -m django collectstatic --noinput --clear
|
||||||
|
|
||||||
echo "shower: starting gunicorn"
|
echo "shower: starting gunicorn"
|
||||||
exec ${pyEnv}/bin/gunicorn \
|
exec ${pyDeps}/bin/gunicorn \
|
||||||
--bind 0.0.0.0:8000 \
|
--bind 0.0.0.0:8000 \
|
||||||
--workers 2 \
|
--workers 2 \
|
||||||
--forwarded-allow-ips='*' \
|
--forwarded-allow-ips='*' \
|
||||||
|
|
@ -84,7 +142,8 @@ in
|
||||||
pkgs.dockerTools.buildLayeredImage {
|
pkgs.dockerTools.buildLayeredImage {
|
||||||
name = "blumeops/shower";
|
name = "blumeops/shower";
|
||||||
contents = [
|
contents = [
|
||||||
pyEnv
|
python
|
||||||
|
pyDeps
|
||||||
pkgs.cacert
|
pkgs.cacert
|
||||||
pkgs.tzdata
|
pkgs.tzdata
|
||||||
pkgs.bashInteractive
|
pkgs.bashInteractive
|
||||||
|
|
@ -111,6 +170,7 @@ pkgs.dockerTools.buildLayeredImage {
|
||||||
"TMPDIR=/tmp"
|
"TMPDIR=/tmp"
|
||||||
"LANG=C.UTF-8"
|
"LANG=C.UTF-8"
|
||||||
"LC_ALL=C.UTF-8"
|
"LC_ALL=C.UTF-8"
|
||||||
|
"PYTHONDONTWRITEBYTECODE=1"
|
||||||
];
|
];
|
||||||
ExposedPorts = {
|
ExposedPorts = {
|
||||||
"8000/tcp" = { };
|
"8000/tcp" = { };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue