BesluitBron 1.4.4-23 Status Anmelden

Einführung und Verwaltung

Zuletzt geändert am .

BesluitBron wird als öffentlicher Dienst auf besluitbron.nl betrieben. Wer den Dienst innerhalb der eigenen Organisation betreiben möchte, beispielsweise um den Datenverkehr im eigenen Netzwerk zu halten, kann dies mit demselben Container-Image tun.

Die Anordnung in der Produktion

flowchart TD
    NET["Internet"] --> CAD["Caddy<br/>TLS-Terminierung<br/>für BesluitBron.nl"]
    CAD --> APP["BesluitBron<br/>Container, ein Tor"]
    APP --> VOL[("Datenordner<br/>Protokoll, Exporte,<br/>Zertifikat")]
    APP --> PG[("PostgreSQL 18<br/>erforderlich")]
    APP --> UP["Quellenplattformen<br/>ausgehendes HTTPS"]
  • Caddy übernimmt die TLS-Terminierung und leitet den Datenverkehr an den Container weiter. Dabei handelt es sich um normales HTTP innerhalb des gemeinsamen Netzwerks;
  • Der Datenordner enthält das Anforderungsprotokoll, die Exporte und – bei eigenem TLS – das Zertifikat. Dieser Ordner gehört in eine Sicherung und in eine Bereinigungsrichtlinie;
  • PostgreSQL ist erforderlich. Die Einstellungen und das Protokoll befinden sich in PostgreSQL 18. Bis einschließlich Version 1.3.50 war dies PostgreSQL 17. Bis einschließlich Version 1.3.72 war die Datenbank optional und der Dienst schrieb ohne Datenbank in Dateien; ab Version 1.3.73 startet eine Installation ohne Datenbank nicht.

Aktualisieren

Das Image wird pro Version für amd64 und arm64 über ein Register außerhalb dieser Website bereitgestellt. Die Adresse wird auf der öffentlichen Seite „Selbst ausführen“ angezeigt, sobald ein Administrator sie eingegeben hat, siehe Installatie. Eine neue Version wird durch Ersetzen des Images und Neustarten des Containers bereitgestellt. Das Connector-Register wird einmalig beim Start berechnet, daher ist ein Neustart auch die Methode, um eine geänderte Quellschaltung zu übernehmen. Siehe Connectorregister.

Während des Austauschs dürfen zwei Container gleichzeitig laufen. Ab Version 1.3.51 arbeitet höchstens einer von beiden gleichzeitig am Datenmodell: Der zweite wartet, bis der erste fertig ist, und fährt dann fort. Im Protokoll wird dies als „Another instance is migrating this database; waiting for it.“ angezeigt, gefolgt von „The other instance finished“. Ein Start, der länger als gewöhnlich dauert, ist daher erklärbar und stellt keine Störung dar. Bis einschließlich Version 1.3.50 konnten sich zwei Container gegenseitig behindern, was zu einer Fehlermeldung über „pg_class_relname_nsp_index“ führte.

Das Datenmodell führt bei jedem Start eine Selbstprüfung durch

Die Datenbank verfügt über eine Versionsnummer. Diese Nummer gibt an, welche Tabellen und Spalten vorhanden sein sollten. Ab Version 1.3.59 akzeptiert der Dienst diese Nummer nicht mehr ohne Weiteres. Bei jedem Start vergleicht der Dienst die Datenbank mit dem, was das Datenmodell für diese Nummer vorsieht.

Fehlt eine Tabelle oder eine Spalte, erstellt der Dienst diese. Dies geschieht, bevor eine neuere Version des Datenmodells angewendet wird. Im Protokoll des Containers befinden sich dann zwei Zeilen mit den Codes „bbnmig016“ und „bbnmig017“. Beide geben die Anzahl und den Namen jeder Tabelle und Spalte an. Diese Zeilen stellen eine Warnung und keinen Fehler dar: Die Korrektur ist bereits erfolgt.

Der Dienst fügt lediglich Daten hinzu. Eine bereits vorhandene Spalte bleibt unverändert, auch wenn das Datenmodell einen anderen Typ angibt. Eine Spalte oder Tabelle, die im Datenmodell nicht vorhanden ist, bleibt bestehen. Auf diese Weise können bei einer Reparatur keine Daten verloren gehen.

