// foundation-first-node.lock — what a machine must be before a mesh exists. // // The whole bootstrap (novox/hq 03-DESIGN/01-to-be/07-the-foundation.md): a container runtime, a // store, a database per context, those contexts' schemas, the broker, and the control plane // running on top of them. // // It stopped before the control plane once, and this comment said so for longer than it was true. // // The broker generates its OWN certificate, in its own image, into a volume it then mounts read // only. Self-signed, because at this moment there is no mesh to issue one and no public name to // obtain one for -- and it does not matter, because what a joining node checks is the fingerprint // pinned in its token, not a chain or a name (novox/hq ADR 0004). The subject is decoration. // // PINNED BY DIGEST, and the digest is not decoration: a tag can be made to point at a different // image, and this file is applied on a machine with no mesh to ask about anything. These digests // belong to the registry the lab raises, which is what a real node pulls from anyway — what is // required is a reference that is exact and cannot move (novox/hq ADR 0006). // // The store waits up to three minutes rather than one. A machine that has just pulled the // image and is running initdb for the first time can take longer than sixty seconds, and it // failed that way three times in the lab -- a flaky bootstrap that a second run always fixed, // which is the worst kind because it teaches people to run things twice. // // It failed a fourth time on 2026-08-30, on a loaded machine, and the report was `docker exited // 1:` with nothing after the colon. Raising the timeout again would treat the symptom; what makes // a retry the only available response is a timeout that reports nothing. So the wait now says // what it saw before giving up. // // The store's data is a NAMED VOLUME, not a directory on the machine. A directory the host // creates is owned by root, and the database runs as somebody else inside the container — so it // could not write, and the container crash-looped. A named volume lets the image set up its own // ownership, and outlives the container, which is what you want for the thing holding the mesh's // state. { "declaration": 1, "resources": [ { "id": "container-runtime", "type": "package", "package": "docker" }, { "id": "container-runtime-running", "type": "service", "unit": "docker.service", "state": "running", "boot": "enabled" }, { "id": "store", "type": "container", "name": "mesh-store", "image": "192.0.2.250:5000/postgres@sha256:7abf537131b66ed5af448d90653abf1679b0c7e9a1f07efdd4c3108a401b259a", "env": { "POSTGRES_PASSWORD": "bootstrap", "PGDATA": "/var/lib/postgresql/data/pgdata" }, "ports": ["127.0.0.1:5432:5432"], "volumes": ["mesh-store-data:/var/lib/postgresql/data"] }, // Over TCP, not the socket. While the store initialises it runs a temporary server on the // socket ONLY, then stops it and starts the real one — so a socket check passes, the action // exits happy, and the verify a moment later lands in the gap and fails. The action and its // verify must ask the same question, or the action can succeed into a state verify rejects. { "id": "store-ready", "type": "action", "in": "mesh-store", "command": ["sh", "-c", "for i in $(seq 1 180); do pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1 && exit 0; sleep 1; done; echo 'the store did not answer within 180s; its own last words follow'; pg_isready -h 127.0.0.1 -U postgres; tail -n 20 /var/lib/postgresql/data/log/*.log 2>/dev/null; exit 1"], "verify": ["pg_isready", "-h", "127.0.0.1", "-U", "postgres"] }, { "id": "inventory-database", "type": "action", "in": "mesh-store", "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE inventory'"], "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw inventory"] }, { "id": "identity-database", "type": "action", "in": "mesh-store", "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE identity'"], "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw identity"] }, // Each context owns its own database (novox/hq ADR 0008). A third one is a third database, // created the same way and named the same way — which is the whole of adding a context to the // bootstrap, and is why the count is not something the foundation has an opinion about. { "id": "licences-database", "type": "action", "in": "mesh-store", "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE licences'"], "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw licences"] }, { "id": "context-schemas", "type": "action", "command": ["docker", "run", "--rm", "--network", "container:mesh-store", "-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", "-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable", "-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", "migrate"], "verify": ["sh", "-c", "docker exec mesh-store psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec mesh-store psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key && docker exec mesh-store psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"] }, { "id": "broker-certificate", "type": "action", "command": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", "192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336", "-c", "test -f /tls/tls.crt || (openssl req -x509 -newkey rsa:2048 -nodes -keyout /tls/tls.key -out /tls/tls.crt -days 3650 -subj '/CN=mesh-broker' >/dev/null 2>&1 && chmod 644 /tls/tls.crt && chmod 600 /tls/tls.key)"], "verify": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", "192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336", "-c", "test -s /tls/tls.crt && openssl x509 -in /tls/tls.crt -noout"] }, { "id": "broker", "type": "container", "name": "mesh-broker", "image": "192.0.2.250:5000/cloudamqp/lavinmq@sha256:b117c254e6e269a29db479e6b410ca4e46e035b4981e49d24b159673ef09d336", "ports": ["5671:5671", "127.0.0.1:5672:5672", "127.0.0.1:15672:15672"], "volumes": ["mesh-broker-data:/var/lib/lavinmq", "mesh-broker-tls:/tls:ro"], "args": ["--amqps-port=5671", "--cert=/tls/tls.crt", "--key=/tls/tls.key"] }, { "id": "broker-ready", "type": "action", "in": "mesh-broker", "command": ["sh", "-c", "for i in $(seq 1 60); do lavinmqctl status >/dev/null 2>&1 && exit 0; sleep 1; done; exit 1"], "verify": ["lavinmqctl", "status"] }, { "id": "control-plane", "type": "container", "name": "mesh-controller", "image": "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", "network": "host", "args": ["serve"], "volumes": ["mesh-broker-tls:/broker-tls:ro"], "env": { "MESH_STORE_INVENTORY": "postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", "MESH_STORE_IDENTITY": "postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable", "MESH_STORE_LICENCES": "postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", "MESH_BROKER_AMQP": "amqp://guest:guest@127.0.0.1:5672/", "MESH_BROKER_MANAGEMENT": "http://guest:guest@127.0.0.1:15672", "MESH_BROKER_ADDRESS": "192.0.2.10:5671", "MESH_BROKER_CERTIFICATE": "/broker-tls/tls.crt" } } ] }