BesluitBron 1.4.4-23 Status Sign in

Roll-out and Management

Last changed on .

BesluitBron operates as a public service at besluitbron.nl. Anyone wishing to run the service within their own organisation – for example, to keep traffic within their own network – can do so using the same container image.

The production layout

flowchart TD
    NET["Internet"] --> CAD["Caddy<br/>TLS termination<br/>for BesluitBron.nl"]
    CAD --> APP["BesluitBron<br/>container, one port"]
    APP --> VOL[("Data folder<br/>log, exports,<br/>certificate")]
    APP --> PG[("PostgreSQL 18<br/>required")]
    APP --> UP["Source platforms<br/>outbound HTTPS"]
  • Caddy handles the TLS termination and forwards the traffic to the container. This is done via standard HTTP within the shared network;
  • The data folder contains the request log, the exports and, if using your own TLS, the certificate. This is the folder that should be included in a backup and in a clean-up policy;
  • PostgreSQL is required. The settings and log are stored in PostgreSQL 18. Up to and including version 1.3.50, this was PostgreSQL 17. Up to and including version 1.3.72, the database was optional and the service wrote to files without a database; from version 1.3.73 onwards, an installation without a database will not start.

Update

The image is provided for amd64 and arm64 per version via a registry outside this site. The address will appear on the public ‘Run it yourself’ page as soon as an administrator has entered it; see Installatie. A new version is rolled out by replacing the image and restarting the container. The connector registry is calculated once at start-up, so a restart is also the way to implement a modified source circuit. See Connectorregister.

Two containers may run simultaneously during replacement. From version 1.3.51 onwards, only one of the two works on the data model at a time: the second waits until the first has finished and then continues. The log states this as “Another instance is migrating this database; waiting for it.”, followed by “The other instance finished”. A start-up that takes longer than usual is therefore to be expected and does not constitute a fault. Up to and including version 1.3.50, two containers could interfere with each other, resulting in an error message stating “pg_class_relname_nsp_index”.

The data model checks itself every time it starts up

The database has a version number. That number specifies which tables and columns should be present. From version 1.3.59 onwards, the service does not automatically accept that number. Each time it starts up, the service compares the database with what the data model for that version specifies.

If a table or column is missing, the service creates it. This happens before a newer version of the data model is applied. The container’s log will then contain two entries, with the codes bbnmig016 and bbnmig017. Both specify the number and name of each table and column. These entries are warnings, not errors: the repair has already been carried out.

The service only adds data. A column that already exists remains as it is, even if the data model specifies a different type. A column or table that is not recognised by the data model remains in place. This ensures that no data is lost during a repair.

PostgreSQL cannot add a column that must be filled in and has no default value to a table containing rows. In this case, the update process will stop and display an error message specifying the table and the column. An administrator must then decide what values to assign to the existing rows. This is done outside the service, using SQL.

Up to and including version 1.3.58, there was no validation. A database could be missing a column that, according to the version number, should have existed, and nothing flagged this. On 18 August 2026, the public installation crashed as a result. The update stopped at version 17 of the data model, with a message regarding the column ‘template_key’ in the table ‘bbn_questions’. The dashboard then displayed an error message regarding the column ‘organisation’, which version 19 was supposed to have added.

Updating the database to PostgreSQL 18

From version 1.3.51 onwards, the included composition runs on PostgreSQL 18. An existing installation on PostgreSQL 17 will not be automatically upgraded: PostgreSQL cannot read data from a previous major version. This is a feature of PostgreSQL itself and not of this service.

The container therefore refuses to start and terminates with an error message. This begins with “in 18+, these Docker images are configured to store database data in a format which is compatible with pg_ctlcluster”. That is the safe outcome, and it has been verified: no empty database is created alongside the old one. This is because an empty database would simply start up, after which the service would recreate its tables and the installation would appear to be in good working order, whilst the log file is missing.

There is another change. The volume is now mounted at /var/lib/postgresql rather than /var/lib/postgresql/data. Images from version 18 onwards place the database in a folder for each major version, in this case /var/lib/postgresql/18/docker. This allows the next major version to update the data in its correct location.

Anyone who has been running without a database up to now will not lose their log: the existing daily files are imported once when the database is first started, and are then moved to a subfolder.