Eine Spalte, die zwingend ausgefüllt werden muss und keinen Standardwert hat, kann PostgreSQL nicht zu einer Tabelle mit Zeilen hinzufügen. In diesem Fall wird der Aktualisierungsvorgang mit einer Fehlermeldung abgebrochen, in der die Tabelle und die Spalte genannt werden. Ein Administrator entscheidet dann, welche Werte die bestehenden Zeilen erhalten sollen. Dies geschieht außerhalb des Dienstes mithilfe von SQL.

Bis einschließlich Version 1.3.58 gab es keine Überprüfung. In einer Datenbank konnte eine Spalte fehlen, die laut Versionsnummer vorhanden sein sollte, ohne dass dies gemeldet wurde. Am 18.08.2026 kam es daraufhin zu einem Absturz der öffentlichen Installation. Die Aktualisierung brach bei Version 17 des Datenmodells ab, mit einer Meldung bezüglich der Spalte „template_key“ in der Tabelle „bbn_questions“. Das Dashboard gab anschließend eine Fehlermeldung bezüglich der Spalte „organisation“ aus, die in Version 19 hinzugefügt worden sein sollte.

Die Datenbank auf PostgreSQL 18 aktualisieren

Ab Version 1.3.51 läuft die mitgelieferte Komposition unter PostgreSQL 18. Eine bestehende Installation unter PostgreSQL 17 wird nicht automatisch übernommen: PostgreSQL kann die Daten einer früheren Hauptversion nicht lesen. Dies ist eine Eigenschaft von PostgreSQL selbst und nicht von diesem Dienst.

Der Container weigert sich daher, zu starten, und bricht mit einer Fehlermeldung ab. Diese beginnt mit „in 18+ sind diese Docker-Images so konfiguriert, dass sie Datenbankdaten in einem Format speichern, das mit pg_ctlcluster kompatibel ist“. Das ist das sichere Ergebnis, und es wurde überprüft: Es wird keine leere Datenbank neben der alten angelegt. Eine leere Datenbank würde nämlich einfach starten, woraufhin der Dienst ihre Tabellen neu anlegt und die Installation intakt erscheint, obwohl das Protokoll fehlt.

Es gibt noch eine weitere Änderung. Das Volume befindet sich nun unter /var/lib/postgresql und nicht mehr unter /var/lib/postgresql/data. Bei den Images ab Version 18 wird die Datenbank in einem Ordner pro Hauptversion abgelegt, in diesem Fall unter /var/lib/postgresql/18/docker. Dadurch kann eine nachfolgende Hauptversion die Daten an Ort und Stelle aktualisieren.

Wer bisher ohne Datenbank gearbeitet hat, verliert sein Logbuch nicht: Die vorhandenen Tagesdateien werden beim ersten Start mit einer Datenbank einmalig eingelesen und anschließend in einen Unterordner verschoben.

Schritt für Schritt aktualisieren

Die folgenden Befehle basieren auf dem mitgelieferten Verzeichnis „docker-compose.production.yml“ mit der Datenbank „bbn“, dem Eigentümer „bbn_install“ und dem Volume „bbn-pgdata“. Läuft die Installation unter anderen Namen, sind diese in der Datei „.env“ aufgeführt.

Schauen Sie zunächst nach, welche Version derzeit läuft. Wenn dort „18“ steht, müssen Sie nichts unternehmen.

docker compose -f docker-compose.production.yml exec postgres psql -U bbn_install -d bbn -tAc "select version();"

Notieren Sie anschließend, wie viele Zeilen die größten Tabellen haben. Diese Zahl dient später als Kontrollwert, um sicherzustellen, dass alles korrekt übernommen wurde.

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;"

Stellen Sie anschließend nur die Anwendung ein. Die Datenbank läuft noch eine Weile weiter, da der Dump von dort stammen muss.

docker compose -f docker-compose.production.yml stop bbn_web

Erstellen Sie den Dump mit dem Befehl „pg_dump“, der sich im laufenden Container befindet. Das ist die Version, die zur alten Datenbank gehört, und genau das ist auch beabsichtigt. Die Datei wird auf dem Host gespeichert.

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

Die Rollen „bbn_install“ und „bbn_web“ gehören zum Server und nicht zur Datenbank, daher sind sie nicht im Dump enthalten. Sie müssen auch nicht separat gespeichert werden: „bbn_install“ wird vom Image aus „POSTGRES_USER“ erstellt, und „bbn_web“ erstellt der Dienst selbst beim ersten Start.

Halten Sie nun alle Prozesse an und speichern Sie das alte Volume unter einem anderen Namen. Das neue Volume muss leer sein, sonst startet PostgreSQL 18 nicht.

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

