1,7 GB verstopte binaries in Codex: weet jij wat er in je .NET-artifact zit?

Simon Willison vond een complete LibreOffice, Python en Node verstopt in de Codex-app. Een praktische kijk op wat er echt in je .NET-deployment zit, en hoe je dat onder controle krijgt.

Jean-Pierre Broeders

Freelance .NET Developer

3 september 20269 min. leestijd
1,7 GB verstopte binaries in Codex: weet jij wat er in je .NET-artifact zit?

Simon Willison ging deze week door zijn cachemap en vond iets grappigs. De Codex-desktopapp, inmiddels omgedoopt tot ChatGPT, bewaart daar stilletjes 1,7 GB aan losse binaries. Een volledige Python-installatie van 440,6 MB. Een volledige Node.js van 446,4 MB. Een headless LibreOffice van 429,7 MB. Poppler voor 187,9 MB en git voor nog eens 148,1 MB. Allemaal netjes weggestopt onder ~/.cache/codex-runtimes, waar geen gebruiker ooit kijkt.

Zijn notitie belandde meteen op de Hacker News-voorpagina. Half de reacties lachte om de omvang. De andere helft snapte precies waarom het er stond: de app heeft skills die documenten moeten kunnen lezen en converteren, en dan heb je nu eenmaal LibreOffice en Poppler nodig. Beide reacties kloppen. En daar zit de les die niks met OpenAI te maken heeft, maar alles met jouw deployments.

Want de vraag is niet of het gek is dat een chat-app een tekstverwerker meesleept. De vraag is: weet jij wat er in jouw artifact zit als je vrijdag naar productie pusht? De meeste .NET-teams die ik binnenkom weten dat niet. Ze weten hun eigen code. De rest is een black box met een groene checkmark ervoor.

Waarom die binaries er staan, en waarom dat soms verstandig is

Even eerlijk over de Codex-aanpak, voordat ik ga preken. Een tool die op elke machine documenten moet kunnen openen heeft twee opties. Optie een: reken erop dat de gebruiker LibreOffice heeft, of Python, of de juiste versie van git. Optie twee: neem het zelf mee, gepind op een versie die je getest hebt.

Optie twee kost een gigabyte. Optie een kost je een supportticket per week, want de helft van de machines heeft niks staan en de andere helft heeft de verkeerde versie. Reproduceerbaarheid is geld waard. Als jouw applicatie een specifieke ffmpeg nodig heeft om te werken, dan is die meenemen vaak de nuchtere keuze, ook al blaast het je image op.

Het echte probleem in het Codex-verhaal is niet de omvang. Het is dat niemand het wist tot iemand toevallig zijn schijf opruimde. Er was geen bewuste afweging zichtbaar, geen regel in de release notes, niks. Dat is precies de val waar .NET-projecten in trappen, alleen dan zonder dat er ooit een Simon Willison langskomt om het te vinden.

Het .NET-equivalent: je publish-output

Begin bij het begin. Wat gebeurt er als je publiceert? Dat hangt af van een keuze die veel mensen nooit expliciet maken.

Een framework-dependent build gaat ervan uit dat de .NET-runtime al op de doelmachine staat. Klein, snel, maar je bent afhankelijk van wat er geinstalleerd is.

dotnet publish -c Release
# output: tientallen kleine dll's, geen runtime meegeleverd

Een self-contained build neemt de hele runtime mee. Nu heb je niks nodig op de doelmachine, maar je artifact wordt fors groter, en elke dotnet-patch die Microsoft uitbrengt moet jij opnieuw uitrollen, want jouw kopie veroudert niet vanzelf.

dotnet publish -c Release -r linux-x64 --self-contained true
# output: 70+ MB, inclusief complete runtime

Dit is jouw versie van het LibreOffice-verhaal. Self-contained is de reproduceerbare keuze, en op een Docker-image waar je toch alles zelf bepaalt is dat vaak overbodig gewicht. In een container heb je de runtime immers al in je base image zitten. Self-contained bovenop een aspnet-image betekent dat je de runtime twee keer meesleept: een keer in de laag van Microsoft, een keer in je eigen output. Ik zie het vaker dan me lief is.

Je Docker-image is het echte artifact

Voor de meeste van mijn klanten is de container het ding dat naar productie gaat, niet de losse dll's. En daar stapelt het gewicht zich op in lagen die je niet ziet als je alleen naar je Dockerfile-regels kijkt.

De klassieke fout is bouwen en draaien in hetzelfde image. Je pakt de SDK, je compileert, en je verscheept het geheel. De .NET-SDK is bijna een gigabyte. Die heb je in productie voor exact niks nodig. Een multi-stage build lost dat op:

# Bouwfase: de dikke SDK, blijft achter in productie
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app

# Runfase: alleen de runtime, niks meer
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "MijnApi.dll"]

De build-fase met zijn SDK van 900 MB verdwijnt volledig. Alleen wat je expliciet kopieert komt in het eindresultaat. Dit is het minimum, en toch draaien er teams rond die stap nog steeds op de SDK in productie.

De volgende winst zit in je base image. De standaard aspnet:9.0 is prima, maar Microsoft levert sinds een tijd chiseled images: een uitgeklede Ubuntu zonder shell, zonder package manager, met alleen wat de runtime nodig heeft.

FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS final

Kleiner, en meteen een stuk minder aanvalsoppervlak omdat er geen bash en geen apt in zitten waar een aanvaller mee kan spelen. De keerzijde die je moet weten: geen shell betekent geen docker exec om even rond te kijken, en debuggen doe je met andere gereedschappen. Dat is een prima ruil voor productie, maar weet dat je hem maakt.

