The three services Harbor talks to while you are working on it. None of them is optional: the library lives in PostgreSQL, Harbor refuses to save a page it cannot archive, and login is mandatory on every route.
./run.sh env upThen start the app as usual; application.properties defaults to exactly these
containers, so no configuration is needed.
./run.sh runOpen http://localhost:8080 and sign in as reader / reader.
One task for the whole stack rather than one per service: which containers it brings
up is the stack definition's decision, so a service added there needs no change to
run.sh. ./run.sh env down stops them and keeps the data; ./run.sh env reset
throws the volumes away, which is how you get back to a first-run empty library.
./run.sh env logs follows all three at once.
The stack is defined twice, and ./run.sh env picks by what is installed.
Under docker it is compose.yaml, run with up -d --wait.
Under podman it is the quadlets in quadlet/ — systemd units that podman's
generator turns into services — because compose is not how podman is meant to be
driven. systemctl starts them, and Notify=healthy on every container is what makes
systemd treat a unit as started only once its healthcheck passes: that is what buys
the ordering depends_on: service_healthy and up --wait bought under compose, which
is the whole reason the realm setup can assume Keycloak is answering.
The files are templates (*.in). A quadlet cannot say where this checkout lives, and
two of them mount a file out of this directory, so ./run.sh env up substitutes the
path and copies them into ~/.config/containers/systemd. Editing a unit therefore
means re-running ./run.sh env up.
The units are started, never enabled — whether this stack should come back after a
reboot is not run.sh's call. systemctl --user enable harbor-dev-postgres.service
and friends if you want it to.
Keeping both definitions in step is manual: a service added to one needs adding to the other.
Keycloak starts empty. keycloak/init.mjs then creates the realm over the admin REST
API, from a keycloak-init container that runs once and exits — so a fresh checkout
plus ./run.sh env up is a working identity provider with no manual steps and no
clicking through the admin console.
Not a realm export imported with --import-realm. An export has to be an exactly
correct RealmRepresentation or the container refuses to boot, and it says so through
a Jackson "unrecognized field" error that names nothing about Keycloak. A wrong field
here is an HTTP 400 that names it instead. Each step looks up what it would create and
skips it if it is already there, so re-running ./run.sh env up is safe.
Keycloak reports readiness on its management port, and the image carries no curl and no
wget — so keycloak/HealthCheck.java is the probe, run straight from source by the
image's own JVM. keycloak-init waits on that rather than on the port, because
Keycloak binds long before it can answer.
| Realm | harbor, at http://localhost:8081/realms/harbor |
| Client | harbor, confidential, secret harbor-dev-secret |
| Reader | reader / reader |
| Admin console | http://localhost:8081, admin / admin |
Keycloak assigns the reader user's id, and the admin API ignores one supplied on
create — only a realm import can pin it. That id is the sub in every token, so it is
the owner_id of every row the reader writes: throwing away the Keycloak container on
its own orphans the development library, because the reader comes back as somebody
else while the old rows stay in Postgres. ./run.sh env reset clears both together.
None of this reaches the test suite. HarborJourneyIT starts a Keycloak of its own
through HarborIdentity, which builds its own realm, hands its own client id and secret
to Spring, and reads its reader's id back out of Keycloak — so a journey depends on
nothing in this directory.
The client accepts * as its redirect URI. A realm's redirect URIs are not Harbor's
to get right — a deployment adds Harbor's exact callback
(https://your-harbor/login/oauth2/code/oidc, where oidc is Harbor's registration id
rather than the realm or the client) to a provider it already runs, very
likely alongside other applications. What this realm exists to prove is that Harbor
speaks OIDC, so it accepts whatever port a laptop or a test happens to be on.
A second reader, for checking that one library really is invisible to another: add a user in the admin console, give it a password, and sign in as it from a private window.
Every credential in this directory is published and guessable — harbor/harbor on a
published 5432, admin/admin on the Keycloak console, a client secret in version
control, and a client that will redirect anywhere at all. An unrestricted redirect URI
is normally a finding, and it is one here too the moment this reaches a machine anyone
else can reach.
This is a laptop's configuration. A deployment sets HARBOR_DB_*,
HARBOR_BROWSER_URL and the HARBOR_OIDC_* trio against a realm of its own, with
its own secret and its own exact redirect URI. Nothing here is a starting point for
that.
Requires either podman, or docker with the Compose plugin (docker compose or the
standalone docker-compose). Rootless podman also needs its socket listening —
systemctl --user start podman.socket — which run.sh will tell you about rather than
do for you. The test suite needs the same runtime for a different reason:
Testcontainers starts its own throwaway PostgreSQL, Chromium and Keycloak, and sets the
last of those up itself rather than using anything in this directory.