BesluitBron draait als openbare dienst op besluitbron.nl. Wie de dienst binnen de eigen organisatie wil draaien, bijvoorbeeld om het verkeer binnen het eigen netwerk te houden, kan dat met hetzelfde container-image.
De opstelling in productie
flowchart TD
NET["Internet"] --> CAD["Caddy<br/>TLS-terminatie<br/>voor besluitbron.nl"]
CAD --> APP["BesluitBron<br/>container, één poort"]
APP --> VOL[("Gegevensmap<br/>logboek, exports,<br/>certificaat")]
APP --> PG[("PostgreSQL 18<br/>vereist")]
APP --> UP["Bronplatformen<br/>uitgaand HTTPS"]
- Caddy verzorgt de TLS-terminatie en stuurt het verkeer door naar de container. Dat gaat over gewoon HTTP binnen het gedeelde netwerk;
- De gegevensmap bevat het verzoeklogboek, de exports en, bij eigen TLS, het certificaat. Dit is de map die in een back-up en in een opschoonbeleid thuishoort;
- PostgreSQL is vereist. De instellingen en het logboek staan in PostgreSQL 18. Tot en met versie 1.3.50 was dat PostgreSQL 17. Tot en met versie 1.3.72 was de database optioneel en schreef de dienst zonder database naar bestanden; vanaf versie 1.3.73 start een installatie zonder database niet.
Bijwerken
Het image wordt per versie uitgeleverd voor amd64 en arm64, via een registry buiten deze site. Het adres staat op de publieke pagina "Zelf draaien" zodra een beheerder het heeft ingevuld, zie Installatie. Een nieuwe versie wordt uitgerold door het image te vervangen en de container te herstarten. Het connectorregister wordt eenmalig bij het starten berekend, dus een herstart is ook de manier om een gewijzigde bronschakeling door te voeren. Zie Connectorregister.
Twee containers mogen tegelijk draaien tijdens het vervangen. Vanaf versie 1.3.51 werkt er hoogstens een van beide tegelijk aan het datamodel: de tweede wacht tot de eerste klaar is en gaat dan verder. In het logboek staat dat als "Another instance is migrating this database; waiting for it.", gevolgd door "The other instance finished". Een start die langer duurt dan gewoonlijk is dus verklaarbaar en geen storing. Tot en met 1.3.50 konden twee containers elkaar in de weg zitten, met een foutmelding over pg_class_relname_nsp_index als gevolg.
Het datamodel controleert zichzelf bij elke start
De database heeft een versienummer. Dat nummer zegt welke tabellen en kolommen er horen te zijn. Vanaf versie 1.3.59 gelooft de dienst dat nummer niet zonder meer. Bij elke start vergelijkt de dienst de database met wat het datamodel bij dat nummer belooft.
Ontbreekt er een tabel of een kolom, dan maakt de dienst die aan. Dat gebeurt voordat een nieuwere versie van het datamodel wordt toegepast. In het logboek van de container staan dan twee regels, met de codes bbnmig016 en bbnmig017. Beide noemen het aantal en de naam van elke tabel en kolom. Die regels zijn een waarschuwing en geen storing: de reparatie is al gebeurd.
De dienst voegt alleen toe. Een kolom die er al is blijft zoals hij is, ook als het datamodel een ander type noemt. Een kolom of tabel die het datamodel niet kent blijft staan. Zo kan een reparatie geen gegevens kwijtmaken.
Een kolom die verplicht gevuld moet zijn en geen standaardwaarde heeft, kan PostgreSQL niet toevoegen aan een tabel met rijen. Dan stopt het bijwerken met een foutmelding die de tabel en de kolom noemt. Een beheerder beslist dan wat de bestaande rijen krijgen. Dat gebeurt buiten de dienst om, met SQL.
Tot en met versie 1.3.58 was er geen controle. Een database kon een kolom missen die volgens het versienummer bestond, en niets meldde dat. Op 2026-08-18 liep de openbare installatie daarop vast. Het bijwerken stopte bij versie 17 van het datamodel, met een melding over de kolom template_key in de tabel bbn_questions. Het dashboard gaf daarna een foutmelding over de kolom organisation, die versie 19 zou hebben toegevoegd.
De database bijwerken naar PostgreSQL 18
Vanaf versie 1.3.51 draait de meegeleverde compositie op PostgreSQL 18. Een bestaande installatie op PostgreSQL 17 gaat niet vanzelf mee: PostgreSQL kan de gegevens van een vorige hoofdversie niet lezen. Dat is een eigenschap van PostgreSQL zelf en niet van deze dienst.
De container weigert daarom te starten en stopt met een foutmelding. Die begint met "in 18+, these Docker images are configured to store database data in a format which is compatible with pg_ctlcluster". Dat is de veilige uitkomst, en die is nagemeten: er wordt geen lege database naast de oude gezet. Een lege database zou namelijk gewoon starten, waarna de dienst haar tabellen opnieuw aanmaakt en de installatie er gezond uitziet terwijl het logboek weg is.
Er verandert nog iets. Het volume hangt nu aan /var/lib/postgresql en niet meer aan /var/lib/postgresql/data. De images van 18 en later zetten de database in een map per hoofdversie, in dit geval /var/lib/postgresql/18/docker. Daardoor kan een volgende hoofdversie de gegevens op hun plaats bijwerken.
Wie tot nu toe zonder database draaide, raakt zijn logboek niet kwijt: de bestaande dagbestanden worden bij de eerste start met een database eenmalig ingelezen en daarna naar een submap verplaatst.
Bijwerken stap voor stap
De opdrachten hieronder gaan uit van de meegeleverde docker-compose.production.yml, met de database bbn, de eigenaar bbn_install en het volume bbn-pgdata. Draait de installatie onder andere namen, dan staan die in de .env.
Kijk eerst welke versie er nu draait. Staat er 18, dan is er niets te doen.
docker compose -f docker-compose.production.yml exec postgres psql -U bbn_install -d bbn -tAc "select version();"
Noteer daarna hoeveel rijen de grootste tabellen hebben. Dat getal is later de controle dat alles is overgekomen.
docker compose -f docker-compose.production.yml exec postgres psql -U bbn_install -d bbn \
-c "select count(*) from bbn_mcp_log;" -c "select count(*) from bbn_errors;"
Zet daarna alleen de applicatie stil. De database blijft nog even draaien, want daar moet de dump uit komen.
docker compose -f docker-compose.production.yml stop bbn_web
Maak de dump met de pg_dump die in de draaiende container zit. Dat is de versie die bij de oude database hoort, en dat is precies de bedoeling. Het bestand komt op de host terecht.
docker compose -f docker-compose.production.yml exec -T postgres \
pg_dump -U bbn_install -d bbn -Fc > bbn-17.dump
ls -l bbn-17.dump
De rollen bbn_install en bbn_web horen bij de server en niet bij de database, dus die zitten niet in de dump. Ze hoeven ook niet apart te worden bewaard: bbn_install wordt door het image aangemaakt uit POSTGRES_USER, en bbn_web maakt de dienst zelf aan bij de eerste start.
Zet nu alles stil en bewaar het oude volume onder een andere naam. Het nieuwe volume moet leeg zijn, anders begint PostgreSQL 18 niet.
docker compose -f docker-compose.production.yml down
docker volume create bbn-pgdata-17
docker run --rm -v mcp2ori_bbn-pgdata:/from -v bbn-pgdata-17:/to alpine sh -c "cp -a /from/. /to/"
docker volume rm mcp2ori_bbn-pgdata
Het volume heet in Docker <project>_bbn-pgdata. De projectnaam is de map waarin het composebestand staat, hier mcp2ori. docker volume ls toont de echte naam.
In die naam staat geen versienummer, en dat is een keuze. Een naam die met het image meebeweegt zou bij een volgende hoofdversie naar een volume wijzen dat nog niet bestaat. Docker maakt dat volume dan gewoon aan, leeg. PostgreSQL zet er een nieuwe database in, de dienst maakt haar tabellen opnieuw aan en de installatie ziet er gezond uit terwijl het logboek weg is. Met een vaste naam kan dat niet gebeuren: de oude gegevens staan er, en de container weigert te starten. De weigering is de bescherming.
Het bewaarde volume krijgt het versienummer wel, hier bbn-pgdata-17. Dat volume wordt door geen enkel image nog gebruikt, dus de naam is het enige wat nog zegt wat erin zit.
Welke versie in het levende volume staat, is altijd te zien. De map is naar de hoofdversie genoemd.
docker run --rm -v mcp2ori_bbn-pgdata:/v alpine ls /v
Start daarna alleen de database. Docker maakt het volume opnieuw aan, nu leeg, en het image zet er een database van 18 in.
docker compose -f docker-compose.production.yml up -d postgres
docker compose -f docker-compose.production.yml exec postgres psql -U bbn_install -d bbn -tAc "select version();"
Maak de runtime-rol aan voordat de dump wordt teruggelezen. In de dump staan rechten voor bbn_web, en zonder die rol meldt pg_restore daar een fout over. Het wachtwoord is BBN_WEB_PASSWORD uit de .env.
docker compose -f docker-compose.production.yml exec postgres \
psql -U bbn_install -d bbn -c "create role bbn_web login password 'HET_WACHTWOORD_UIT_ENV';"
Lees dan de dump terug.
docker compose -f docker-compose.production.yml exec -T postgres \
pg_restore -U bbn_install -d bbn < bbn-17.dump
Meldingen over rechten voor bbn_web zijn hier niet erg. De dienst zet die rechten bij elke start opnieuw, op elke tabel die er dan is.
Start ten slotte de dienst.
docker compose -f docker-compose.production.yml up -d
Na het bijwerken controleren
Controleer deze vier dingen, in deze volgorde. Klopt er een niet, dan staat het oude volume er nog.
- De database draait op 18. Herhaal de opdracht met
select version();. - De aantallen rijen komen overeen met wat er voor de dump is genoteerd.
- De dienst is gestart en heeft het datamodel gecontroleerd. In het logboek van de container staat "Data model is up to date" met het versienummer erbij, of "Upgrading data model" wanneer het image nieuwer is dan de database. Met
docker compose -f docker-compose.production.yml logs bbn_web | grep bbnmigzijn die regels te vinden. - Het scherm Data in de beheerconsole toont alle tabellen met de verwachte aantallen, en
/healthmeldt elke connector.
Klopt alles, dan mag het bewaarde volume weg.
docker volume rm bbn-pgdata-17
Blijkt er iets niet te kloppen, dan is de weg terug: de dienst stoppen, het nieuwe volume weggooien, het bewaarde volume terugzetten onder de oorspronkelijke naam en het oude image weer draaien.
Wat een beheerder moet regelen
- Het beheerderswachtwoord instellen. Vanaf 1.3.21 levert het image geen standaardwachtwoord mee en weigert de dienst te starten zonder ingesteld wachtwoord. Het productie-composebestand betrekt het uit
BBN_ADMIN_PASSWORDin de.enven stopt wanneer die ontbreekt. Tot 1.3.21 startte de dienst met een openbaar standaardwachtwoord waarover de console een waarschuwing toonde; - Bepalen of de MCP-adressen publiek bereikbaar mogen zijn. Zij kennen geen authenticatie, wat voor openbare gegevens een bewuste keuze is. Binnen een organisatie kan een netwerkbeperking alsnog gewenst zijn. Zie Openbaarheid en Persoonsgegevens;
- Een bewaartermijn kiezen voor de gegevensmap, zie Logging en Verantwoording;
- Kiezen welke bronnen worden aangeboden. Minder bronnen betekent minder gereedschappen die om de aandacht van de assistent concurreren, zie Connector;
- Weten wat
sitemap.xmlover de datums zegt, want een beheerder krijgt daar vragen over. Elke pagina in dat bestand draagt vanaf versie 1.3.35 eenlastmod. Bij een documentatiepagina is dat de dag waarop die tekst zelf veranderde, uit de versiegeschiedenis. Bij elke andere pagina is dat de dag waarop deze versie is gebouwd, want die tekst zit in het image: eerder kan hij niet zijn gewijzigd. Het gevolg staat er eerlijk bij: een nieuwe versie zet die datum op álle pagina's opnieuw, ook op de pagina's die niet zijn veranderd. Tot en met versie 1.3.34 droegen alleen de documentatiepagina's een datum en de rest geen. Kan de bouwdatum niet worden gelezen, dan blijft de datum weg in plaats van te worden verzonnen; - De omgeving benoemen. Vanaf 1.2.24 volgt de zichtbaarheid voor zoekmachines het label onder
BesluitBron:Environment:Name. Een installatie die "Production" heet wordt geïndexeerd; elk ander label wordt geweigerd voor zoekmachines, inrobots.txt, in een kopregel op elk antwoord en in de pagina zelf. Een test- of acceptatieomgeving die het label vergeet, blijft daarmee uit de zoekresultaten in plaats van erin te belanden.BesluitBron:Seo:NoIndexoverschrijft het oordeel waar dat nodig is; - Bepalen of deze installatie een adres voor het container-image noemt, onder
BesluitBron:Distribution:ImageReference. Leeg betekent dat de pagina "Zelf draaien" alleen de broncode noemt, wat klopt voor een installatie die zelf niets uitlevert. Vanaf versie 1.3.21; - Zorgen dat de host minimaal 2 GB geheugen voor de container vrij houdt. De productie-compositie legt die ondergrens vast op de container
bbn_web. Onder een piek in het verkeer heeft de dienst dat geheugen nodig; met minder loopt een uitschieter vast. Vanaf versie 1.3.21; - Een meldadres voor kwetsbaarheden vaststellen onder
BesluitBron:SecurityTxt:Contact. Er staat een adres voorgevuld; een installatie die meldingen elders leest, wijzigt het. Zonder adres publiceert de dienst geensecurity.txten vindt een melder geen ingang.
Elke instelling hierboven kan op meer dan één plek staan, met een vaste volgorde waarin de ene de andere overschrijft; zie Instellingen Overschrijven.
Het bestand llms.txt
De dienst publiceert /llms.txt. Dat bestand vertelt een AI-assistent in gewone tekst wat deze installatie is en welke MCP-adressen zij aanbiedt. Het is bedoeld voor een assistent die de site tegenkomt zonder dat iemand een connector heeft ingesteld. Het staat los van robots.txt, dat over zoekmachines gaat.
Het bestand volgt de vorm van llmstxt.org: een titel, een korte samenvatting, daarna de opmerkingen die een lezer nodig heeft, en tot slot lijsten met verwijzingen. Vanaf versie 1.3.54 is elk adres in die lijsten een echte verwijzing in Markdown, met een toelichting erachter. Tot en met 1.3.53 stond elk adres als kale tekst in de regel. Een assistent die verwijzingen zoekt in plaats van tekst te lezen, vond er toen geen enkele. Vanaf dezelfde versie noemt de dienst ook de tekenset in de kopregel, zodat namen als "Officiële Bekendmakingen" bij elke lezer goed aankomen.
De inhoud komt uit het connectorregister, zie Connectorregister. Een bron die uit staat, staat er dus niet in. Het bestand noemt per connector twee adressen: het MCP-adres zelf en de pagina die uitlegt hoe een assistent erop wordt aangesloten. Daarbij staat hoeveel gereedschappen dat adres aanbiedt en wat de bron dekt. Die aantallen komen uit de adressen die echt zijn gebouwd en niet uit een lijst die iemand bijhoudt.
Snelheid van de publieke pagina's
Een publieke pagina ziet er voor elke bezoeker hetzelfde uit. De taal staat in het adres en niet in de bezoeker. De dienst bewaart daarom de opgemaakte pagina en geeft die aan iedereen die hetzelfde adres opvraagt. Na zestig seconden maakt de dienst de pagina opnieuw op.
Dat geldt vanaf versie 1.3.7 voor de marketingpagina's, de aansluitpagina's, de documentatie en de zeven verkenners. Het geldt niet voor /documentatie/zoeken, omdat elke zoekvraag een eigen antwoord heeft.
Twee regels houden de bewaarde pagina veilig:
- Een aangemeld verzoek komt er nooit uit en vult hem ook nooit. Een beheerder ziet altijd een verse pagina;
- Een antwoord met een cookie die bij één bezoeker hoort, wordt niet bewaard. Alleen de taalcookie mag mee, want die volgt uit het adres en is voor elke bezoeker van dat adres gelijk.
Een wijziging in de instellingen is dus na hoogstens zestig seconden op elke pagina zichtbaar. Een wijziging die pas bij het starten wordt berekend, zoals het aan- of uitzetten van een bron, vraagt nog steeds een herstart.
Gezondheid
Er zijn twee gezondheidsadressen, met elk een eigen taak.
Het adres /health meldt elke aangeboden connector afzonderlijk. Een platform dat niet bereikbaar is, maakt daarmee niet de dienst ongezond maar de betreffende connector. Zo is een storing bij BesluitBron te onderscheiden van een storing bij een bronhouder.
Het adres /health/live is de levenscontrole van de container. Het antwoordt alleen of het proces leeft en verzoeken kan verwerken. Vanaf 1.3.25 doet het geen navraag meer bij een bronplatform of de database. Een trage of onbereikbare bron herstart de container daardoor niet, want een herstart lost dat niet op. Het oordeel over de bronnen staat op /health, dat voor een beheerder en een monitor is bedoeld. Onder zware belasting blijft /health/live bereikbaar, ook wanneer de MCP-adressen worden afgeknepen.
Snelheidsgrens op de MCP-adressen
Vanaf 1.3.25 staat er een snelheidsgrens op de MCP-adressen. Een client die te snel aanroept, krijgt HTTP 429 met een Retry-After. Een beheerder hoeft hier niets voor te regelen: de grens staat aan met ruime standaardwaarden die normaal gebruik niet raken. Een installatie met veel gelijktijdige clients verhoogt ze onder BesluitBron:McpRateLimit. Zie Buffering en Doorlooptijd.