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