Das Volume heißt in Docker „<project>_bbn-pgdata“. Der Projektname ist der Ordner, in dem sich die Compose-Datei befindet, hier „mcp2ori“. Mit dem Befehl „docker volume ls“ wird der tatsächliche Name angezeigt.

Dieser Name enthält keine Versionsnummer, und das ist bewusst so gewählt. Ein Name, der sich mit dem Image mitändert, würde bei einer nächsten Hauptversion auf ein Volume verweisen, das noch nicht existiert. Docker würde dieses Volume dann einfach erstellen – leer. PostgreSQL legt dort eine neue Datenbank an, der Dienst erstellt ihre Tabellen neu, und die Installation sieht einwandfrei aus, obwohl das Protokoll fehlt. Bei einem festen Namen kann das nicht passieren: Die alten Daten sind noch vorhanden, und der Container weigert sich zu starten. Diese Weigerung ist der Schutzmechanismus.

Das gespeicherte Volume erhält zwar eine Versionsnummer, in diesem Fall „bbn-pgdata-17“. Dieses Volume wird von keinem Image mehr verwendet, daher gibt nur noch der Name Aufschluss darüber, was darin enthalten ist.

Man kann jederzeit sehen, welche Version sich im Live-Volume befindet. Der Ordner ist nach der Hauptversion benannt.

docker run --rm -v mcp2ori_bbn-pgdata:/v alpine ls /v

Starten Sie anschließend nur die Datenbank. Docker erstellt das Volume erneut – diesmal leer – und das Image fügt eine Datenbank mit dem Wert 18 hinzu.

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();"

Erstellen Sie die Laufzeitrolle, bevor der Dump eingelesen wird. Der Dump enthält Berechtigungen für „bbn_web“, und ohne diese Rolle meldet „pg_restore“ diesbezüglich einen Fehler. Das Passwort lautet „BBN_WEB_PASSWORD“ aus der Datei „.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';"

Dann lies den Dump noch einmal durch.

docker compose -f docker-compose.production.yml exec -T postgres \
  pg_restore -U bbn_install -d bbn < bbn-17.dump

Meldungen zu Berechtigungen für „bbn_web“ sind hier kein Problem. Der Dienst legt diese Berechtigungen bei jedem Start neu fest, und zwar für jede Tabelle, die zu diesem Zeitpunkt vorhanden ist.

Starten Sie schließlich den Dienst.

docker compose -f docker-compose.production.yml up -d

Nach der Aktualisierung überprüfen

Überprüfen Sie diese vier Punkte in dieser Reihenfolge. Sollte einer davon nicht stimmen, ist der alte Datenträger noch vorhanden.

  • Die Datenbank läuft auf 18. Wiederholen Sie den Befehl mit „select version();“.
  • Die Anzahl der Zeilen stimmt mit den Angaben vor dem Dump überein.
  • Der Dienst wurde gestartet und hat das Datenmodell überprüft. Im Protokoll des Containers steht entweder „Data model is up to date“ zusammen mit der Versionsnummer oder „Upgrading data model“, wenn das Image neuer ist als die Datenbank. Mit dem Befehl „docker compose -f docker-compose.production.yml logs bbn_web | grep bbnmig“ lassen sich diese Einträge finden.
  • Der Bildschirm „Daten“ in der Verwaltungskonsole zeigt alle Tabellen mit den erwarteten Zahlen an, und unter „/health“ werden alle Konnektoren aufgeführt.

Wenn alles stimmt, kann das gespeicherte Volumen gelöscht werden.

docker volume rm bbn-pgdata-17

Sollte etwas nicht stimmen, gibt es einen Weg zurück: Den Dienst beenden, das neue Volume löschen, das gesicherte Volume unter dem ursprünglichen Namen wiederherstellen und das alte Image erneut ausführen.

