- 01 · Overview
Quickstart
The whole system on a laptop — services, stores, gateway and the observability stack — from one command that does not succeed until a request has crossed all of it.
Requirements #
- Docker, or rootless podman, with a compose implementation —
docker composeorpodman-compose. Both are first-class;make doctorreports which runtime, compose and socket it found. - About 2 GB of free memory. Six application containers measure about 1.14 GB; the observability stack adds about 590 MB.
- Free ports 8080, 8081, 9090, 9091, 3000, 9093 and 9095. Everything binds to 127.0.0.1.
- A few minutes for the first build. Both services compile inside the image build; later builds reuse the layer cache.
On an SELinux-enforcing host with rootless podman, bind-mounted secrets need relabelling and the gateway needs to read the podman socket. An overlay that does both is applied automatically; nothing in the base compose file is loosened to make it work.
Bring it up #
make bootstrap # build, start everything, and prove it works
make smoke # re-run the end-to-end assertions against the running stack
make down # stop and delete volumes
make bootstrap generates the local secret files, builds both hardened images, starts every container
in health order and runs the smoke test. Dependencies are expressed as health conditions, never sleeps: Postgres
and Redis must answer before the API starts, and Kafka's health check asks the broker to serve a real request,
because a port check passes while the broker is still recovering.
What bootstrap proves #
Bootstrap does not return successfully until a request has travelled the whole system:
- through the gateway, to a ready API replica;
- into Postgres, with the item and its outbox event in one transaction;
- out through the relay to Kafka;
- through the worker, applied once, into the Redis read model;
- and back out of the API's inventory summary endpoint.
scripts/smoke-test.sh asserts 27 things along the way, including liveness and readiness, the metrics the
alerts depend on, and that the management endpoints are not reachable through the gateway.
Endpoints #
| What | Where |
|---|---|
| API, through the gateway | http://localhost:8080/api/v1/items |
| Inventory summary | http://localhost:8080/api/v1/inventory/summary |
| OpenAPI UI | http://localhost:8080/swagger-ui.html |
| Gateway dashboard | http://localhost:8081/dashboard/ |
| API health and metrics | http://localhost:9090/actuator/health |
| Worker health and metrics | http://localhost:9091/actuator/health |
| Grafana — dashboards, logs, traces; read-only without a login | http://localhost:3000 |
| Prometheus | http://localhost:9095 |
| Alertmanager | http://localhost:9093 |
| Loki and Tempo APIs | http://localhost:3100 · http://localhost:3200 |
Secrets, locally #
Bootstrap writes development-only secret files to deploy/compose/secrets/, which is gitignored. They are
files rather than environment variables because the services read configuration from
configtree:/run/secrets/ — the same mechanism production uses, where the files are projected from a
SOPS-encrypted Kubernetes Secret. Using environment variables locally and files in production would mean the code
path that reads secrets is never exercised until it fails in a cluster.
The file name is the property name: spring.datasource.password becomes the Spring property of the same name.
Exercise the claims #
make obs-validateEvery observability config through its own binary, the alert unit tests and the routing tree — exactly the CI job.make loadSteady load through the gateway, gated on the objectives. RATE=60 DURATION=5m by default.make drillRoll every API pod under load in a local kind cluster; fails on a single failed request.make restore-drillBack up Postgres, destroy it, restore to a point in time; fails on one wrong row. ROWS=20000.make alertsWhat is firing right now, from Alertmanager. Locally, alerts fire and route to nobody.make k8s-validateRender every kustomization and check it against the Kubernetes 1.33 schemas.
make drill needs kind and kubectl; under podman it runs kind rootless. The first run pulls node and
application images and takes several minutes.
Look inside the stores #
make psqlA psql shell against the local database.make redis-cliredis-cli against the cache and read model.make lagConsumer group lag for the worker — its real SLI.make topicsKafka topics and their partition counts.make dlqAnything sitting in the dead-letter topic.make logs-<service>Follow one service: api, worker, gateway, kafka, alloy…make psThe state of every container in the stack.Tear down #
make down stops everything and deletes the volumes. That is destructive on purpose: a teardown that
leaves a stale database volume behind is how a stack that works on one machine stops working on the next.
Every target, with what it does, is in Make targets.