5.5 KiB
Local development
How to run the site on your own machine and how the tests run. The app is one
container (eb-blazor) that reads its content from a second container running
PocketBase (eb-pocketbase); both come up together as a compose stack.
For deploying a built image to Unraid, see deployment.md. For the architecture
reasoning, see architektur.md.
The three compose files
There is one stack, described by a base file plus two overlays. You never edit the base for a local run — you layer an overlay on top of it.
| File | Role |
|---|---|
compose.yaml |
Base, production-shaped: both services, no host ports, PocketBase data on the Unraid bind mount. Also holds the fixed project name eb-stack. |
compose.dev.yaml |
Dev overlay: adds host ports (5000, 8090), builds the web image from local source instead of pulling it, swaps the Unraid bind mount for a throwaway volume, and seeds a local PocketBase superuser. |
compose.test.yaml |
Test overlay: used only by the integration tests. Drops the fixed host port and container names so a test run does not collide with a running dev stack. |
Project name.
name: eb-stackis set once, in the base. The overlays deliberately don't set their ownname:— a competingname:across-ffiles resolves depending on load order (and the CLI and Rider load them in opposite orders), so keeping it in one place avoids surprises. The tests override it with-p eb-test-stack.
Running the dev stack
From Rider (one click)
The run configuration DevEnvironment (.run/DevEnvironment.run.xml, checked
in) starts the stack with --build and force-recreate. It loads the dev overlay
on top of the base for you — just press run.
From the terminal
docker compose -f compose.yaml -f compose.dev.yaml up --build
PocketBase starts first; once its healthcheck passes, the web app starts
(depends_on: service_healthy). Then:
- Web app: http://localhost:5000
- PocketBase admin UI: http://localhost:8090/_/
- PocketBase health: http://localhost:8090/api/health
The dev overlay auto-creates a PocketBase superuser (
test@example.com/test-password) on a fresh volume, so you skip the/_/setup screen. These are throwaway local credentials, not the production ones.
Stop and clean up (removes the throwaway PocketBase volume too):
docker compose -f compose.yaml -f compose.dev.yaml down --volumes
If the admin UI already existed once and you want the superuser recreated from
scratch, remove the volume first: docker volume rm eb-stack_pb_data_dev.
Running the tests
dotnet test
The integration tests (PocketBaseFixture in Elternbeirat.Web.Tests) start
their own PocketBase from the very same compose files — the fixture shells
out to:
docker compose -p eb-test-stack -f compose.yaml -f compose.dev.yaml -f compose.test.yaml up --detach --wait eb-pocketbase
so the image version, environment and port are defined in one place, not
duplicated in the test code. Only eb-pocketbase is started; the web app is not
built for the tests.
Because the test stack has its own project name (eb-test-stack), auto-named
containers and a random host port, it runs happily alongside a running
dev stack — you can have DevEnvironment up in Rider and run dotnet test at the
same time. The fixture cleans up before and after each run, so a hard-killed
earlier run leaves nothing behind.
Requirements: Docker with the Compose plugin must be available when the tests
run (they call docker compose).
Content while developing
The app reads its content from PocketBase over the compose network
(PocketBase__BaseUrl=http://eb-pocketbase:8090 — the service name, resolved
inside the compose network, independent of the container name). Editing content
means editing records in the PocketBase admin UI at
http://localhost:8090/_/, not editing files. See redaktion.md.
Adding a new content type
Editors can add records to the existing collections and add plain text
pages, but a new kind of list — its own collection rendered on its own
route with its own sorting or grouping — is a development task, not editorial.
It takes three steps, all small because the plumbing is shared:
- Collection + DTO. Create the collection in the PocketBase admin (give it a
publicbool and open List/View rules, like the others), then add a matching record type inElternbeirat.Contractswith[JsonPropertyName(...)]on each field — mirrorPage/Post. - Client method. Add one method to
PocketBaseClient. The sharedGetRecordsAsync<T>(collection, sort, ct)already does thepublic=truefilter, the sort and the deserialization, so a new type is a one-liner:public Task<IReadOnlyList<Minutes>> GetMinutesAsync(CancellationToken ct = default)=> GetRecordsAsync<Minutes>("minutes", "-date", ct); - Feature component. Add a component under
Features/<Name>/with its own@page "/…"route that injectsPocketBaseClient, calls the new method and renders the result. Literal routes win over the/{Slug}catch-all, so pick a slug that nopagesrecord needs. Follow an existing list (PostList,EventList,FaqList) for the load-in-OnInitializedAsync, log-and-degrade pattern, and add a route smoke test plus a fixture seed.
Text pages need none of this — they are all served generically by
ContentPage (@page "/{Slug}"), which is why a new pages record is live
without a rebuild.