Was ein Verwalter zu erledigen hat

  • Das Administratorpasswort festlegen. Ab dem 1.3.21 enthält das Image kein Standardpasswort mehr und der Dienst weigert sich, ohne festgelegtes Passwort zu starten. Die Produktions-Compose-Datei bezieht das Passwort aus der Datei „BBN_ADMIN_PASSWORD“ im Verzeichnis „.env“ und bricht ab, wenn diese fehlt. Bis zur Version 1.3.21 startete der Dienst mit einem öffentlichen Standardpasswort, woraufhin die Konsole eine Warnung anzeigte;
  • Festlegen, ob die MCP-Adressen öffentlich erreichbar sein dürfen. Sie sehen keine Authentifizierung vor, was bei öffentlichen Daten eine bewusste Entscheidung ist. Innerhalb einer Organisation kann dennoch eine Netzwerkeinschränkung wünschenswert sein. Siehe Openbaarheid en Persoonsgegevens;
  • Um eine Aufbewahrungsfrist für den Datenordner festzulegen, siehe Logging en Verantwoording;
  • Auswahl der angebotenen Quellen. Weniger Quellen bedeuten weniger Hilfsmittel, die um die Aufmerksamkeit des Assistenten konkurrieren, siehe Connector;
  • Man sollte wissen, was sitemap.xml über die Datumsangaben aussagt, da ein Administrator dazu Fragen erhält. Jede Seite in dieser Datei trägt ab Version 1.3.35 ein „lastmod“. Bei einer Dokumentationsseite ist dies der Tag, an dem sich der Text selbst geändert hat, wie aus der Versionshistorie hervorgeht. Bei jeder anderen Seite ist dies der Tag, an dem diese Version erstellt wurde, da sich dieser Text im Image befindet: Früher kann er nicht geändert worden sein. Die Folge wird dort ehrlich angegeben: Eine neue Version setzt dieses Datum auf allen Seiten neu, auch auf den Seiten, die nicht geändert wurden. Bis einschließlich Version 1.3.34 trugen nur die Dokumentationsseiten ein Datum, die übrigen Seiten hingegen keines. Kann das Erstellungsdatum nicht ausgelesen werden, bleibt das Datum weg, anstatt dass ein beliebiges Datum erfunden wird;
  • Die Umgebung benennen. Ab dem 1.2.24 richtet sich die Sichtbarkeit für Suchmaschinen nach der Bezeichnung unter BesluitBron:Environment:Name. Eine Installation mit der Bezeichnung „Production“ wird indexiert; jede andere Bezeichnung wird für Suchmaschinen, unter robots.txt, in einer Kopfzeile jeder Antwort und auf der Seite selbst abgelehnt. Eine Test- oder Abnahmeumgebung, bei der die Kennzeichnung fehlt, bleibt somit aus den Suchergebnissen heraus, anstatt darin zu erscheinen. BesluitBron:Seo:NoIndex überschreibt die Bewertung, wo dies erforderlich ist;
  • Festlegen, ob bei dieser Installation unter BesluitBron:Distribution:ImageReference eine Adresse für das Container-Image angegeben wird. Ist das Feld leer, bedeutet dies, dass auf der Seite „Selbst ausführen“ nur der Quellcode angegeben wird, was bei einer Installation zutrifft, die selbst nichts ausgibt. Ab Version 1.3.21;
  • Stellen Sie sicher, dass der Host mindestens 2 GB Arbeitsspeicher für den Container freihält. Die Produktionskonfiguration legt diese Untergrenze für den Container „bbn_web“ fest. Bei einem Traffic-Spitzenwert benötigt der Dienst diesen Arbeitsspeicher; bei weniger Speicher kommt es bei einer Spitzenlast zu einem Absturz. Ab Version 1.3.21;
  • Legen Sie unter BesluitBron:SecurityTxt:Contact eine Meldeadresse für Sicherheitslücken fest. Dort ist bereits eine Adresse voreingestellt; eine Installation, die Meldungen an anderer Stelle ausliest, ändert diese. Ohne diese Adresse veröffentlicht der Dienst keine security.txt und ein Melder findet keinen Zugang.

Jede der oben genannten Einstellungen kann an mehr als einer Stelle vorkommen, wobei eine bestimmte Reihenfolge gilt, in der die eine Einstellung die andere überschreibt; siehe Instellingen Overschrijven.

Die Datei „llms.txt“

Der Dienst veröffentlicht die Datei „/llms.txt“. Diese Datei erklärt einem KI-Assistenten in einfacher Sprache, um welche Installation es sich handelt und welche MCP-Adressen sie bereitstellt. Sie ist für einen Assistenten gedacht, der auf die Website stößt, ohne dass jemand einen Connector eingerichtet hat. Sie steht in keinem Zusammenhang mit der Datei „robots.txt“, die sich mit Suchmaschinen befasst.