Step-by-step guide to updating

The commands below assume the following settings: docker-compose.production.yml, with the database bbn, the owner bbn_install and the volume bbn-pgdata. If the installation is configured with different names, these will be listed in the .env.

First, check which version is currently running. If it says 18, there’s nothing you need to do.

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

Next, make a note of how many rows the largest tables have. That number will later serve as a check to ensure that everything has been transferred correctly.

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

After that, simply stop the application. The database will continue to run for a little while longer, as the dump needs to be generated from it.

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

Create the dump using the pg_dump located in the running container. That is the version associated with the old database, which is exactly what is required. The file will be saved on the host.

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

The roles bbn_install and bbn_web are associated with the server rather than the database, so they are not included in the dump. Nor do they need to be stored separately: bbn_install is created by the image from POSTGRES_USER, and bbn_web creates the service itself on first start-up.

Now stop everything and save the old volume under a different name. The new volume must be empty, otherwise PostgreSQL 18 will not start.

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

In Docker, the volume is called ‘<project>_bbn-pgdata’. The project name is the folder containing the Compose file, in this case ‘mcp2ori’. ‘docker volume ls’ displays the actual name.

That name does not contain a version number, and that is by design. A name that changes along with the image would, in the event of a subsequent major version, point to a volume that does not yet exist. Docker would then simply create that volume, empty. PostgreSQL would populate it with a new database, the service would recreate its tables, and the installation would appear to be in good working order even though the log file is missing. With a fixed name, this cannot happen: the old data remains there, and the container refuses to start. This refusal is the safeguard.

The preserved volume is assigned a version number, in this case bbn-pgdata-17. That volume is no longer used by any image, so the name is the only thing that indicates what it contains.

You can always see which version is in the live volume. The folder is named after the main version.

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

Next, start the database on its own. Docker will recreate the volume – this time empty – and the image will populate it with a database containing the number 18.

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

Create the runtime role before restoring the dump. The dump contains permissions for ‘bbn_web’, and without that role, pg_restore will report an error regarding this. The password is ‘BBN_WEB_PASSWORD’ from .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';"

In that case, go back and read the dump.

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

Reports regarding permissions for bbn_web are not a problem here. The service resets those permissions every time it starts up, on every table that exists at that time.

Finally, start the service.

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

Check after updating

Check these four things, in this order. If one of them is incorrect, the old volume will still be there.

  • The database is running on 18. Repeat the command using select version();.
  • The number of rows corresponds to what was recorded before the dump.
  • The service has started and has checked the data model. The container’s log shows either “Data model is up to date” along with the version number, or “Upgrading data model” if the image is newer than the database. These lines can be found using docker compose -f docker-compose.production.yml logs bbn_web | grep bbnmig.
  • The ‘Data’ screen in the management console displays all tables with their expected figures, and /health lists each connector.

If everything is correct, the reserved volume can be removed.

docker volume rm bbn-pgdata-17

If something turns out to be wrong, here’s how to reverse the process: stop the service, delete the new volume, restore the backed-up volume under its original name, and run the old image again.

