This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git


The following commit(s) were added to refs/heads/master by this push:
     new 9403d87682 dev: single-container Docker-in-Docker Tilt environment 
with HTTPS ingress (#622)
9403d87682 is described below

commit 9403d8768290659cdbd103de8bd46a376bc14d93
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Sat Jun 6 00:55:50 2026 -0400

    dev: single-container Docker-in-Docker Tilt environment with HTTPS ingress 
(#622)
    
    Runs the full dev stack (infra + server) inside one host-visible DinD
    container with its own Docker daemon + registry, driven entirely by
    `tilt up`/`tilt down`. Traefik serves *.airavata.localhost over HTTP and
    HTTPS (mkcert). Server connection settings are injected via
    JAVA_TOOL_OPTIONS; /actuator/health is restored (Armeria actuator bridge)
    and InfrastructureHealthIndicator reads broker hosts from config instead
    of hardcoded localhost.
---
 .dockerignore                                      |  16 ++-
 .gitignore                                         |   3 +
 CLAUDE.md                                          |  67 +++++++--
 Tiltfile                                           | 149 +++++++++++++++++----
 airavata-server/pom.xml                            |   6 +
 .../health/InfrastructureHealthIndicator.java      |  27 +++-
 compose.dind.yml                                   |  26 ++++
 compose.yml                                        | 146 ++++++++++++++++++--
 conf/traefik/dynamic/tls.yml                       |  11 ++
 9 files changed, 392 insertions(+), 59 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index 43fb6a53e7..b3542f6780 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,11 @@
-.devcontainer
-.github
-.idea
-.run
-.vscode
\ No newline at end of file
+# The airavata-server image build context is the repo root, but the image only 
needs
+# the prebuilt fat JAR. Whitelist just that (+ the Dockerfile) so the in-DinD 
build
+# does not ship the entire tree. (Only root-context builds use this file; the 
agent /
+# jupyterhub images use their own subdirectory contexts.)
+*
+!Dockerfile
+!airavata-server
+airavata-server/*
+!airavata-server/target
+airavata-server/target/*
+!airavata-server/target/*.jar
diff --git a/.gitignore b/.gitignore
index 80e16aa986..889a16305e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,6 @@ modules/*/*/distribution/
 
 docs/superpowers/
 .worktrees/
+
+# Dev TLS material (mkcert-generated; never commit)
+conf/traefik/certs/
diff --git a/CLAUDE.md b/CLAUDE.md
index ca7f9e47ff..35079a0d17 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -16,20 +16,61 @@ mvn test -pl airavata-api -Dgroups=runtime        # 
Integration tests (needs Doc
 mvn test -pl airavata-api/research-service        # Single module tests
 ```
 
-Health check: `http://localhost:9090/internal/actuator/health`
-API docs: `http://localhost:9090/docs`
+Health check: `http://localhost:9090/actuator/health` (or 
`https://api.airavata.localhost/actuator/health`)
+API docs: `http://localhost:9090/docs` (or 
`https://api.airavata.localhost/docs`)
+
+### Verifying changes
+
+Compile-green is not enough — the running Tilt-managed server must actually 
stay up. After each batch of changes:
+
+```bash
+tilt logs airavata-server 2>&1 | tail -40        # scan for stack traces
+curl http://localhost:9090/actuator/health
+```
+
+Do not run `mvn clean` during an active session: it deletes 
`airavata-server/target/*.jar` while the JVM is running, and lazy classloaders 
(e.g. the MariaDB JDBC ServiceLoader) then fail at runtime with 
`NoSuchFileException`, cascading into HikariCP connection timeouts. Prefer 
incremental `mvn compile`. If a clean was unavoidable, rebuild and force a 
restart: `mvn package -DskipTests -pl airavata-server -am` then `tilt trigger 
airavata-server` (Tilt may watch sources but not the jar artifact).
 
 ### Infrastructure (started by `tilt up`)
 
-| Service | Port | Notes |
-|---------|------|-------|
-| Airavata Server | 9090 | gRPC + REST (Armeria) |
-| MariaDB | 13306 | `airavata` / `123456` |
-| Keycloak | 18080 | `admin` / `admin` |
-| RabbitMQ | 5672, 15672 | `airavata` / `airavata` (admin UI on 15672) |
-| Kafka | 9092 | |
-| ZooKeeper | 2181 | |
-| SFTP | 2222 | `airavata` / `pass` |
+`tilt up` starts a **single** host container, `airavata-dind` 
(Docker-in-Docker), running
+its own Docker daemon + registry. All services below run **inside** it — the 
host's
+`docker ps` shows only `airavata-dind`. `tilt down` removes it and everything 
inside
+(state is ephemeral: every `up` is a fresh DB/Kafka/Keycloak). Each inner 
service is its
+own Tilt resource, driven via `docker exec airavata-dind docker compose …` 
(gated behind a
+`dind-ready` step that waits for the inner daemon). The server image is built 
inside DinD
+and pulled from the in-DinD registry (`localhost:5000`). The DinD layer is 
`compose.dind.yml`;
+the inner stack is `compose.yml`.
+
+One-time host setup for HTTPS: `brew install mkcert && mkcert -install` 
(trusts the local
+CA so browsers accept `https://*.airavata.localhost`). The cert is generated 
under
+`conf/traefik/certs/` (gitignored) and imported into the server's JVM 
truststore.
+
+Reach services by hostname (`*.airavata.localhost` → loopback per RFC 6761; 
HTTP **and
+HTTPS** via the Traefik ingress, no `/etc/hosts` needed):
+
+| Hostname | Service | Creds |
+|----------|---------|-------|
+| `api.airavata.localhost` | Airavata server (gRPC + REST) | — |
+| `auth.airavata.localhost` | Keycloak | `admin` / `admin` |
+| `rabbitmq.airavata.localhost` | RabbitMQ management UI | `airavata` / 
`airavata` |
+| `adminer.airavata.localhost` | Adminer (`--profile tools`) | — |
+
+Also published directly on the host: `9090` (server gRPC/REST), `13306` 
(MariaDB
+`airavata`/`123456`), `5000` (registry), `80`/`443` (Traefik). Internally the 
server reaches
+infra by compose **service name** (`db:3306`, `rabbitmq:5672`, `kafka:9092`,
+`zookeeper:2181`, `keycloak:18080`, `sftp:22`; SFTP creds `airavata`/`pass`).
+
+Gotchas:
+- Connection settings are injected as `-D` system properties via 
`JAVA_TOOL_OPTIONS` in the
+  `airavata-server` service. Airavata's `ApplicationSettings` resolves 
*dotted* keys
+  (`system-property > env-var-with-exact-dotted-key > file`), so 
`SPRING_DATASOURCE_URL`-style
+  env vars do **not** override `kafka.broker.url` / `rabbitmq.broker.url` / 
etc.
+- The Keycloak issuer is `https://auth.airavata.localhost/realms/default` for 
both browser and
+  in-network server (TLS terminates at Traefik, which forwards 
`X-Forwarded-Proto`).
+- `docker` must be on `PATH` for Tilt's shell-outs (colima installs the CLI 
keg-only — run
+  `brew link docker` if `which docker` is empty).
+- Add a hostname: give the service a 
`traefik.http.routers.<name>.rule=Host(...)` label
+  (+ `entrypoints=web,websecure`, `tls=true`) and a network alias on the 
`traefik` service.
 
 ## Module Structure
 
@@ -112,3 +153,7 @@ Launched as `IServer` workers in the same JVM:
 - Mockito for unit tests (`@ExtendWith(MockitoExtension.class)`)
 - TestContainers (MariaDB) for integration tests via `AbstractIntegrationTest` 
base class
 - Integration tests share a singleton MariaDB container across the test suite
+
+## Schema & Code Consolidation
+
+When simplifying or consolidating schemas or code, only merge things that 
naturally belong together — same lifecycle, same owning service, same 
conceptual identity. Do not consolidate by surface-level shape similarity 
(shared column names or a common family suffix): e.g. don't fold a 
workflow-engine `HANDLER_STATUS` into execution-core `EXEC_STATUS` just because 
both carry STATE + TIMESTAMP — they have different lifecycles, services, and 
invariants. Mechanical consolidation yields mostly [...]
diff --git a/Tiltfile b/Tiltfile
index 84a87cec15..c4aeef9027 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -1,52 +1,153 @@
-# Airavata Development Tiltfile
-# Usage: tilt up
+# Airavata Development Tiltfile — single-container (Docker-in-Docker) stack.
+# `tilt up`  boots ONE host container (airavata-dind) that runs its own 
dockerd +
+#            registry; all infra and the Airavata server run inside it.
+# `tilt down` removes that one container and everything inside it (ephemeral).
+
+DIND = 'airavata-dind'
+REPO = '/workspace/airavata'                                   # repo path 
INSIDE DinD
+COMPOSE = 'docker exec %s docker compose -f %s/compose.yml' % (DIND, REPO)
+
+# Friendly hostnames (resolve via *.localhost loopback + Traefik Host routing).
+HOSTNAMES = {
+    'api.airavata.localhost':      'airavata-server',
+    'auth.airavata.localhost':     'keycloak',
+    'rabbitmq.airavata.localhost': 'rabbitmq',
+    'adminer.airavata.localhost':  'adminer',
+}
 
 # --- Generate dev SSH keypair for SFTP container (idempotent) ---
 local(
-    'mkdir -p conf/sftp && '
-    'test -f conf/sftp/id_rsa || '
-    'ssh-keygen -t rsa -b 2048 -f conf/sftp/id_rsa -N "" -q && '
+    'mkdir -p conf/sftp && ' +
+    'test -f conf/sftp/id_rsa || ' +
+    'ssh-keygen -t rsa -b 2048 -f conf/sftp/id_rsa -N "" -q && ' +
     'echo "Generated dev SSH keypair at conf/sftp/id_rsa"',
     quiet=True,
 )
 
-# --- Infrastructure (from compose.yml) ---
-docker_compose('./compose.yml')
+# --- Generate dev TLS cert for *.airavata.localhost via mkcert (idempotent) 
---
+# Requires a one-time `brew install mkcert && mkcert -install` so the local CA 
is
+# trusted by your browser. The cert + CA are written under conf/traefik/certs/
+# (gitignored). The CA is also imported into the server container's JVM 
truststore.
+local(
+    """
+mkdir -p conf/traefik/certs
+if [ ! -f conf/traefik/certs/airavata.localhost.crt ]; then
+  if command -v mkcert >/dev/null 2>&1; then
+    mkcert -cert-file conf/traefik/certs/airavata.localhost.crt -key-file 
conf/traefik/certs/airavata.localhost.key "*.airavata.localhost" 
airavata.localhost localhost 127.0.0.1 ::1
+    cp "$(mkcert -CAROOT)/rootCA.pem" conf/traefik/certs/rootCA.pem
+    echo "Generated dev TLS cert for *.airavata.localhost"
+  else
+    echo "WARN: mkcert not installed -- HTTPS disabled. Run: brew install 
mkcert && mkcert -install"
+  fi
+fi
+""",
+    quiet=True,
+)
+
+# --- The single host container: Docker-in-Docker host (Tilt resource: 
airavata-dind) ---
+docker_compose('./compose.dind.yml')
+dc_resource('airavata-dind', labels=['platform'])   # group under the platform 
bucket (not "unlabeled")
+
+# --- Gate: block until the inner dockerd actually accepts connections ---
+# The dind container can be "running" a beat before its daemon is ready; Tilt 
does not
+# wait on the compose healthcheck for resource_deps, so gate explicitly here. 
Single
+# quotes only (no nested double-quotes / &&) so Tilt's shell parses it cleanly.
+local_resource(
+    'dind-ready',
+    cmd="docker exec %s sh -c 'until docker info >/dev/null 2>&1; do sleep 1; 
done'" % DIND,
+    resource_deps=['airavata-dind'],
+    labels=['platform'],
+)
+
+# readiness probe: an inner container reports docker health == healthy
+def inner_health(container):
+    return probe(
+        exec=exec_action(command=[
+            'sh', '-c',
+            "docker exec %s docker inspect -f '{{.State.Health.Status}}' %s | 
grep -q healthy" % (DIND, container),
+        ]),
+        initial_delay_secs=5, period_secs=5, timeout_secs=10,
+    )
+
+# bring up one inner compose service as its own Tilt resource (logs + 
readiness)
+def inner_service(name, container, deps=[], labels=['infra']):
+    local_resource(
+        name,
+        cmd='%s up -d %s' % (COMPOSE, name),
+        serve_cmd='%s logs -f --no-log-prefix %s' % (COMPOSE, name),
+        readiness_probe=inner_health(container),
+        resource_deps=['dind-ready'] + deps,
+        labels=labels,
+    )
 
