# LLeMbas in a container. # # One stage, on purpose. There is nothing to build: no Node, no compiled assets, # no wheel worth producing separately — the vendored browser libraries are # committed and the templates are read at runtime. A multi-stage build here # would be ceremony that saves nothing and hides where the files came from. # # **This image is not a deployment on its own.** It serves plain HTTP and expects # a TLS reverse proxy in front, and that is a constraint rather than a # preference: a service worker and a microphone both require HTTPS or localhost, # so over plain http on a LAN address the app installs as nothing and cannot # dictate. See deploy/README.md. FROM python:3.12-slim # `bash` and `git` earn their place: `git` is what /admin/updates reads to say # what is running, and its absence there is reported rather than crashed on. # `curl` is the healthcheck below. Everything else stays out. RUN apt-get update \ && apt-get install --no-install-recommends -y git curl \ && rm -rf /var/lib/apt/lists/* # A real account rather than root, and made before the install so the layers it # owns are its own. 10001 rather than the first free id: a bind-mounted volume # on the host is easier to reason about when the id is stated. RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin lembas WORKDIR /app # The dependency install is its own layer, keyed on the files that decide it, so # editing a template does not re-resolve the whole tree. # # LICENSE is in the list because `pyproject.toml` declares `license = { file = # "LICENSE" }` and the build backend reads it -- without it the install fails # with "License file does not exist", which reads like a packaging problem and # is a missing COPY. README.md is there for the same reason (`readme = `). COPY pyproject.toml README.md LICENSE ./ COPY src/lembas/__init__.py src/lembas/__init__.py RUN pip install --no-cache-dir -e ".[search,ssh]" COPY . . # Again, because the first install ran against a source tree with one file in # it. Cheap: everything is already resolved and cached above. RUN pip install --no-cache-dir --no-deps -e "." \ && chown -R lembas:lembas /app # The database, the uploads and the encryption at rest all live here. Declared # so that running without `-v` still works and says where the data went, rather # than losing it silently at the first `docker rm`. ENV LEMBAS_DATA_DIR=/data \ LEMBAS_HOST=0.0.0.0 \ LEMBAS_PORT=8080 \ PYTHONUNBUFFERED=1 RUN install -d -o lembas -g lembas /data VOLUME ["/data"] # **No secret key is baked in.** One in an image is one every copy of the image # shares, and rotating it signs everybody out *and* makes stored upstream API # keys unreadable. Without LEMBAS_SECRET_KEY the app generates a temporary one # and warns loudly at startup, which is the right failure: it works for a look # and cannot be mistaken for a deployment. USER lembas EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1 CMD ["lembas", "serve"]