feat(dev): local docker-compose stack
ci / shared (push) Successful in 5s
ci / validate (push) Successful in 2s

Adds dev/docker-compose.yml + breakpilot-dev Keycloak realm + make dev-up/down/reset/logs targets so a developer can boot the full dependency stack from this repo.
This commit was merged in pull request #4.
This commit is contained in:
2026-05-19 09:35:02 +00:00
parent 8e37f65b8e
commit e3a62c8113
6 changed files with 444 additions and 1 deletions
+1
View File
@@ -6,6 +6,7 @@ Generated section is appended on release tag via `git-cliff` (see `.gitea/workfl
## [Unreleased]
### Added
- feat(dev): local docker-compose stack (Keycloak + Postgres + Redis + Mongo + MinIO) with pre-imported breakpilot-dev realm and seed users
- feat(iac): scaffold orca-platform — manifests/, overlays/, dns/, scripts/, Makefile (M1.1)
-
+29 -1
View File
@@ -5,11 +5,18 @@
# make apply ENV=<env> (M1.2+) push resolved set to Orca controller
# make diff ENV=<env> alias for plan
# make clean remove .orca-out/
#
# Local dev stack (dev/docker-compose.yml):
# make dev-up bring up Keycloak + Postgres + Redis + Mongo + MinIO
# make dev-down stop, keep volumes
# make dev-reset stop, wipe volumes, fresh start
# make dev-logs tail logs from every service
.PHONY: help validate plan apply diff clean
.PHONY: help validate plan apply diff clean dev-up dev-down dev-reset dev-logs
ENV ?=
ORCA_API_URL ?=
COMPOSE = docker compose -f dev/docker-compose.yml
help:
@echo "orca-platform targets:"
@@ -18,6 +25,12 @@ help:
@echo " make apply ENV=<env> push resolved set (no-op until M1.2)"
@echo " make diff ENV=<env> alias for plan"
@echo " make clean remove .orca-out/"
@echo ""
@echo "Local dev stack:"
@echo " make dev-up bring up the dev compose stack"
@echo " make dev-down stop, keep volumes"
@echo " make dev-reset stop, wipe volumes, fresh start"
@echo " make dev-logs tail logs"
validate:
@./scripts/validate.sh
@@ -33,3 +46,18 @@ apply:
clean:
@rm -rf .orca-out
@echo "removed .orca-out/"
dev-up:
@$(COMPOSE) up -d
@echo "Keycloak: http://localhost:8080 (admin / admin-dev-pass on master realm)"
@echo " realm 'breakpilot-dev', user test@breakpilot.dev / test"
dev-down:
@$(COMPOSE) down
dev-reset:
@$(COMPOSE) down -v
@$(COMPOSE) up -d
dev-logs:
@$(COMPOSE) logs -f
+14
View File
@@ -42,6 +42,8 @@ The single source of truth for which container runs on which VM in which environ
## Run locally
### IaC validation
```bash
make validate # check all manifests parse + have required fields
make plan ENV=stage # resolve manifests for stage → .orca-out/stage/
@@ -51,6 +53,18 @@ make apply ENV=stage # no-op until M1.2 stands up the Orca controller
`make validate` runs in CI on every PR.
### Dev stack
`platform/orca-platform` doubles as the home for the local-dev compose stack so a developer can clone this repo, run `make dev-up`, and immediately work against a real Keycloak realm + Postgres / Redis / Mongo / MinIO. See [`dev/README.md`](./dev/README.md) for the full picture.
```bash
make dev-up # Keycloak (:8080) + Postgres (:5432) + Redis (:6379) + Mongo (:27017) + MinIO (:9000)
make dev-down # stop, keep volumes
make dev-reset # stop, wipe, fresh
```
Seed user: `test@breakpilot.dev` / `test` (tenant `acme`, products `certifai` + `compliance`).
## Per-milestone fill-in schedule
Each stub manifest in `manifests/` carries a header comment naming the milestone that finalises its real values. Summary:
+62
View File
@@ -0,0 +1,62 @@
# Local dev stack
Docker-compose that brings up just enough infrastructure to run `platform/tenant-registry` and `platform/portal` locally with a real Keycloak realm + seed user.
## What's running
| Service | Port | Purpose |
|---|---|---|
| Keycloak 26 | `:8080` | OIDC provider for portal + tenant-registry |
| pg-keycloak | (internal only) | Keycloak's backing Postgres |
| pg-app | `:5432` | Tenant Registry's Postgres (`platform/platform/platform-dev-pass`) |
| Redis | `:6379` | Session cache for portal |
| Mongo | `:27017` | (Future) CERTifAI data store |
| MinIO | `:9000` / `:9001` console | (Future) Compliance evidence object store |
Stack is loopback only — no public exposure, no DNS, no TLS. Tenant-registry and portal run on the **host**, not in this compose.
## Run
From the repo root:
```bash
make dev-up # bring everything up; first start takes ~30s for Keycloak realm import
make dev-down # stop, keep volumes
make dev-reset # stop, wipe volumes, fresh start
make dev-logs # tail logs from every service
```
## First login
The realm `breakpilot-dev` ships pre-imported with two users:
| Email | Password | Role |
|---|---|---|
| `test@breakpilot.dev` | `test` | IT_ADMIN of tenant `acme`, products: certifai + compliance |
| `admin@breakpilot.dev`| `admin` | BREAKPILOT_ADMIN (platform staff, backstage access) |
Verify Keycloak is up: <http://localhost:8080> → click "Administration Console" → log in as `admin`/`admin-dev-pass` (master admin) → switch realm to `breakpilot-dev`.
## Realm contents
- **Clients:**
- `dev-portal` — public PKCE client used by `platform/portal`. Redirect URIs cover `http://localhost:3000/*` and `http://*.localhost:3000/*` so subdomain routing works in dev.
- `dev-tenant-registry` — bearer-only client used by the Go service to validate JWTs.
- **Protocol mappers on `dev-portal`** put `tenant_id`, `tenant_slug`, `org_roles`, `products`, `plan`, and `tenant_status` claims into every issued token. The portal middleware reads these to resolve the tenant + render the dashboard. The `test@breakpilot.dev` user has these claims set as user attributes — edit the user in Keycloak admin to flip flags during dev.
- **Realm roles** (platform-staff): `BREAKPILOT_ADMIN`, `SUPPORT_ENGINEER`, `SALES_REP`.
## Reset just Keycloak
If you mess up the realm and want to reimport the JSON:
```bash
docker compose -f dev/docker-compose.yml down keycloak pg-keycloak
docker volume rm breakpilot-dev_pg-keycloak-data
make dev-up
```
The other services' data survives.
## Why this is in `orca-platform`
This is the only repo that already knows the whole topology — manifests in `manifests/` reference the same images this compose runs. Keeping the dev stack here means a developer clones `orca-platform`, runs `make dev-up`, and is ready to clone the service repo they actually want to work on. Per `INFRASTRUCTURE.md §1` dev runs entirely on developer laptops via docker-compose; this is that compose file.
+139
View File
@@ -0,0 +1,139 @@
# Local-dev stack for the Breakpilot Platform.
#
# Brings up the dependencies a developer needs to run tenant-registry +
# portal against a real Keycloak realm with seed data — no SysEleven VMs,
# no PowerDNS, no Stalwart. Loopback only.
#
# make dev-up bring this stack up
# make dev-down stop it, keep volumes
# make dev-reset stop, wipe volumes, bring up fresh
#
# Networking: every service joins the `breakpilot-dev` bridge network so
# tenant-registry and portal (running on the host, not in this compose)
# can reach Postgres etc. via `localhost:<port>`. Inter-service traffic
# inside the compose (Keycloak → pg-keycloak) uses the service name.
name: breakpilot-dev
networks:
breakpilot-dev:
driver: bridge
volumes:
pg-keycloak-data:
pg-app-data:
mongo-data:
minio-data:
services:
# ─── Identity ──────────────────────────────────────────────────────────
pg-keycloak:
image: postgres:16-alpine
restart: unless-stopped
networks: [breakpilot-dev]
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak-dev-pass
volumes:
- pg-keycloak-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 5s
timeout: 3s
retries: 10
keycloak:
image: quay.io/keycloak/keycloak:26.0
restart: unless-stopped
depends_on:
pg-keycloak:
condition: service_healthy
networks: [breakpilot-dev]
ports:
- "8080:8080"
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://pg-keycloak:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak-dev-pass
KC_HEALTH_ENABLED: "true"
KC_HOSTNAME: localhost
KC_HTTP_ENABLED: "true"
KC_PROXY_HEADERS: xforwarded
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin-dev-pass
command:
- start-dev
- --import-realm
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/breakpilot-dev.json:ro
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q UP <&3"]
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
# ─── App data ──────────────────────────────────────────────────────────
pg-app:
image: postgres:16-alpine
restart: unless-stopped
networks: [breakpilot-dev]
ports:
- "5432:5432"
environment:
POSTGRES_DB: platform
POSTGRES_USER: platform
POSTGRES_PASSWORD: platform-dev-pass
volumes:
- pg-app-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U platform -d platform"]
interval: 5s
timeout: 3s
retries: 10
# ─── Caches + object stores ────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: unless-stopped
networks: [breakpilot-dev]
ports:
- "6379:6379"
mongo:
image: mongo:7
restart: unless-stopped
networks: [breakpilot-dev]
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: mongo-dev-pass
volumes:
- mongo-data:/data/db
healthcheck:
test: ["CMD-SHELL", "echo 'db.runCommand({ping:1})' | mongosh --quiet"]
interval: 10s
timeout: 3s
retries: 6
minio:
image: minio/minio:latest
restart: unless-stopped
networks: [breakpilot-dev]
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minio-dev
MINIO_ROOT_PASSWORD: minio-dev-pass
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/minio/health/live"]
interval: 10s
timeout: 3s
retries: 5
+199
View File
@@ -0,0 +1,199 @@
{
"realm": "breakpilot-dev",
"enabled": true,
"displayName": "Breakpilot (dev)",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"accessTokenLifespan": 900,
"ssoSessionIdleTimeout": 3600,
"ssoSessionMaxLifespan": 36000,
"internationalizationEnabled": false,
"defaultSignatureAlgorithm": "RS256",
"organizationsEnabled": true,
"roles": {
"realm": [
{ "name": "BREAKPILOT_ADMIN", "description": "Platform staff: full backstage access" },
{ "name": "SUPPORT_ENGINEER", "description": "Platform staff: tenant impersonation + read" },
{ "name": "SALES_REP", "description": "Platform staff: demo-tenant access only" }
]
},
"groups": [
{
"name": "IT_ADMIN",
"path": "/IT_ADMIN",
"attributes": { "org_role": ["IT_ADMIN"] }
},
{
"name": "CXO",
"path": "/CXO",
"attributes": { "org_role": ["CXO"] }
},
{
"name": "USER",
"path": "/USER",
"attributes": { "org_role": ["USER"] }
}
],
"users": [
{
"username": "test@breakpilot.dev",
"email": "test@breakpilot.dev",
"emailVerified": true,
"firstName": "Test",
"lastName": "Acme",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "test",
"temporary": false
}
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000001"],
"tenant_slug": ["acme"],
"org_roles": ["IT_ADMIN"],
"products": ["certifai", "compliance"],
"plan": ["professional"],
"tenant_status": ["active"]
},
"groups": ["/IT_ADMIN"]
},
{
"username": "admin@breakpilot.dev",
"email": "admin@breakpilot.dev",
"emailVerified": true,
"firstName": "Platform",
"lastName": "Admin",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "admin",
"temporary": false
}
],
"realmRoles": ["BREAKPILOT_ADMIN"]
}
],
"clients": [
{
"clientId": "dev-portal",
"name": "Customer Portal (dev)",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"rootUrl": "http://localhost:3000",
"baseUrl": "http://localhost:3000",
"redirectUris": [
"http://localhost:3000/*",
"http://acme.localhost:3000/*",
"http://demo.localhost:3000/*"
],
"webOrigins": ["+"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:3000/*##http://*.localhost:3000/*"
},
"protocolMappers": [
{
"name": "tenant_id-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "tenant_slug-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_slug",
"claim.name": "tenant_slug",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "org_roles-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "org_roles",
"claim.name": "org_roles",
"jsonType.label": "String",
"multivalued": "true",
"id.token.claim": "true",
"access.token.claim": "true"
}
},
{
"name": "products-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "products",
"claim.name": "products",
"jsonType.label": "String",
"multivalued": "true",
"id.token.claim": "true",
"access.token.claim": "true"
}
},
{
"name": "plan-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "plan",
"claim.name": "plan",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true"
}
},
{
"name": "tenant_status-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_status",
"claim.name": "tenant_status",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
},
{
"clientId": "dev-tenant-registry",
"name": "Tenant Registry (dev)",
"enabled": true,
"bearerOnly": true,
"publicClient": false,
"standardFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false
}
]
}