-# --- Build ---
+# --- Platform services inside DinD ---
+# registry comes up first and creates the shared `airavata_default` compose 
network;
+# every other inner service depends on it so the parallel `up -d` calls don't 
race on
+# network creation.
+inner_service('registry', 'airavata-registry', labels=['platform'])
+inner_service('traefik',  'airavata-traefik',  deps=['registry'], 
labels=['platform'])
+
+# --- Infrastructure inside DinD ---
+inner_service('db',        'airavata-db',        deps=['registry'])
+inner_service('rabbitmq',  'airavata-rabbitmq',  deps=['registry'])
+inner_service('zookeeper', 'airavata-zookeeper', deps=['registry'])
+inner_service('kafka',     'airavata-kafka',     deps=['registry'])
+inner_service('keycloak',  'airavata-keycloak',  deps=['registry'])
+inner_service('sftp',      'airavata-sftp',      deps=['registry'])
+
+# --- Build the server fat JAR on the host (incremental) ---
+# NOTE: watching the full airavata-api/airavata-server source trees overflows 
macOS
+# fsnotify (mvn writes thousands of files into target/), which thrashes the 
build in a
+# re-trigger loop. Watch a narrow signal instead; use `tilt trigger build` (or 
the
+# Tilt UI) to force a rebuild after source edits. See docs for the 
auto-rebuild loop.
 local_resource(
     'build',
     cmd='mvn install -DskipTests -Dmaven.test.skip=true -T4 -q',
-    deps=[
-        'airavata-server/pom.xml',
-    ],
-    ignore=['**/target/**'],
-    trigger_mode=TRIGGER_MODE_AUTO,
+    deps=['airavata-server/pom.xml'],
     labels=['build'],
 )
 