Die Datei folgt dem Format von llmstxt.org: ein Titel, eine kurze Zusammenfassung, anschließend die für den Leser notwendigen Anmerkungen und schließlich Listen mit Verweisen. Ab Version 1.3.54 ist jede Adresse in diesen Listen ein echter Verweis in Markdown, gefolgt von einer Erläuterung. Bis einschließlich 1.3.53 stand jede Adresse als reiner Text in der Zeile. Ein Assistent, der nach Verweisen sucht, anstatt den Text zu lesen, fand damals keinen einzigen. Ab derselben Version gibt der Dienst auch den Zeichensatz in der Kopfzeile an, sodass Namen wie „Officiële Bekendmakingen“ bei jedem Leser korrekt angezeigt werden.

Der Inhalt stammt aus dem Anschlussregister, siehe Connectorregister. Eine Quelle, die ausgeschaltet ist, ist dort also nicht aufgeführt. Die Datei nennt für jeden Anschluss zwei Adressen: die MCP-Adresse selbst und die Seite, auf der erklärt wird, wie ein Assistent daran angeschlossen wird. Dabei wird angegeben, wie viele Werkzeuge diese Adresse bereitstellt und was die Quelle abdeckt. Diese Zahlen stammen aus den tatsächlich realisierten Anschlüssen und nicht aus einer Liste, die jemand führt.

Geschwindigkeit der öffentlichen Seiten

Eine öffentliche Seite sieht für jeden Besucher gleich aus. Die Sprache ist in der Adresse festgelegt und nicht vom Besucher abhängig. Der Dienst speichert daher die bereits erstellte Seite und stellt sie jedem zur Verfügung, der dieselbe Adresse aufruft. Nach 60 Sekunden erstellt der Dienst die Seite neu.

Dies gilt ab Version 1.3.7 für die Marketingseiten, die Landingpages, die Dokumentation und die sieben Suchassistenten. Es gilt nicht für /documentatie/zoeken, da jede Suchanfrage eine eigene Antwort hat.

Zwei Regeln sorgen für die Sicherheit der gespeicherten Seite:

  • Eine angemeldete Anfrage wird nie ausgegeben und füllt ihn auch nie aus. Ein Administrator sieht immer eine neue Seite;
  • Eine Antwort mit einem Cookie, das zu einem bestimmten Besucher gehört, wird nicht gespeichert. Lediglich das Sprach-Cookie darf mitgesendet werden, da es sich aus der Adresse ergibt und für jeden Besucher dieser Adresse gleich ist.

Eine Änderung der Einstellungen ist somit spätestens nach 60 Sekunden auf jeder Seite sichtbar. Eine Änderung, die erst beim Start berechnet wird, wie beispielsweise das Ein- oder Ausschalten einer Quelle, erfordert weiterhin einen Neustart.

Gesundheit

Es gibt zwei Gesundheitsstellen, die jeweils eine eigene Aufgabe haben.

Die Adresse /health meldet jeden angebotenen Konnektor einzeln. Eine nicht erreichbare Plattform beeinträchtigt somit nicht den Dienst, sondern den betreffenden Konnektor. So lässt sich eine Störung bei BesluitBron von einer Störung bei einem Quellenbetreiber unterscheiden.

Die Adresse /health/live dient als Lebenszeichen-Prüfung des Containers. Sie gibt lediglich Auskunft darüber, ob der Prozess aktiv ist und Anfragen verarbeiten kann. Ab Version 1.3.25 fragt es nicht mehr bei einer Quellplattform oder der Datenbank nach. Eine langsame oder nicht erreichbare Quelle führt daher nicht zu einem Neustart des Containers, da ein Neustart das Problem nicht behebt. Die Bewertung der Quellen findet sich unter /health, das für Administratoren und zur Überwachung gedacht ist. Auch unter hoher Auslastung bleibt /health/live erreichbar, selbst wenn die MCP-Adressen gedrosselt werden.

Geschwindigkeitsbegrenzung bei den MCP-Adressen

Ab Version 1.3.25 gilt eine Geschwindigkeitsbegrenzung für die MCP-Adressen. Ein Client, der zu schnell Anfragen stellt, erhält den HTTP-Status 429 mit der Meldung „Retry-After“. Ein Administrator muss hierfür keine Maßnahmen ergreifen: Die Begrenzung ist standardmäßig mit großzügigen Standardwerten aktiviert, die den normalen Betrieb nicht beeinträchtigen. Bei einer Installation mit vielen gleichzeitigen Clients können diese Werte unter BesluitBron:McpRateLimit erhöht werden. Siehe Buffering en Doorlooptijd.