Getting the whole system running locally, from a fresh clone.
- Docker or Podman, with Compose
- JDK 25 LTS
- Python 3 — only for registering Avro schemas
git clone https://github.com/vertyll/veds.git
cd vedsThere is nothing to configure. Local defaults live next to the thing that needs them — docker-compose.local.yml for the
infrastructure, application-local.yml for the gateway — so a fresh clone runs as-is.
Only the prod profile demands real values, and it takes them from the environment with no fallback:
| Variable | Used by |
|---|---|
REDIS_PASSWORD |
application-prod.yml, and docker-compose.local.yml as an override |
GATEWAY_SESSION_ENCRYPTION_KEY |
application-prod.yml — must decode to 32 bytes |
openssl rand -base64 32 # GATEWAY_SESSION_ENCRYPTION_KEYdocker compose -f docker-compose.local.yml up -dEvery docker compose command in this document works verbatim as podman compose — the
compose file uses nothing Docker-specific.
This brings up PostgreSQL (one database per service), Keycloak, Kafka with Schema Registry, Redis, Garage and MailDev. Three one-shot jobs run automatically and then exit — they are supposed to:
topics-initappliesinfra/kafka/topics.tf, creating every topicschemas-initregisters every Avro schema undercontracts/with the Schema Registryobject-storage-initgives Garage its cluster layout, bucket, access key and CORS rules
None of this can be expressed in a configuration file: it is cluster state. topics-init applies it with
OpenTofu, schemas-init with scripts/schema_registry/register_schemas.py, object-storage-init through the
Garage admin API.
Wait for the health checks before moving on:
docker compose -f docker-compose.local.yml psschemas-init registers the schemas on every up. After changing a schema, register it again without restarting
the stack:
python scripts/schema_registry/register_schemas.py --registry-url http://localhost:8081Producers register on first publish, but doing it up front means an incompatible schema is caught now rather than at runtime, and consumers can start in any order.
./gradlew clean buildThe root project is a composite build aggregating every module. It also exposes ktlintCheck,
ktlintFormat, detekt, test and checkHexagonalDependencies across all included builds; check runs the
verification tasks together. -contracts modules are excluded from those aggregators because they contain only
generated Avro classes, and checkHexagonalDependencies covers the -service builds only — api-gateway and the
shared-* libraries have no application layer to check.
To build one service on its own:
cd <service-name> && ./gradlew buildEach in its own terminal, or through the .run configurations in IntelliJ (All_services.run.xml starts everything):
cd <service-name>
./gradlew bootRunNote
Order matters in one place only. Every service registers its translation keys with translation-service at
start-up, so starting that one first avoids a failed registration in the logs. Nothing breaks if you do not:
registration failure is deliberately non-fatal, and the keys are republished on the next restart.
| Service | Port |
|---|---|
api-gateway |
8080 |
iam-service |
8082 |
mail-service |
8083 |
project-service |
8084 |
task-service |
8085 |
notification-service |
8086 |
translation-service |
8087 |
file-service |
8088 |
template-service is a reference for cloning and is not meant to be run.
| Component | URL |
|---|---|
| API Gateway | http://localhost:8080 |
| Front end | http://localhost:4200 |
| Keycloak | http://localhost:9000 |
| Kafka UI | http://localhost:8090 |
| Schema Registry | http://localhost:8081 |
| MailDev | http://localhost:1080 |
| Object storage (S3) | http://localhost:9100 |
| Object storage console | http://localhost:9101 |
Databases are exposed on 5432 (iam), 5433 (mail), 5434 (keycloak), 5435 (project), 5436 (task), 5437 (notification), 5438 (translation), 5439 (file).
schemas-init fails on a second compose up if a schema was reshaped — a renamed namespace or a changed field type is,
correctly, incompatible with what the registry already holds. Locally the registry is disposable:
python scripts/schema_registry/register_schemas.py --registry-url http://localhost:8081 --resetWarning
--reset drops each subject before registering. Never use it against a shared registry — there the refusal is the
feature.
./gradlew build runs the unit tests only. The integration tests need a container runtime and are tagged out of the
default build:
./gradlew test -PintegrationTestsThe flag widens what each build runs; every included build takes part either way. shared-messaging-kafka has
integration tests of its own, so narrowing by name would silently skip them.
Under Podman they also need DOCKER_HOST and TESTCONTAINERS_RYUK_DISABLED — see
Testing.
Each service serves its own Swagger UI at /swagger-ui.html — for example
http://localhost:8084/swagger-ui.html for project-service.
Library API documentation is generated with Dokka:
./gradlew docs # output in docs/dokka/An Insomnia collection is provided at insomnia-collection.yaml.
Every service exposes Spring Boot Actuator at /actuator/health.
./gradlew ktlintFormat # format
./gradlew ktlintCheck # verify
./gradlew detekt # static analysischeck additionally runs checkHexagonalDependencies, which fails the build if a framework reaches a service's
application layer. See Hexagonal Layering.
| Symptom | Cause |
|---|---|
| Gateway cannot authenticate against Redis | REDIS_PASSWORD exported for compose but not for bootRun, or the reverse — the two then disagree |
| Gateway exits at start-up complaining about a key | GATEWAY_SESSION_ENCRYPTION_KEY overridden with a value that is not 32 base64-decoded bytes |
| Uploads fail in the browser with a CORS error | FRONTEND_ORIGIN does not match where the SPA runs; re-run object-storage-init |
The UI shows keys such as project.not_found |
translation-service is not running, or the services started before it and have not been restarted |
| A consumer logs a schema error | Step 3 was skipped |
| Login redirects but never returns | Keycloak is not healthy yet, or the realm import has not finished |