refactor(compose): unify into single file with local/dev/demo profiles
Some checks failed
Holocron Frontend CI / lint-and-test (push) Has been cancelled

K8s is the production target — docker compose is local-only. Collapse the
three compose files (docker-compose.yml, .local.yml, .local-dev.yml) into a
single docker-compose.yml driven by profiles:

  local  → full stack in containers
  dev    → backing services only; frontend on host (npm run start:dev)
  demo   → synthetic MQTT publisher (composable with local or dev)

Make targets:
  make up      → --profile local --profile demo
  make up-dev  → --profile dev   --profile demo
  make up-bare → --profile local (no synthetic data)

All values (MQTT_TOPIC_PREFIX, MAPTILER_KEY, KEYCLOAK_*, host port mappings)
are env-driven with sensible local defaults. The same env contract is what
the K8s ConfigMap will inject in production.

Also:
- Bump frontend Dockerfile (local/dev) from node:18 → node:20-alpine.
  Vite 7 requires crypto.hash (Node 20.12+). Also bumps the test-publisher
  base image to node:20-alpine.
- Default host MQTT port moved to 1885 and frontend to 3002 (compose env
  defaults) to avoid common conflicts; both overridable via .env.

Verified:
- docker compose --profile local --profile demo config parses OK
- docker compose --profile dev   --profile demo config parses OK
- `make up` brings up 6 healthy containers
- curl localhost:7070/health → 200
- curl localhost:3002 → 200
- Test publisher streams UAV/SHIP/HELO/TANK positions on r2d2/holocron/...
- Browser screenshot confirms Cesium map rendering with live MQTT assets

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Belke 2026-05-28 09:57:46 -04:00
commit 88ffa3cad5
7 changed files with 134 additions and 268 deletions

View file

@ -1,8 +1,35 @@
# Copy to .env and set your keys. Do not commit .env.
# MAPTILER_KEY=your_maptiler_api_key_here
# MAPTILER_KEY=
# Copy to .env and set your values. Do not commit .env.
#
# This same env contract is used by the K8s production deployment.
# Production values come from a ConfigMap/Secret in the cluster; local values
# come from this file.
# Optional: production / non-local
# ─── MQTT topic prefix ──────────────────────────────────────────────
# Local default: r2d2/holocron
# Production: set per environment (e.g. r2d2-prod/holocron, nato/holocron/...)
MQTT_TOPIC_PREFIX=r2d2/holocron
# ─── Map tiles ──────────────────────────────────────────────────────
# Get a free key at https://maptiler.com
MAPTILER_KEY=
# ─── Host port mappings (local docker compose only) ─────────────────
# Override if these conflict with other services on your machine.
PROXY_PORT=7070
FRONTEND_PORT=3002
WS_PORT=9002
MQTT_PORT=1885
MQTT_WS_PORT=9001
# ─── Keycloak ───────────────────────────────────────────────────────
# Local defaults are fine; production overrides via Secret.
KEYCLOAK_DB_NAME=keycloak_db
KEYCLOAK_DB_USER=keycloakuser
KEYCLOAK_DB_PASSWORD=keycloakuser
KEYCLOAK_ADMIN=adminkeycloak
KEYCLOAK_ADMIN_PASSWORD=adminkeycloak
# ─── Production-only (set in K8s ConfigMap, not here) ───────────────
# CORS_ORIGIN=https://app.example.com,https://other.example.com
# WS_PUBLIC_URL=wss://app.example.com/ws
# MAX_MESSAGE_BYTES=524288

View file

@ -1,23 +1,44 @@
# HOLOCRON - Local development targets
# R2D2-Holocron — local development targets
#
# Production deploys to Kubernetes, not docker compose. See docs/plans/ for K8s.
#
# Profiles defined in docker-compose.yml:
# local → full stack in containers
# dev → backing services only; frontend on host (npm run start:dev)
# demo → synthetic MQTT publisher
.PHONY: up down build logs clean
.PHONY: up up-dev up-bare down logs rebuild clean ps config
# Start the local Docker stack (builds images on first run)
COMPOSE := docker compose
# Default: full local stack with demo data
up:
docker compose -f docker-compose.local.yml up --build -d
$(COMPOSE) --profile local --profile demo up --build -d
# Dev mode: backing services in docker, frontend on host
up-dev:
$(COMPOSE) --profile dev --profile demo up --build -d
@echo ""
@echo "Now start the frontend on the host:"
@echo " cd holocron-frontend && npm run start:dev"
# Local stack without synthetic data (for real MQTT feeds)
up-bare:
$(COMPOSE) --profile local up --build -d
# Stop the stack
down:
docker compose -f docker-compose.local.yml down
$(COMPOSE) --profile local --profile dev --profile demo down
# View logs (follow)
logs:
docker compose -f docker-compose.local.yml logs -f
$(COMPOSE) logs -f
# Rebuild and restart
rebuild: down
docker compose -f docker-compose.local.yml up --build -d
rebuild: down up
# Remove containers and volumes
clean: down
docker compose -f docker-compose.local.yml down -v
clean:
$(COMPOSE) --profile local --profile dev --profile demo down -v
ps:
$(COMPOSE) ps
config:
$(COMPOSE) --profile local --profile demo config