What an administrator needs to organise

  • Setting the administrator password. From 1 March 2021 onwards, the image no longer includes a default password and the service will refuse to start unless a password has been set. The production compose file retrieves this from BBN_ADMIN_PASSWORD in the .env and will halt if it is missing. Up to version 1.3.21, the service started with a public default password, about which the console displayed a warning;
  • Determine whether the MCP addresses should be publicly accessible. They do not require authentication, which is a deliberate choice for public data. Within an organisation, however, network restrictions may still be desirable. See Openbaarheid en Persoonsgegevens;
  • To select a retention period for the data folder, see Logging en Verantwoording;
  • Choosing which resources to offer. Fewer resources mean fewer tools competing for the assistant’s attention; see Connector;
  • Find out what sitemap.xml says about the dates, as an administrator will be asked about them. From version 1.3.35 onwards, every page in that file carries a lastmod. For a documentation page, this is the day on which the text itself was changed, as recorded in the version history. For every other page, this is the day on which this version was built, as that text is contained within the image: it cannot have been modified before then. The consequence is clearly stated: a new version resets that date on all pages, including those that have not been changed. Up to and including version 1.3.34, only the documentation pages carried a date; the rest did not. If the build date cannot be read, the date is omitted rather than being invented;
  • Specifying the environment. From 1 February 2024, search engine visibility will be determined by the label under BesluitBron:Environment:Name. An installation named ‘Production’ will be indexed; any other label will be excluded from search engines, in robots.txt, in a header on every response and on the page itself. A test or acceptance environment that omits the label will therefore remain out of the search results rather than appearing in them. BesluitBron:Seo:NoIndex overrides the assessment where necessary;
  • Determine whether this installation specifies a URL for the container image, under BesluitBron:Distribution:ImageReference. If left blank, the ‘Run it yourself’ page will only list the source code, which is correct for an installation that does not serve anything itself. From version 1.3.21;
  • Ensure that the host reserves at least 2 GB of memory for the container. The production composition sets this lower limit for the container bbn_web. During a traffic peak, the service requires this memory; with less, a traffic spike will cause it to crash. From version 1.3.21;
  • Set up a reporting address for vulnerabilities at BesluitBron:SecurityTxt:Contact. An address is pre-filled; a system that reads reports from elsewhere will change it. Without an address, the service will not publish security.txt and a reporter will not be able to submit a report.

Each setting listed above may appear in more than one place, with a fixed order in which one overrides the other; see Instellingen Overschrijven.

The llms.txt file

The service publishes /llms.txt. This file explains to an AI assistant, in plain text, what this installation is and which MCP addresses it provides. It is intended for an assistant that comes across the site without anyone having set up a connector. It is separate from robots.txt, which deals with search engines.

The file follows the format used on llmstxt.org: a title, a short summary, followed by the notes a reader needs, and finally lists of references. From version 1.3.54 onwards, every address in those lists is a genuine Markdown reference, with an explanatory note following it. Up to and including version 1.3.53, every address appeared as plain text on the line. An assistant that searches for references rather than reading text was unable to find any at that time. From the same version onwards, the service also specifies the character set in the header, ensuring that names such as ‘Officiële Bekendmakingen’ are displayed correctly for every reader.

The content is taken from the connector register; see Connectorregister. A source that is switched off is therefore not listed there. For each connector, the file lists two addresses: the MCP address itself and the page explaining how to connect an assistant to it. It also states how many tools that address offers and what the source covers. These figures are based on the addresses that have actually been built, not on a list maintained by anyone.

Speed of public pages

A public page looks the same to every visitor. The language is specified in the URL, not by the visitor. The service therefore caches the rendered page and serves it to anyone who requests the same URL. After sixty seconds, the service re-renders the page.

This applies from version 1.3.7 onwards to the marketing pages, the landing pages, the documentation and the seven explorers. It does not apply to /documentatie/zoeken, as each search query has its own response.

Two rules ensure the saved page remains secure:

  • A submitted request is never returned and is never processed. An administrator always sees a fresh page;
  • A response containing a cookie associated with a single visitor is not stored. Only the language cookie is permitted, as this is derived from the address and is the same for every visitor to that address.

A change to the settings will therefore be visible on every page within sixty seconds at the latest. A change that is only calculated on start-up, such as enabling or disabling a source, still requires a restart.

Health

There are two health centres, each with its own remit.

The address /health reports each connector offered separately. A platform that is unavailable therefore does not render the service unreliable; rather, it is the connector in question that is at fault. This means that a fault at BesluitBron can be distinguished from a fault at a data provider.

The address /health/live acts as the container’s health check. It simply indicates whether the process is running and able to process requests. From version 1.3.25 onwards, it no longer queries a source platform or the database. Consequently, a slow or unreachable source will not cause the container to restart, as a restart would not resolve the issue. The status of the sources is available at /health, which is intended for administrators and monitoring purposes. Under heavy load, /health/live remains accessible, even when the MCP addresses are throttled.

Speed limit on MCP addresses

From 1 March 2025, a rate limit will be in place for MCP addresses. A client that makes requests too quickly will receive an HTTP 429 error with a ‘Retry-After’. An administrator does not need to take any action: the limit is enabled with generous default values that do not affect normal use. For an installation with many concurrent clients, these values can be increased via BesluitBron:McpRateLimit. See Buffering en Doorlooptijd.