-# --- Airavata Server (unified: gRPC + REST via Armeria on port 9090) ---
+# --- Server image: build in DinD -> push to registry -> pull -> run ---
 local_resource(
     'airavata-server',
-    serve_cmd='java -jar 
airavata-server/target/airavata-server-0.21-SNAPSHOT.jar',
+    cmd=' && '.join([
+        '%s build airavata-server' % COMPOSE,
+        '%s push airavata-server' % COMPOSE,
+        '%s pull airavata-server' % COMPOSE,
+        '%s up -d --force-recreate airavata-server' % COMPOSE,
+    ]),
+    serve_cmd='%s logs -f --no-log-prefix airavata-server' % COMPOSE,
+    # /actuator/health (Spring Actuator, bridged onto Armeria) reflects DB + 
messaging
+    # reachability — a real readiness signal, not just "Armeria is serving".
     readiness_probe=probe(
-        http_get=http_get_action(port=9090, path='/internal/actuator/health'),
-        initial_delay_secs=30,
-        period_secs=5,
-        timeout_secs=5,
+        http_get=http_get_action(port=9090, path='/actuator/health'),
+        initial_delay_secs=30, period_secs=5, timeout_secs=5,
     ),
-    resource_deps=['build', 'db', 'rabbitmq', 'zookeeper', 'kafka', 
'keycloak'],
+    deps=['airavata-server/target/airavata-server-0.21-SNAPSHOT.jar'],
+    resource_deps=['dind-ready', 'build', 'registry', 'traefik',
+                   'db', 'rabbitmq', 'zookeeper', 'kafka', 'keycloak'],
     links=[
-        link('http://localhost:9090/docs', 'API Docs (Armeria DocService)'),
-        link('http://localhost:9090/internal/actuator/health', 'Health'),
+        link('http://api.airavata.localhost/docs', 'API Docs'),
+        link('http://api.airavata.localhost/internal/actuator/health', 
'Health'),
+        link('http://auth.airavata.localhost', 'Keycloak'),
+        link('http://rabbitmq.airavata.localhost', 'RabbitMQ'),
     ],
     labels=['airavata'],
 )
 
