Trim the image: 906 MB, measured, and where it goes #157
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
Reference
Postulo/postulo#157
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
Measured on the test instance against
postulo:latestas built from0.3.0atd0cc4c8,arm64. Total: 906 MB.
Where it actually is
By layer:
uv sync --locked --no-dev --extra serverapt-get install— pango, harfbuzz, fontconfig, dejavu, noto, curlCOPY /uv /usr/local/bin/uvdpkg-dev,g++)COPY src ./srccollectstaticmessages.py compileBy directory in the finished image:
/app/.venv/root/.cache/uv/usr/local/bin/uv/app/srclocale/25 MB — 17 MB of.po, 6.6 MB of.mo/app/staticfilesWhat can go, in order of size against difficulty
UV_NO_CACHE=1, or clear it in the same layere2egroup--no-default-groupsuvbinary.pocatalogues.mo.poat runtimePOSTULO_ADMIN_URL#154 alone takes the image from 906 MB to roughly 430 MB. The rest of this is measured
against what would be left.
The structural change that makes most of it automatic
The Python side of the build is single-stage. Only the CSS uses a build stage
(
FROM node:22-bookworm-slim AS styles); everything else —uv sync,messages.py compile,collectstatic— happens in the image that ships. So every build artefact is baked into alayer, and a later
RUN rmfrees nothing because the bytes are already below.A second stage that copies in only what runs — the
.venv,/app/srcminus the.pofiles,staticfiles, and the fonts — drops the cache and the source catalogues by construction, andmakes shipping
uva decision rather than a side effect. That is the change worth making;the individual flags above are what to do if it is not.
Worth being careful about
uvis load-bearing and its 45 MB is not free to reclaim.plugins/installing.pydoesshutil.which("uv")and prefers it "where the image put it", falling back to pip. Droppingit does not break plugin installation; it makes it slower and changes which resolver an
operator gets. That is a real trade to state rather than a saving to claim.
The fonts are the one large thing that has to stay.
fonts-noto-coreis why an Amharic,Georgian or Tigrinya CV renders glyphs instead of empty boxes, and #70 added it with a test
holding the image's fonts to the languages offered. Trimming here would be trimming the
languages.
The flags are 1476 files for 245 flags, because the manifest storage keeps a hashed copy
of each and WhiteNoise adds
.gzand.brbeside both. That is the storage doing its job —it is 9.7 MB and it is served pre-compressed — but it is worth knowing before somebody
concludes the flag set is enormous. Every one of the 245 is reachable: the telephone field
offers a flag per country.
The admin's 6.6 MB is awkward rather than easy.
django.contrib.adminis installed —through
PostuloAdminConfig, which is the same app with a rate-limited login — socollectstaticcollects its assets whether or notPOSTULO_ADMIN_URLis set. Butcollectstaticruns at build time and the admin's URL is a runtime environment variable, sothe image cannot know. Either it is a build argument, or it stays.
A smaller base is a separate lever with a separate benefit.
python:3.14-slim-trixiewould cut some of the 97 MB rootfs and, more usefully, reset most of the unfixable CVE
backlog in #155. Distroless is the aggressive version and would fight WeasyPrint's system
libraries, which are exactly what a distroless image does not have.
Smaller is not the goal; fewer moving parts is. 906 MB on a Raspberry Pi is a slow pull
and a disk that fills sooner, and every megabyte that is not there is a thing that cannot be
vulnerable — but the reason to do this is that the image should contain what it runs, which
is also what makes a scan (#156) mean something.
The issue named the change worth making and this makes that one:
Three stages now.
stylesbuilds the CSS,buildbuilds the environment and compiles thecatalogues,
runtimecopies in the.venv, the source andmanage.py— and nothing thatmade them.
The distinction the whole thing turns on: the same
RUN rmfrees 20 MB in a stage thatis discarded and nothing at all in the stage that ships, because there the bytes are
already in a layer below. That is why the
.pofiles could not be deleted before and canbe now.
What it takes
e2egroup.pocataloguesPlus what stops shipping by construction: the lock, the manifest,
README.md,scripts/messages.py, and whatever git the extra-packages step needed. Two side effectsworth having — that step no longer purges git, because git is left behind with the stage;
and
collectstaticruns as plainpython, so the lock and manifest are no longer presentin the shipping image purely so
uv runwould agree to start.status.jsonstays. It sits among the.pofiles and the language picker reads it to sayhow far along each translation is, so the deletion is by extension rather than by directory
— and a test says so, because that is exactly the sort of thing a later tidy-up gets wrong.
Two savings deliberately not taken
uv, 45 MB. The issue put it correctly — "a real trade to state rather than a saving to
claim".
plugins/installing.pyprefers it where the image put it and falls back to pip.Dropping it does not break installing a plugin through the interface; it makes it slower and
hands an operator a resolver this project does not test with. A working feature beats 45 MB
of a ~400 MB image.
tests/test_image_build.pyfails if the binary goes without the reasonbeside it going too.
Django admin's 6.6 MB. The issue's own framing: "Either it is a build argument, or it
stays." It stays.
POSTULO_ADMIN_URLis read at run time andcollectstaticruns at buildtime, so a build argument hands somebody an image whose admin page loses its stylesheet the
day they decide to enable it. That is a worse thing to own.
The fonts and the flags are untouched, for the reasons the issue gives: trimming
fonts-noto-corewould be trimming the languages (#70), and the 1476 flag files are 245flags each stored hashed and pre-compressed by the manifest storage doing its job.
Not measured, and saying so
Nothing here builds an image (#81), so this is read rather than weighed: the figures are the
issue's own measurements plus a recount of the
.pofiles, and the tests parse theDockerfile into stages so they can ask which stage a step is in. A real before-and-after
belongs to #156, which needs the same runner.
python:3.14-slim-trixieis left alone deliberately — the issue calls it a separate leverwith a separate benefit, and it belongs with #155's CVE backlog rather than here.
Shipped in
4346dfaon0.3.0, withmainkept level.