View file

@ -1,104 +0,0 @@
---
# Local development: all services in Docker EXCEPT the frontend.
# Frontend runs on the host with hot-reload (Vite).
#
# 1. Start stack: docker compose -f docker-compose.local-dev.yml up --build
# 2. Start frontend: cd holocron-frontend && npm run start:dev
# 3. Open: http://localhost:7070 (proxy serves app from host:3333)
#
# Endpoints:
# - App (via proxy from host): http://localhost:7070
# - Keycloak: http://localhost:7070/auth
# - Health: http://localhost:7070/health
# - Mosquitto: 1883, 9001
# - Frontend dev server: http://localhost:3333 (direct, optional)
# - Frontend API/WS (on host): http://localhost:3001, ws://localhost:9002
services:
holocron-keycloak-postgresql:
image: postgres:15-alpine
container_name: holocron-keycloak-postgresql-local
restart: unless-stopped
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak_db
POSTGRES_USER: keycloakuser
POSTGRES_PASSWORD: keycloakuser
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloakuser -d keycloak_db"]
interval: 15s
timeout: 15s
retries: 5
holocron-keycloak:
build:
context: ./holocron-keycloak
dockerfile: Dockerfile.local.public
image: holocron-keycloak:local
container_name: holocron-keycloak-local
restart: unless-stopped
command:
[
"-Dkeycloak.migration.action=import",
"-Dkeycloak.migration.provider=dir",
"-Dkeycloak.migration.dir=/opt/keycloak/data/import",
"-Dkeycloak.migration.strategy=IGNORE_EXISTING"
]
environment:
KC_DB_URL: jdbc:postgresql://holocron-keycloak-postgresql-local/keycloak_db
KC_DB_USERNAME: keycloakuser
KC_DB_PASSWORD: keycloakuser
KEYCLOAK_ADMIN: adminkeycloak
KEYCLOAK_ADMIN_PASSWORD: adminkeycloak
KC_HTTP_PORT: 8090
depends_on:
holocron-keycloak-postgresql:
condition: service_healthy
holocron-proxy:
build:
context: ./holocron-proxy
dockerfile: Dockerfile.local.public
image: holocron-proxy:local
container_name: holocron-proxy-local
restart: unless-stopped
ports:
- "7070:7070"
volumes:
- ./holocron-proxy/nginx/local-dev.conf:/etc/nginx/conf.d/default.conf
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- holocron-keycloak
mosquitto:
image: eclipse-mosquitto:2
container_name: holocron-mosquitto-local
restart: unless-stopped
ports:
- "1883:1883"
- "9001:9001"
volumes:
- ./mosquitto/config:/mosquitto/config
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
test-publisher:
image: node:18-alpine
container_name: holocron-test-publisher-local
working_dir: /app
volumes:
- ./holocron-cesium/test-publisher:/app
command: sh -c "npm install && node publisher.js"
environment:
MQTT_BROKER: mqtt://mosquitto:1883
MQTT_TOPIC_PREFIX: ${MQTT_TOPIC_PREFIX:-r2d2/holocron}
depends_on:
- mosquitto
restart: unless-stopped
volumes:
keycloak_postgres_data:
mosquitto_data:
mosquitto_log:

View file