-# --- Integration Tests (manual trigger) ---
+# --- Integration tests (manual; run inside the DinD network) ---
 local_resource(
     'integration-tests',
-    cmd='mvn test -pl airavata-api -Dgroups=runtime 
-Dsurefire.excludedGroups="" -q',
+    cmd=' '.join([
+        'docker exec %s docker run --rm --network airavata_default' % DIND,
+        '-v %s:/work -w /work' % REPO,
+        '-e SPRING_DATASOURCE_URL=jdbc:mariadb://db:3306/airavata',
+        '-e KAFKA_BROKER_URL=kafka:9092',
+        '-e ZOOKEEPER_SERVER_CONNECTION=zookeeper:2181',
+        '-e RABBITMQ_BROKER_URL=amqp://airavata:airavata@rabbitmq:5672',
+        'maven:3.9-eclipse-temurin-17',
+        'mvn test -pl airavata-api -Dgroups=runtime 
-Dsurefire.excludedGroups="" -q',
+    ]),
     resource_deps=['airavata-server'],
     auto_init=False,
     trigger_mode=TRIGGER_MODE_MANUAL,
diff --git a/airavata-server/pom.xml b/airavata-server/pom.xml
index 1fa842ba99..b2f8041924 100644
--- a/airavata-server/pom.xml
+++ b/airavata-server/pom.xml
@@ -108,6 +108,12 @@ under the License.
             <groupId>com.linecorp.armeria</groupId>
             <artifactId>armeria-grpc</artifactId>
         </dependency>
+        <!-- Bridges Spring Boot Actuator endpoints onto the Armeria server so
+             /internal/actuator/health is actually served (version via 
armeria-bom). -->
+        <dependency>
+            <groupId>com.linecorp.armeria</groupId>
+            
<artifactId>armeria-spring-boot3-actuator-autoconfigure</artifactId>
+        </dependency>
 
         <!-- Mapping -->
         <dependency>
diff --git 
a/airavata-server/src/main/java/org/apache/airavata/server/health/InfrastructureHealthIndicator.java
 
b/airavata-server/src/main/java/org/apache/airavata/server/health/InfrastructureHealthIndicator.java
index bafb0cafbe..954676813d 100644
--- 
a/airavata-server/src/main/java/org/apache/airavata/server/health/InfrastructureHealthIndicator.java
+++ 
b/airavata-server/src/main/java/org/apache/airavata/server/health/InfrastructureHealthIndicator.java
@@ -21,6 +21,7 @@ package org.apache.airavata.server.health;
 
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.URI;
 import org.apache.airavata.config.ServerSettings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -43,21 +44,33 @@ public class InfrastructureHealthIndicator implements 
HealthIndicator {
         var builder = Health.up();
         boolean allHealthy = true;
 
-        allHealthy &= checkTcp(builder, "rabbitmq", "localhost", 5672);
-        allHealthy &= checkTcp(builder, "zookeeper", "localhost", 2181);
-
+        // RabbitMQ host/port parsed from rabbitmq.broker.url 
(amqp://user:pass@host:port).
         try {
-            String kafkaUrl = ServerSettings.getSetting("kafka.broker.url", 
"localhost:9092");
-            String[] parts = kafkaUrl.split(":");
-            allHealthy &= checkTcp(builder, "kafka", parts[0], 
Integer.parseInt(parts[1]));
+            URI amqp = 
URI.create(ServerSettings.getSetting("rabbitmq.broker.url", 
"amqp://localhost:5672"));
+            int port = amqp.getPort() > 0 ? amqp.getPort() : 5672;
+            allHealthy &= checkTcp(builder, "rabbitmq", amqp.getHost(), port);
         } catch (Exception e) {
-            builder.withDetail("kafka", "config error: " + e.getMessage());
+            builder.withDetail("rabbitmq", "config error: " + e.getMessage());
             allHealthy = false;
         }
 
+        allHealthy &= checkHostPort(builder, "zookeeper", 
"zookeeper.server.connection", "localhost:2181");
+        allHealthy &= checkHostPort(builder, "kafka", "kafka.broker.url", 
"localhost:9092");
+
         return allHealthy ? builder.build() : builder.down().build();
     }
 
+    /** Reads a "host:port" setting and checks TCP connectivity. */
+    private boolean checkHostPort(Health.Builder builder, String name, String 
settingKey, String defaultValue) {
+        try {
+            String[] parts = ServerSettings.getSetting(settingKey, 
defaultValue).split(":");
+            return checkTcp(builder, name, parts[0], 
Integer.parseInt(parts[1]));
+        } catch (Exception e) {
+            builder.withDetail(name, "config error: " + e.getMessage());
+            return false;
+        }
+    }
+
     private boolean checkTcp(Health.Builder builder, String name, String host, 
int port) {
         try (Socket socket = new Socket()) {
             socket.connect(new InetSocketAddress(host, port), 
CONNECT_TIMEOUT_MS);
diff --git a/compose.dind.yml b/compose.dind.yml
new file mode 100644
index 0000000000..45897aa293
--- /dev/null
+++ b/compose.dind.yml
@@ -0,0 +1,26 @@
+# compose.dind.yml
+# The single host-visible container: a Docker-in-Docker host for the entire 
Airavata
+# dev stack. Tilt manages ONLY this container; `tilt down` removes it and 
everything
+# running inside it. `docker ps` on the host shows just `airavata-dind`.
+services:
+  airavata-dind:
+    image: docker:28-dind
+    container_name: airavata-dind
+    privileged: true
+    environment:
+      DOCKER_TLS_CERTDIR: ""          # plain local daemon; driven via `docker 
exec`
+    volumes:
+      - .:/workspace/airavata          # host repo -> DinD (inner compose 
working dir)
+    working_dir: /workspace/airavata
+    ports:
+      - "80:80"        # Traefik ingress (HTTP, *.airavata.localhost)
+      - "443:443"      # Traefik ingress (HTTPS)
+      - "5000:5000"    # in-DinD registry
+      - "9090:9090"    # airavata-server (gRPC + REST, direct)
+      - "13306:13306"  # MariaDB (host DB tools)
+    healthcheck:
+      test: ["CMD", "docker", "info"]
+      interval: 5s
+      timeout: 5s
+      retries: 30
+      start_period: 10s
diff --git a/compose.yml b/compose.yml
index 9123c0ab33..769acc9ce6 100644
--- a/compose.yml
+++ b/compose.yml
@@ -1,5 +1,53 @@
 services:
 
+  # --- In-DinD image registry (server image, registry-in-the-loop) ---
+  registry:
+    image: registry:2
+    container_name: airavata-registry
+    restart: unless-stopped
+    ports:
+      - "5000:5000"
+    healthcheck:
+      test: ["CMD-SHELL", "wget -qO- http://localhost:5000/v2/ >/dev/null 2>&1 
|| exit 1"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 5s
+
+  # --- Ingress: routes *.airavata.localhost by Host header ---
+  traefik:
+    image: traefik:v3.3
+    container_name: airavata-traefik
+    restart: unless-stopped
+    command:
+      - --providers.docker=true
+      - --providers.docker.exposedByDefault=false
+      - --providers.file.directory=/etc/traefik/dynamic
+      - --providers.file.watch=true
+      - --entrypoints.web.address=:80
+      - --entrypoints.websecure.address=:443
+      - --ping=true
+    ports:
+      - "80:80"
+      - "443:443"
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock:ro
+      - ./conf/traefik/certs:/etc/traefik/certs:ro
+      - ./conf/traefik/dynamic:/etc/traefik/dynamic:ro
+    networks:
+      default:
+        aliases:
+          - api.airavata.localhost
+          - auth.airavata.localhost
+          - rabbitmq.airavata.localhost
+          - adminer.airavata.localhost
+    healthcheck:
+      test: ["CMD", "traefik", "healthcheck", "--ping"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 5s
+
   db:
     image: mariadb:11.8
     container_name: airavata-db
@@ -31,15 +79,18 @@ services:
       RABBITMQ_DEFAULT_PASS: airavata
     volumes:
       - rabbitmq_data:/var/lib/rabbitmq
-    ports:
-      - "5672:5672"
-      - "15672:15672"
     healthcheck:
       test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
       interval: 10s
       timeout: 5s
       retries: 5
       start_period: 20s
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.rabbitmq.rule=Host(`rabbitmq.airavata.localhost`)
+      - traefik.http.routers.rabbitmq.entrypoints=web,websecure
+      - traefik.http.routers.rabbitmq.tls=true
+      - traefik.http.services.rabbitmq.loadbalancer.server.port=15672
 
   zookeeper:
     image: zookeeper:3.9
@@ -48,8 +99,6 @@ services:
     volumes:
       - zk_data:/data
       - zk_logs:/datalog
-    ports:
-      - "2181:2181"
     healthcheck:
       test: ["CMD-SHELL", "curl -sf http://localhost:8080/commands/ruok || 
exit 1"]
       interval: 10s
@@ -65,7 +114,7 @@ services:
       KAFKA_NODE_ID: 1
       KAFKA_PROCESS_ROLES: broker,controller
       KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
-      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
+      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
       KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 
CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
       KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
@@ -73,8 +122,6 @@ services:
       CLUSTER_ID: airavata-kafka-cluster-id-01
     volumes:
       - kafka_data:/var/lib/kafka/data
-    ports:
-      - "9092:9092"
     healthcheck:
       test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh 
--bootstrap-server localhost:9092 > /dev/null 2>&1"]
       interval: 10s
@@ -103,14 +150,18 @@ services:
       - --metrics-enabled=true
       - --log-level=INFO
       - --spi-theme-default=airavata
-    ports:
-      - "18080:18080"
     healthcheck:
       test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET 
/health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' 
>&3 && cat <&3 | grep -q '200 OK'"]
       interval: 10s
       timeout: 5s
       retries: 10
       start_period: 60s
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.auth.rule=Host(`auth.airavata.localhost`)
+      - traefik.http.routers.auth.entrypoints=web,websecure
+      - traefik.http.routers.auth.tls=true
+      - traefik.http.services.auth.loadbalancer.server.port=18080
 
   sftp:
     image: atmoz/sftp:latest
@@ -119,8 +170,6 @@ services:
     volumes:
       - ./conf/sftp/id_rsa.pub:/home/airavata/.ssh/keys/id_rsa.pub:ro
       - sftp_data:/home/airavata/storage
-    ports:
-      - "2222:22"
     command: "airavata::1000"
     healthcheck:
       test: ["CMD-SHELL", "cat /proc/1/cmdline | tr '\\0' ' ' | grep -q sshd 
|| exit 1"]
@@ -129,6 +178,73 @@ services:
       retries: 5
       start_period: 10s
 
+  # --- Airavata server (built on host -> image in DinD -> pulled from 
registry) ---
+  airavata-server:
+    image: localhost:5000/airavata-server:dev
+    build:
+      context: .
+      dockerfile: Dockerfile
+    container_name: airavata-server
+    restart: unless-stopped
+    # Connection settings are read by Airavata's ApplicationSettings 
(resolution order:
+    # system-property > env-var-with-exact-dotted-key > properties file) and 
by Spring
+    # @Value. SCREAMING_SNAKE env vars do NOT override the dotted keys, so 
inject -D
+    # system properties via JAVA_TOOL_OPTIONS (auto-read by the JVM, honored 
by both).
+    environment:
+      JAVA_TOOL_OPTIONS: >-
+        -Dspring.datasource.url=jdbc:mariadb://db:3306/airavata
+        -Dkafka.broker.url=kafka:9092
+        -Dairavata.kafka.broker-url=kafka:9092
+        -Drabbitmq.broker.url=amqp://airavata:airavata@rabbitmq:5672
+        -Dspring.rabbitmq.host=rabbitmq
+        -Dspring.rabbitmq.port=5672
+        -Dspring.rabbitmq.username=airavata
+        -Dspring.rabbitmq.password=airavata
+        -Dzookeeper.server.connection=zookeeper:2181
+        
-Dairavata.security.openid-url=https://auth.airavata.localhost/realms/default
+        -Diam.server.url=https://auth.airavata.localhost
+        -Djavax.net.ssl.trustStore=/tmp/airavata-truststore.p12
+        -Djavax.net.ssl.trustStorePassword=changeit
+    # The Keycloak issuer is now https (TLS terminated at Traefik). Trust the 
mkcert dev
+    # CA by importing it into a writable copy of the JVM truststore before 
launching, so
+    # the server can fetch JWKS over https. The Dockerfile's JAVA_HOME is 
unreliable, so
+    # detect the real java home from `command -v java`. `$$` escapes compose 
substitution.
+    entrypoint: ["/bin/sh", "-c"]
+    command:
+      - >-
+        JH=$$(dirname $$(dirname $$(readlink -f $$(command -v java))));
+        cp "$$JH/lib/security/cacerts" /tmp/airavata-truststore.p12;
+        keytool -importcert -noprompt -alias airavata-dev-ca -file 
/certs/rootCA.pem
+        -keystore /tmp/airavata-truststore.p12 -storepass changeit
+        || echo "WARN: mkcert CA import failed; https to keycloak may not 
validate";
+        exec java -jar airavata-server.jar
+    volumes:
+      - ./conf/traefik/certs/rootCA.pem:/certs/rootCA.pem:ro
+    ports:
+      - "9090:9090"
+    depends_on:
+      db: { condition: service_healthy }
+      rabbitmq: { condition: service_healthy }
+      zookeeper: { condition: service_healthy }
+      kafka: { condition: service_healthy }
+      keycloak: { condition: service_healthy }
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.api.rule=Host(`api.airavata.localhost`)
+      - traefik.http.routers.api.entrypoints=web,websecure
+      - traefik.http.routers.api.tls=true
+      - traefik.http.services.api.loadbalancer.server.port=9090
+      - traefik.http.services.api.loadbalancer.server.scheme=h2c
+    # Spring Actuator is bridged onto Armeria 
(armeria-spring-boot3-actuator-autoconfigure),
+    # so /actuator/health reflects DB + messaging health (200 UP only when db, 
rabbitmq,
+    # kafka, and zookeeper are all reachable; 503 otherwise).
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:9090/actuator/health";]
+      interval: 15s
+      timeout: 10s
+      retries: 10
+      start_period: 40s
+
   # Dev tools (start with: docker compose --profile tools up -d)
 
   adminer:
@@ -141,6 +257,12 @@ services:
     depends_on:
       db:
         condition: service_healthy
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.adminer.rule=Host(`adminer.airavata.localhost`)
+      - traefik.http.routers.adminer.entrypoints=web,websecure
+      - traefik.http.routers.adminer.tls=true
+      - traefik.http.services.adminer.loadbalancer.server.port=8080
 
 volumes:
   db_data:
diff --git a/conf/traefik/dynamic/tls.yml b/conf/traefik/dynamic/tls.yml
new file mode 100644
index 0000000000..1787b8365f
--- /dev/null
+++ b/conf/traefik/dynamic/tls.yml
@@ -0,0 +1,11 @@
+# Traefik dynamic config (file provider): serve the mkcert wildcard cert for
+# *.airavata.localhost as the default certificate on the websecure (:443) 
entrypoint.
+tls:
+  stores:
+    default:
+      defaultCertificate:
+        certFile: /etc/traefik/certs/airavata.localhost.crt
+        keyFile: /etc/traefik/certs/airavata.localhost.key
+  certificates:
+    - certFile: /etc/traefik/certs/airavata.localhost.crt
+      keyFile: /etc/traefik/certs/airavata.localhost.key

Reply via email to