facett-ops/src/containerfiles/streamlit.Containerfile
# nordisk STREAMLIT — where the facett-demo Streamlit app RUNS.
#
# Rickard's rule for this lane, verbatim: "use a CONTAINER for the horrible python
# language". Streamlit is python, its dependency tree is python, and none of it goes
# on oden. This image is where the demo lives.
#
# THE SPLIT THAT MATTERS. Two different networks, at two different times:
# * BUILD time — reaches PyPI once, to install the FRAMEWORK (streamlit and its
# tree). That is a one-off, and the resulting image is the pin.
# * RUN time — no PyPI. The two RELEASES under test, the `facett` and `facett-demo`
# wheels (0.2.0), are `pip install`ed THEN — from the built wheelhouse bound in
# read-only (`--no-index`), or from holger's PEP 503 index on 127.0.0.1:8455 —
# and the app is started by the INSTALLED `facett-demo` console script. They are
# deliberately NOT baked in: an image with the wheels already inside it could not
# tell us whether the release works. grpcio + protobuf below are what the
# wheel's optional `[korp]` extra asks for, so it resolves with no network.
#
# Base pinned by digest, same instant as the other two images in this lane.
FROM debian@sha256:6788062a1b42ac281f053ac876170b79a3eaed5d61383b8ed7eaca6c6965f3b1
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
`# step: RUN — the interpreter the app and the wheel are imported by.` \
python3 \
`# step: INSTALL — pip, for the framework at build time and the artifact at run time.` \
python3-pip \
`# step: INSTALL — Debian marks the system python EXTERNALLY-MANAGED (PEP 668), so` \
`# everything lives in a venv. That is also what a consumer does, so the demo and` \
`# the real world agree.` \
python3-venv \
`# step: INSTALL — streamlit pulls packages that fetch over TLS at install time;` \
`# without a trust root pip fails on the certificate, not on the package.` \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# The venv every later step uses. On PATH so nothing has to remember the prefix.
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv "$VIRTUAL_ENV"
ENV PATH="/opt/venv/bin:$PATH"
# The FRAMEWORK, pinned. Not the artifact — that arrives over our own index at run
# time, because installing it here would hide exactly what this lane is testing.
#
# `protobuf` and `grpcio` are here for the LIVE panes (the korp-server gRPC client).
# The Gallery pane needs neither, which is the point of it: the demo has something
# true to render with no backend at all.
# Versions come from `py/requirements.txt`, which is the one writer for what this app
# runs on ("verified installed on Python 3.14, oden"). They are repeated here as ARGs
# rather than read from the file because the requirements file is a RUN-time bind
# mount and this is a BUILD-time step — `xtask/tests/streamlit_image_matches_requirements.rs`
# is what keeps the two from drifting.
ARG STREAMLIT_VERSION=1.60.0
RUN pip install --no-cache-dir --disable-pip-version-check \
"streamlit==${STREAMLIT_VERSION}" \
"grpcio==1.83.0" \
`# step: TEST — the demo own suite runs IN HERE too. No-python-on-the-host` \
`# is not only about the build: a pytest run on oden would be the same rule` \
`# broken at a different hour. Pinned from py/requirements.txt like the rest.` \
"pytest==9.1.1" \
`# step: TEST — the WIRE guards and the regenerator both need protoc. This` \
`# line is new (2026-09-18) and it closes a claim the file was already` \
`# making: py/requirements.txt lists grpcio-tools under "also installed in` \
`# the image — the suite runs there too", and it was not. The check below` \
`# said so and swallowed the answer, so an image without protoc reported` \
`# itself healthy, the stub regenerator could not run in the one place` \
`# python is allowed to run, and the stubs drifted 25 RPCs behind korp` \
`# with nothing able to regenerate them.` \
`# NOTE: no backticks inside these comments. The trick is a shell command` \
`# substitution used as a comment, so a nested backtick CLOSES it early and` \
`# the rest of the line becomes live shell. That is not hypothetical: the` \
`# first version of this paragraph quoted a two-word shell fragment, which` \
`# ended the pip install before grpcio-tools and swallowed the failure.` \
"grpcio-tools==1.83.0" \
"protobuf==7.35.1"
# ── THE IMAGE ASSERTS ITS OWN CONTENTS ──────────────────────────────────────
#
# Same lesson the other two recipes carry: a check that cannot fail is
# indistinguishable from a law being obeyed. `import streamlit` would pass on a
# broken install of a framework that cannot start; this runs its CLI and asserts the
# version it reports is the version that was asked for.
RUN set -eu; \
got="$(streamlit version 2>&1 | tr -d '\r')"; \
echo "$got" | grep -q "${STREAMLIT_VERSION}" \
|| { echo "FATAL: asked for streamlit ${STREAMLIT_VERSION}, image reports '$got'"; exit 1; }; \
`# No swallowing here either. A check that cannot fail is indistinguishable` \
`# from a law being obeyed — this file says so at the top and then made the` \
`# exception on this very line, which is how the missing protoc survived.` \
python3 -c "import grpc_tools.protoc"; \
python3 -c "import google.protobuf, grpc; print('grpc ok')"
# Streamlit writes ~/.streamlit and a usage-stats file; give it a home that exists
# for whatever uid the engine runs as, and turn the telemetry off — a demo box does
# not phone anywhere.
#
# STREAMLIT_SERVER_ADDRESS IS LOOPBACK ONLY, and that is not a preference. The demo
# runs with `--network host` (so it can reach holger on 127.0.0.1:8455) — which also
# means the HOST's real interfaces are this container's. MEASURED 2026-09-05: with
# 0.0.0.0, streamlit announced
#
# External URL: http://80.217.64.156:8501
#
# and the demo was reachable FROM THE INTERNET, on a development workstation holding
# every repo in the constellation, for as long as it ran. Nothing about a demo is
# authenticated. A demo is not a service.
#
# (These are ENV lines, so the backtick-comment trick the RUN steps use does not
# work here — ENV is parsed by the builder, not by a shell.)
ENV HOME=/tmp \
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
STREAMLIT_SERVER_HEADLESS=true \
STREAMLIT_SERVER_ADDRESS=127.0.0.1 \
PYTHONDONTWRITEBYTECODE=1
# Nothing from the repo is mounted except the wheelhouse (read-only, /wheels): the
# container installs the release and runs it from site-packages.
WORKDIR /tmp