Trimmen en Native AOT: minder meenemen

Wil je echt snijden, dan gaat trimmen een stap verder. De linker gooit alle code weg die je nergens aanroept, ook in je NuGet-dependencies.

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>full</TrimMode>
</PropertyGroup>

Klinkt gratis, is het niet. Trimmen en reflectie zijn oude vijanden. Als een library op runtime types opzoekt via Type.GetType of via serialisatie op naam, dan weet de linker niet dat die code nog nodig is, en gooit hij hem weg. Je build slaagt, je container start, en dan valt hij drie schermen diep in een NullReferenceException om die op het eerste gezicht nergens op slaat. Test een getrimde build daarom net zo grondig als een echte release, niet even snel lokaal.

Native AOT gaat nog verder. Je compileert vooraf naar een native binary, zonder JIT, met een snelle koude start en een kleine footprint. Voor CLI-tools en cold-start-gevoelige functions is dat goud.

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

Maar AOT is streng. Geen runtime code genereren, dus veel klassieke reflectie-gebaseerde frameworks doen het niet zonder source generators. Voor een strak API-project met System.Text.Json-source-generation werkt het prachtig. Voor een oude codebase vol dynamische dingen ga je vechten. Kies AOT bewust voor de plek waar hij past, en forceer hem niet overal.

Meet het, anders raad je maar wat

Het punt van het hele Codex-verhaal is dat niemand mat wat er stond. Doe jij het beter. Twee commando's en je weet meer dan de meeste teams over hun eigen image.

# Welke laag zit hoeveel gewicht in?
docker history mijn-api:latest

# Wat zit er per bestand in, en wat is verspild?
dive mijn-api:latest

dive is de tool die ik het vaakst installeer op een nieuw project. Hij laat per laag zien welke bestanden erbij komen en welke je onnodig kopieert, plus een efficiency-score. De eerste keer dat je hem op een gegroeid image draait is bijna altijd een schok. Ergens zit een COPY . . die je hele .git-map, je node_modules van de frontend en drie oude testrapporten meepakt.

Vergeet je .dockerignore niet. Dat is de goedkoopste winst die er is:

.git
**/bin
**/obj
**/node_modules
*.md
Dockerfile

En kijk daarna naar je NuGet-boom. Elke PackageReference sleept transitieve dependencies mee die jij nooit koos.

dotnet list package --include-transitive

Die lijst is jouw LibreOffice. Meestal zit er wel iets tussen dat je niet meer gebruikt sinds die refactor van vorig jaar, of een package die een halve JSON-stack meetrekt voor een functie die je in twintig regels zelf had geschreven.

Wat ik zou doen

Behandel je artifact als iets dat je kent, niet als iets dat de build-pipeline voor je uitspuugt. Concreet: multi-stage build zodat de SDK nooit in productie komt, een chiseled base image als niks je tegenhoudt, en een .dockerignore die klopt. Draai dive een keer per kwartaal en na elke grote dependency-wijziging. Trimmen en AOT zet je in waar de winst het risico waard is, op CLI's en functions, en niet uit principe overal.

En als je iets zwaars meeneemt omdat het moet, zoals de Codex-mensen met LibreOffice, schrijf dan ergens op waarom. Een regel in je README of je Dockerfile. Dan is het een keuze en geen verrassing die iemand over een jaar bij het opruimen van zijn schijf tegenkomt.

De 1,7 GB in Codex is niet het schandaal waar sommigen op HN het voor aanzagen. Het is een spiegel. De meeste van ons weten net zo min wat er in ons eigen image zit, alleen kijkt er bij ons nooit iemand.

Veelgestelde vragen over slanke .NET-artifacts

Wanneer kies ik self-contained boven framework-dependent? Kies self-contained als je geen controle hebt over wat er op de doelmachine staat, bijvoorbeeld bij een tool die klanten zelf draaien. Draai je in een Docker-container met een .NET base image, dan heb je de runtime al en is framework-dependent lichter, want anders sleep je de runtime twee keer mee.

Breekt trimmen mijn applicatie? Het kan, en vaak pas op runtime. De linker verwijdert code die hij nergens ziet aangeroepen worden, en reflectie of serialisatie op typenaam glipt daar onderuit. Zet trimmen aan, maar test de getrimde build net zo streng als een echte release voordat hij naar productie gaat.

Wat zijn chiseled images en wat is de prijs? Chiseled images zijn uitgeklede Ubuntu-varianten van Microsoft zonder shell en zonder package manager, waardoor ze kleiner zijn en minder aanvalsoppervlak hebben. De prijs is dat je niet meer met docker exec een bash in de container kunt openen om rond te kijken, dus je debugt met tooling van buitenaf.

Hoe zie ik snel wat mijn image zo groot maakt? Draai docker history voor een verdeling per laag en dive voor een bestandsniveau met efficiency-score. Bijna altijd vind je een te brede COPY die een .git-map of node_modules meepakt, plus transitieve NuGet-dependencies die je met dotnet list package --include-transitive boven water haalt.

Verder lezen


Conclusie: De 1,7 GB in Codex is geen blunder van OpenAI maar een spiegel voor je eigen deployments. Weet wat er in je artifact zit, meet het met dive, snijd bewust met multi-stage builds en chiseled images, en schrijf op waarom je iets zwaars meeneemt.

Bronnen: Simon Willison over Codex en LibreOffice · Hacker News-discussie.

Wil je op de hoogte blijven?

Schrijf je in voor mijn nieuwsbrief of neem contact op voor freelance projecten.

Neem Contact Op