@ -1,118 +0,0 @@
---
# Local development stack using PUBLIC Docker Hub images.
# Run from project root: docker compose -f docker-compose.local.yml up --build
#
# Endpoints:
# - Proxy: http://localhost:7070
# - Health: http://localhost:7070/health
# - Keycloak: http://localhost:7070/auth
# - Frontend: http://localhost:3001
# - WebSocket: ws://localhost:9002
services:
holocron-keycloak-postgresql:
image: postgres:15-alpine
container_name: holocron-keycloak-postgresql-local
restart: unless-stopped
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak_db
POSTGRES_USER: keycloakuser
POSTGRES_PASSWORD: keycloakuser
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloakuser -d keycloak_db"]
interval: 15s
timeout: 15s
retries: 5
holocron-keycloak:
build:
context: ./holocron-keycloak
dockerfile: Dockerfile.local.public
image: holocron-keycloak:local
container_name: holocron-keycloak-local
restart: unless-stopped
command:
[
"-Dkeycloak.migration.action=import",
"-Dkeycloak.migration.provider=dir",
"-Dkeycloak.migration.dir=/opt/keycloak/data/import",
"-Dkeycloak.migration.strategy=IGNORE_EXISTING"
]
environment:
KC_DB_URL: jdbc:postgresql://holocron-keycloak-postgresql-local/keycloak_db
KC_DB_USERNAME: keycloakuser
KC_DB_PASSWORD: keycloakuser
KEYCLOAK_ADMIN: adminkeycloak
KEYCLOAK_ADMIN_PASSWORD: adminkeycloak
KC_HTTP_PORT: 8090
depends_on:
holocron-keycloak-postgresql:
condition: service_healthy
holocron-proxy:
build:
context: ./holocron-proxy
dockerfile: Dockerfile.local.public
image: holocron-proxy:local
container_name: holocron-proxy-local
restart: unless-stopped
ports:
- "7070:7070"
depends_on:
- holocron-keycloak
mosquitto:
image: eclipse-mosquitto:2
container_name: holocron-mosquitto-local
restart: unless-stopped
ports:
- "1883:1883"
- "9001:9001"
volumes:
- ./mosquitto/config:/mosquitto/config
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
holocron-frontend:
build:
context: ./holocron-frontend
dockerfile: Dockerfile.local
image: holocron-frontend:local
container_name: holocron-frontend-local
restart: unless-stopped
env_file:
- .env
ports:
- "3001:3001"
- "9002:9002"
environment:
- NODE_ENV=production
- PORT=3001
- WS_PORT=9002
- MQTT_BROKER=mqtt://mosquitto:1883
- MQTT_TOPIC_PREFIX=${MQTT_TOPIC_PREFIX:-r2d2/holocron}
- MAPTILER_KEY=${MAPTILER_KEY:-}
depends_on:
- mosquitto
# Sample data publisher: sends test assets to MQTT so the map shows UAVs, ships, etc.
test-publisher:
image: node:18-alpine
container_name: holocron-test-publisher-local
working_dir: /app
volumes:
- ./holocron-cesium/test-publisher:/app
command: sh -c "npm install && node publisher.js"
environment:
MQTT_BROKER: mqtt://mosquitto:1883
MQTT_TOPIC_PREFIX: ${MQTT_TOPIC_PREFIX:-r2d2/holocron}
depends_on:
- mosquitto
restart: unless-stopped
volumes:
keycloak_postgres_data:
mosquitto_data:
mosquitto_log:

View file

@ -1,32 +1,50 @@
---
# Local development stack using PUBLIC Docker Hub images.
# Run from project root: docker compose -f docker-compose.local.yml up --build
# Unified docker-compose for R2D2-Holocron.
#
# Endpoints:
# - Proxy: http://localhost:7070
# - Health: http://localhost:7070/health
# - Keycloak: http://localhost:7070/auth
# - Frontend: http://localhost:3001
# - WebSocket: ws://localhost:9002
# Profiles:
# local → full stack in containers (frontend served by Node from image)
# dev → backing services only; run frontend on host with `npm run start:dev`
# (proxy is configured to forward to host.docker.internal)
# demo → adds test-publisher streaming synthetic MQTT assets
#
# Production deploys to Kubernetes (not this file). The env contract below
# (MQTT_TOPIC_PREFIX, MAPTILER_KEY, KEYCLOAK_*, etc.) is the same contract the
# K8s manifests will inject via ConfigMap/Secret. Keep it stable.
#
# Usage:
# make up # local + demo (default)
# make up-dev # dev + demo
# docker compose --profile local up -d # local only (no test publisher)
# docker compose --profile dev up -d # dev only
#
# Endpoints (local profile):
# - Proxy: http://localhost:${PROXY_PORT:-7070}
# - Health: http://localhost:${PROXY_PORT:-7070}/health
# - Keycloak: http://localhost:${PROXY_PORT:-7070}/auth
# - Frontend: http://localhost:${FRONTEND_PORT:-3002}
# - WebSocket: ws://localhost:${WS_PORT:-9002}
# - MQTT: tcp://localhost:${MQTT_PORT:-1885}
services:
holocron-keycloak-postgresql:
profiles: [local, dev]
image: postgres:15-alpine
container_name: holocron-keycloak-postgresql-local
restart: unless-stopped
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak_db
POSTGRES_USER: keycloakuser
POSTGRES_PASSWORD: keycloakuser
POSTGRES_DB: ${KEYCLOAK_DB_NAME:-keycloak_db}
POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloakuser}
POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloakuser}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloakuser -d keycloak_db"]
test: ["CMD-SHELL", "pg_isready -U ${KEYCLOAK_DB_USER:-keycloakuser} -d ${KEYCLOAK_DB_NAME:-keycloak_db}"]
interval: 15s
timeout: 15s
retries: 5
holocron-keycloak:
profiles: [local, dev]
build:
context: ./holocron-keycloak
dockerfile: Dockerfile.local.public
@ -41,17 +59,18 @@ services:
"-Dkeycloak.migration.strategy=IGNORE_EXISTING"
]
environment:
KC_DB_URL: jdbc:postgresql://holocron-keycloak-postgresql-local/keycloak_db
KC_DB_USERNAME: keycloakuser
KC_DB_PASSWORD: keycloakuser
KEYCLOAK_ADMIN: adminkeycloak
KEYCLOAK_ADMIN_PASSWORD: adminkeycloak
KC_DB_URL: jdbc:postgresql://holocron-keycloak-postgresql-local/${KEYCLOAK_DB_NAME:-keycloak_db}
KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloakuser}
KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloakuser}
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-adminkeycloak}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-adminkeycloak}
KC_HTTP_PORT: 8090
depends_on:
holocron-keycloak-postgresql:
condition: service_healthy
holocron-proxy:
profiles: [local]
build:
context: ./holocron-proxy
dockerfile: Dockerfile.local.public
@ -59,23 +78,42 @@ services:
container_name: holocron-proxy-local
restart: unless-stopped
ports:
- "7070:7070"
- "${PROXY_PORT:-7070}:7070"
depends_on:
- holocron-keycloak
holocron-proxy-dev:
profiles: [dev]
build:
context: ./holocron-proxy
dockerfile: Dockerfile.local.public
image: holocron-proxy:local
container_name: holocron-proxy-local
restart: unless-stopped
ports:
- "${PROXY_PORT:-7070}:7070"
volumes:
- ./holocron-proxy/nginx/local-dev.conf:/etc/nginx/conf.d/default.conf
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- holocron-keycloak
mosquitto:
profiles: [local, dev]
image: eclipse-mosquitto:2
container_name: holocron-mosquitto-local
restart: unless-stopped
ports:
- "1883:1883"
- "9001:9001"
- "${MQTT_PORT:-1885}:1883"
- "${MQTT_WS_PORT:-9001}:9001"
volumes:
- ./mosquitto/config:/mosquitto/config
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
holocron-frontend:
profiles: [local]
build:
context: ./holocron-frontend
dockerfile: Dockerfile.local
@ -85,8 +123,8 @@ services:
env_file:
- .env
ports:
- "3001:3001"
- "9002:9002"
- "${FRONTEND_PORT:-3002}:3001"
- "${WS_PORT:-9002}:9002"
environment:
- NODE_ENV=production
- PORT=3001
@ -97,9 +135,11 @@ services:
depends_on:
- mosquitto
# Sample data publisher: sends test assets to MQTT so the map shows UAVs, ships, etc.
# Synthetic asset publisher (UAVs, ships, helos, tanks moving across Europe).
# Useful for both `local` and `dev` profiles when no real MQTT feed is wired up.
test-publisher:
image: node:18-alpine
profiles: [demo]
image: node:20-alpine
container_name: holocron-test-publisher-local
working_dir: /app
volumes:

View file

@ -1,5 +1,5 @@
# Stage 1: Build React application
FROM node:18-alpine AS builder
FROM node:20-alpine AS builder
WORKDIR /app
@ -16,7 +16,7 @@ COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app

View file

@ -1,5 +1,5 @@
# Stage 1: Build React application
FROM node:18-alpine AS builder
FROM node:20-alpine AS builder
WORKDIR /app
@ -16,7 +16,7 @@ COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app