From 6981dcb8958c8b3761c0a78a87c22b3ff448b6a5 Mon Sep 17 00:00:00 2001 From: Doc Date: Tue, 23 Jun 2026 14:23:22 +0530 Subject: [PATCH 1/4] Revised Dockerfile based on new configs --- .gitignore | 14 +++++++++++- Dockerfile | 65 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index a5c4f7bb..d67cc658 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,16 @@ jmdn.yaml internal/WAL/.tmp/* .claude/* .code-review-graph/* -.cursor/* \ No newline at end of file +.cursor/* +docs/FASTSYNC_V3_MIGRATION_PLAN.md +eventlog.duckdb +Scripts/sign_tx.go +storage/thebe-kv/00001.mem +storage/thebe-kv/000016.vlog +storage/thebe-kv/000017.vlog +storage/thebe-kv/DISCARD +storage/thebe-kv/KEYREGISTRY +storage/thebe-kv/LOCK +storage/thebe-kv/MANIFEST +storage/thebe-kv/outbox.db +dlq/DLQ.log diff --git a/Dockerfile b/Dockerfile index 7eed9af9..5903e50b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,14 @@ # JMDN (Jupiter MetaZK Decentralized Network) - Multi-stage Dockerfile # ============================================================================= # Build: docker build -t jmdn:latest . -# Run: docker run -d --name jmdn -p 8080:8080 -p 4001:4001 jmdn:latest -# Config: docker run -d -v /path/to/config.env:/etc/jmdn/config.env jmdn:latest +# Run: docker run -d --name jmdn -p 6090:6090 -p 6545:6545 jmdn:latest +# Config: docker run -d -v /path/to/jmdn.yaml:/etc/jmdn/jmdn.yaml jmdn:latest # ============================================================================= # ----------------------------------------------------------------------------- # Stage 1: Build # ----------------------------------------------------------------------------- -FROM golang:1.25-bookworm AS builder +FROM golang:1.25.3-bookworm AS builder # Install build dependencies (CGO_ENABLED=1 requires gcc) RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -51,11 +51,20 @@ RUN GIT_COMMIT=${GIT_COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo "u # ----------------------------------------------------------------------------- FROM debian:bookworm-slim -# Install runtime dependencies +# Install runtime dependencies + Yggdrasil (required: network.yggdrasil: true in jmdn.yaml) +ARG YGGDRASIL_VERSION=0.5.12 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ + gnupg \ libc6 \ + && mkdir -p /usr/local/apt-keys \ + && curl -fsSL https://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/key.txt \ + | gpg --dearmor --yes -o /usr/local/apt-keys/yggdrasil-keyring.gpg \ + && echo 'deb [signed-by=/usr/local/apt-keys/yggdrasil-keyring.gpg] http://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/ debian yggdrasil' \ + > /etc/apt/sources.list.d/yggdrasil.list \ + && apt-get update && apt-get install -y --no-install-recommends \ + yggdrasil=${YGGDRASIL_VERSION} \ && rm -rf /var/lib/apt/lists/* # Install ImmuDB @@ -69,32 +78,44 @@ RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \ # Create non-root user RUN groupadd -r jmdn && useradd -r -g jmdn -d /home/jmdn -s /bin/bash -m jmdn -# Create required directories -RUN mkdir -p /etc/jmdn /opt/jmdn/data /var/log/jmdn && \ - chown -R jmdn:jmdn /opt/jmdn /var/log/jmdn /etc/jmdn +# Create required directories (mirrors install_services.sh layout) +RUN mkdir -p \ + /etc/jmdn/certs \ + /opt/jmdn/data/data \ + /opt/jmdn/data/config \ + /opt/jmdn/data/DB \ + /var/log/jmdn \ + && chown -R jmdn:jmdn /opt/jmdn /var/log/jmdn /etc/jmdn -# Copy binary and default config from builder +# Copy binary, wrapper, and default config from builder COPY --from=builder /src/jmdn /usr/local/bin/jmdn -COPY --from=builder /src/jmdn_default.yaml /etc/jmdn/jmdn_default.yaml +COPY --from=builder /src/Scripts/start_jmdn_wrapper.sh /usr/local/bin/start_jmdn_wrapper.sh +RUN chmod +x /usr/local/bin/start_jmdn_wrapper.sh +# Copy default config as jmdn.yaml (mirrors setup_config.sh: cp jmdn_default.yaml → jmdn.yaml) +COPY --from=builder /src/jmdn_default.yaml /etc/jmdn/jmdn.yaml COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json -# Expose common ports -# 8080 - HTTP API / Explorer -# 4001 - P2P / libp2p -# 3323 - ImmuDB -# 50051 - gRPC -EXPOSE 8080 4001 3323 50051 - -# Health check +# Expose ports per jmdn.yaml (localhost-bound ports excluded) +# 6090 - HTTP API / Explorer (ports.api) +# 16050 - Block generation (ports.blockgen) +# 16055 - Block propagation gRPC (ports.blockgrpc) +# 16052 - DID service (ports.did) +# 6545 - Facade / JSON-RPC (ports.facade) +# 6546 - WebSocket (ports.ws) +# 3323 - ImmuDB +EXPOSE 6090 16050 16055 16052 6545 6546 3323 + +# Health check against actual API port (ports.api: 6090) HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:8080/health || exit 1 + CMD curl -f http://localhost:6090/health || exit 1 -# Data volume for ImmuDB persistence +# Data volume for ImmuDB + node persistence VOLUME ["/opt/jmdn/data"] USER jmdn WORKDIR /home/jmdn -# Default entrypoint - override config with -v /your/config.env:/etc/jmdn/config.env -ENTRYPOINT ["jmdn"] -CMD ["-config", "/etc/jmdn/config.env"] +# Wrapper handles binary path resolution; override config with: +# -v /your/jmdn.yaml:/etc/jmdn/jmdn.yaml +ENTRYPOINT ["/usr/local/bin/start_jmdn_wrapper.sh"] +CMD ["-config", "/etc/jmdn/jmdn.yaml"] From afe2505b9c6feb930bf0a8bf30bcca30f4dedeb7 Mon Sep 17 00:00:00 2001 From: Doc Date: Tue, 23 Jun 2026 15:42:54 +0530 Subject: [PATCH 2/4] Update Dockerfile and .gitignore for improved build and dependency management - Refactored Dockerfile to streamline installation of Yggdrasil and other dependencies, removing hardcoded versioning and enhancing key management. - Updated entrypoint scripts for better initialization and execution flow. - Modified .gitignore to simplify ignored patterns, now excluding all files in the storage directory and log files, improving repository cleanliness. --- .gitignore | 11 +---- Dockerfile | 41 ++++++++++------ Scripts/bootstrap_sync.sh | 90 ++++++++++++++++++++++++++++++++++++ Scripts/docker-entrypoint.sh | 47 +++++++++++++++++++ 4 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 Scripts/bootstrap_sync.sh create mode 100644 Scripts/docker-entrypoint.sh diff --git a/.gitignore b/.gitignore index d67cc658..dab5c97e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,12 +55,5 @@ internal/WAL/.tmp/* docs/FASTSYNC_V3_MIGRATION_PLAN.md eventlog.duckdb Scripts/sign_tx.go -storage/thebe-kv/00001.mem -storage/thebe-kv/000016.vlog -storage/thebe-kv/000017.vlog -storage/thebe-kv/DISCARD -storage/thebe-kv/KEYREGISTRY -storage/thebe-kv/LOCK -storage/thebe-kv/MANIFEST -storage/thebe-kv/outbox.db -dlq/DLQ.log +storage/* +*.log diff --git a/Dockerfile b/Dockerfile index 5903e50b..29c0938d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,19 +52,22 @@ RUN GIT_COMMIT=${GIT_COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo "u FROM debian:bookworm-slim # Install runtime dependencies + Yggdrasil (required: network.yggdrasil: true in jmdn.yaml) -ARG YGGDRASIL_VERSION=0.5.12 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ gnupg \ libc6 \ && mkdir -p /usr/local/apt-keys \ - && curl -fsSL https://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/key.txt \ - | gpg --dearmor --yes -o /usr/local/apt-keys/yggdrasil-keyring.gpg \ + && gpg --fetch-keys https://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/key.txt \ + && gpg --export 1C5162E133015D81A811239D1840CDAC6011C5EA \ + | tee /usr/local/apt-keys/yggdrasil-keyring.gpg > /dev/null \ && echo 'deb [signed-by=/usr/local/apt-keys/yggdrasil-keyring.gpg] http://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/ debian yggdrasil' \ > /etc/apt/sources.list.d/yggdrasil.list \ && apt-get update && apt-get install -y --no-install-recommends \ - yggdrasil=${YGGDRASIL_VERSION} \ + yggdrasil \ + netcat-openbsd \ + wget \ + bzip2 \ && rm -rf /var/lib/apt/lists/* # Install ImmuDB @@ -87,13 +90,19 @@ RUN mkdir -p \ /var/log/jmdn \ && chown -R jmdn:jmdn /opt/jmdn /var/log/jmdn /etc/jmdn -# Copy binary, wrapper, and default config from builder +# Copy binary, scripts, and default config from builder COPY --from=builder /src/jmdn /usr/local/bin/jmdn COPY --from=builder /src/Scripts/start_jmdn_wrapper.sh /usr/local/bin/start_jmdn_wrapper.sh -RUN chmod +x /usr/local/bin/start_jmdn_wrapper.sh +COPY --from=builder /src/Scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY --from=builder /src/Scripts/bootstrap_sync.sh /usr/local/bin/bootstrap_sync.sh +RUN chmod +x /usr/local/bin/start_jmdn_wrapper.sh \ + /usr/local/bin/docker-entrypoint.sh \ + /usr/local/bin/bootstrap_sync.sh # Copy default config as jmdn.yaml (mirrors setup_config.sh: cp jmdn_default.yaml → jmdn.yaml) COPY --from=builder /src/jmdn_default.yaml /etc/jmdn/jmdn.yaml -COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json +# peer.json must be at ./config/peer.json relative to WORKDIR (hardcoded in config/constants.go) +# WORKDIR is /opt/jmdn/data (volume) so it persists across restarts +COPY --from=builder /src/config/peer.json /opt/jmdn/data/config/peer.json # Expose ports per jmdn.yaml (localhost-bound ports excluded) # 6090 - HTTP API / Explorer (ports.api) @@ -102,8 +111,8 @@ COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json # 16052 - DID service (ports.did) # 6545 - Facade / JSON-RPC (ports.facade) # 6546 - WebSocket (ports.ws) -# 3323 - ImmuDB -EXPOSE 6090 16050 16055 16052 6545 6546 3323 +# ImmuDB (3322) is container-internal — not exposed +EXPOSE 6090 16050 16055 16052 6545 6546 # Health check against actual API port (ports.api: 6090) HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ @@ -112,10 +121,12 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ # Data volume for ImmuDB + node persistence VOLUME ["/opt/jmdn/data"] -USER jmdn -WORKDIR /home/jmdn +# Run as root — required for bootstrap_sync.sh (chown after snapshot extract) +# WORKDIR matches where jmdn resolves ./config/peer.json (config/constants.go: PeerFile = "./config/peer.json") +WORKDIR /opt/jmdn/data -# Wrapper handles binary path resolution; override config with: -# -v /your/jmdn.yaml:/etc/jmdn/jmdn.yaml -ENTRYPOINT ["/usr/local/bin/start_jmdn_wrapper.sh"] -CMD ["-config", "/etc/jmdn/jmdn.yaml"] +# 1. bootstrap_sync.sh (first run only — downloads snapshot, writes sentinel) +# 2. immudb (starts in background) +# 3. start_jmdn_wrapper.sh → jmdn +# Override config: -v /your/jmdn.yaml:/etc/jmdn/jmdn.yaml +CMD ["/usr/local/bin/docker-entrypoint.sh"] \ No newline at end of file diff --git a/Scripts/bootstrap_sync.sh b/Scripts/bootstrap_sync.sh new file mode 100644 index 00000000..f325aca6 --- /dev/null +++ b/Scripts/bootstrap_sync.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# bootstrap_sync.sh - First-run snapshot sync for JMDN Docker container +# +# Downloads and extracts the latest data snapshot before the node starts. +# Runs ONLY on the first container start — guarded by a sentinel file on the +# volume (/opt/jmdn/data/.bootstrapped). Subsequent restarts skip this entirely. +# +# Override the snapshot URL via env var: +# -e BOOTSTRAP_TAR_URL=https://your-bucket/snapshot.tar.bz2 +# +# To force a re-sync (e.g. after wiping the volume): +# docker exec jmdn rm /opt/jmdn/data/.bootstrapped && docker restart jmdn + +set -eo pipefail + +SENTINEL="/opt/jmdn/data/.bootstrapped" +DEFAULT_TAR_URL="https://storage.googleapis.com/jmzk-releases/JMZK-Decentralised-Network/jmdn_data_20260604_145256.tar.bz2" +TAR_URL="${BOOTSTRAP_TAR_URL:-$DEFAULT_TAR_URL}" +TAR_FILE="/tmp/$(basename "$TAR_URL")" + +log() { echo "[bootstrap] $*"; } +die() { echo "[bootstrap] ERROR: $*" >&2; exit 1; } + +# ── Guard: skip if already bootstrapped ────────────────── +if [ -f "$SENTINEL" ]; then + log "Sentinel found at $SENTINEL — skipping bootstrap." + exit 0 +fi + +log "First run detected — starting bootstrap sync." +log "Snapshot URL: $TAR_URL" + +# ── Download ───────────────────────────────────────────── +log "Downloading snapshot..." +wget -q --show-progress -O "$TAR_FILE" "$TAR_URL" + +# ── Checksum verification ───────────────────────────────── +# Expects a checksums.md5 file in the same bucket directory as the snapshot. +# Format matches the original: md5sum data-patched.part* > checksums_local.md5 +# diff checksums.md5 checksums_local.md5 +SNAPSHOT_DIR="${TAR_URL%/*}" +CHECKSUM_URL="${SNAPSHOT_DIR}/checksums.md5" +CHECKSUM_REMOTE="/tmp/checksums_remote.md5" +CHECKSUM_LOCAL="/tmp/checksums_local.md5" + +log "Downloading remote checksum from: $CHECKSUM_URL" +if wget -q -O "$CHECKSUM_REMOTE" "$CHECKSUM_URL"; then + log "Computing local checksum..." + md5sum "$TAR_FILE" | awk -v fname="$(basename "$TAR_FILE")" '{print $1 " " fname}' > "$CHECKSUM_LOCAL" + + # Normalise: compare only filenames present in the local file + if diff \ + <(grep "$(basename "$TAR_FILE")" "$CHECKSUM_REMOTE" | awk '{print $1}') \ + <(awk '{print $1}' "$CHECKSUM_LOCAL") > /dev/null 2>&1; then + log "Checksum OK." + else + rm -f "$TAR_FILE" "$CHECKSUM_REMOTE" "$CHECKSUM_LOCAL" + die "Checksum mismatch — aborting bootstrap to prevent corrupt data." + fi + rm -f "$CHECKSUM_REMOTE" "$CHECKSUM_LOCAL" +else + log "WARNING: No checksums.md5 found at remote — skipping verification." +fi + +# ── Wipe existing data (mirrors original script) ───────── +# /opt/jmdn/data is a Docker volume mount point — cannot rm the dir itself, +# only its contents. +log "Cleaning /opt/jmdn/data contents..." +find /opt/jmdn/data -mindepth 1 -delete + +log "Clearing immudb identity and state files..." +rm -rf /opt/jmdn/.immudb_state/.identity-* 2>/dev/null || true +rm -rf /opt/jmdn/.immudb_state/.state-* 2>/dev/null || true + +# ── Extract ────────────────────────────────────────────── +log "Extracting snapshot to /..." +tar -xjf "$TAR_FILE" -C / + +# ── Permissions ────────────────────────────────────────── +log "Fixing permissions on /opt/jmdn/data..." +chown -R root:root /opt/jmdn/data + +# ── Cleanup ────────────────────────────────────────────── +log "Removing downloaded tar..." +rm -f "$TAR_FILE" + +# ── Write sentinel ─────────────────────────────────────── +# Sentinel lives on the volume → persists across container restarts +touch "$SENTINEL" +log "Bootstrap complete. Sentinel written → $SENTINEL" diff --git a/Scripts/docker-entrypoint.sh b/Scripts/docker-entrypoint.sh new file mode 100644 index 00000000..a63ae0d0 --- /dev/null +++ b/Scripts/docker-entrypoint.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# docker-entrypoint.sh - Container startup orchestrator for JMDN +# +# Order of operations: +# 1. Bootstrap sync (first run only — downloads + extracts snapshot) +# 2. ImmuDB (starts in background, waits for port 3322) +# 3. jmdn (exec via start_jmdn_wrapper.sh) + +set -eo pipefail + +IMMUDB_PORT="${IMMUDB_PORT:-3322}" +IMMUDB_DIR="${IMMUDB_DIR:-/opt/jmdn/data}" +IMMUDB_READY_TIMEOUT="${IMMUDB_READY_TIMEOUT:-30}" + +log() { echo "[entrypoint] $*"; } + +# ── Step 1: Bootstrap sync (first run only) ────────────── +/usr/local/bin/bootstrap_sync.sh + +# ── Step 2: Start ImmuDB ───────────────────────────────── +log "Starting ImmuDB (dir: ${IMMUDB_DIR})..." +immudb --dir "${IMMUDB_DIR}" & +IMMUDB_PID=$! + +log "Waiting for ImmuDB on port ${IMMUDB_PORT}..." +elapsed=0 +until nc -z 127.0.0.1 "${IMMUDB_PORT}" 2>/dev/null; do + if [ "${elapsed}" -ge "${IMMUDB_READY_TIMEOUT}" ]; then + log "ERROR: ImmuDB did not start within ${IMMUDB_READY_TIMEOUT}s" + kill "${IMMUDB_PID}" 2>/dev/null || true + exit 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +log "ImmuDB ready (${elapsed}s)" + +# Forward SIGTERM/SIGINT to ImmuDB on shutdown +_shutdown() { + log "Shutting down..." + kill "${IMMUDB_PID}" 2>/dev/null || true +} +trap _shutdown TERM INT + +# ── Step 3: Start JMDN ─────────────────────────────────── +log "Starting JMDN..." +exec /usr/local/bin/start_jmdn_wrapper.sh "$@" From 0a6a8a3878bcf04f263e03b6eb67229a0d4e17eb Mon Sep 17 00:00:00 2001 From: Doc Date: Tue, 23 Jun 2026 16:09:27 +0530 Subject: [PATCH 3/4] Fix dockerfile --- Dockerfile | 6 ++++- Scripts/docker-entrypoint.sh | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29c0938d..e82c07c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ netcat-openbsd \ wget \ bzip2 \ + openssl \ && rm -rf /var/lib/apt/lists/* # Install ImmuDB @@ -101,8 +102,11 @@ RUN chmod +x /usr/local/bin/start_jmdn_wrapper.sh \ # Copy default config as jmdn.yaml (mirrors setup_config.sh: cp jmdn_default.yaml → jmdn.yaml) COPY --from=builder /src/jmdn_default.yaml /etc/jmdn/jmdn.yaml # peer.json must be at ./config/peer.json relative to WORKDIR (hardcoded in config/constants.go) -# WORKDIR is /opt/jmdn/data (volume) so it persists across restarts +# WORKDIR is /opt/jmdn/data (volume) so it persists across restarts. +# Also kept at /etc/jmdn/peer.json as a fallback — bootstrap_sync wipes the volume, +# so the entrypoint restores from /etc/jmdn/peer.json if missing after bootstrap. COPY --from=builder /src/config/peer.json /opt/jmdn/data/config/peer.json +COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json # Expose ports per jmdn.yaml (localhost-bound ports excluded) # 6090 - HTTP API / Explorer (ports.api) diff --git a/Scripts/docker-entrypoint.sh b/Scripts/docker-entrypoint.sh index a63ae0d0..4f738cdf 100644 --- a/Scripts/docker-entrypoint.sh +++ b/Scripts/docker-entrypoint.sh @@ -17,6 +17,51 @@ log() { echo "[entrypoint] $*"; } # ── Step 1: Bootstrap sync (first run only) ────────────── /usr/local/bin/bootstrap_sync.sh +# ── Restore / create required paths after bootstrap ────── +# Bootstrap wipes the volume — restore anything that must exist before jmdn starts. + +# peer.json (hardcoded in config/constants.go: PeerFile = "./config/peer.json", +# resolved relative to WORKDIR /opt/jmdn/data) +if [ ! -f /opt/jmdn/data/config/peer.json ]; then + log "peer.json missing — restoring from /etc/jmdn/peer.json" + mkdir -p /opt/jmdn/data/config + cp /etc/jmdn/peer.json /opt/jmdn/data/config/peer.json +fi + +# Required directories (jmdn.yaml: cdc.dlq_path, thebe.kv_path) +mkdir -p \ + /opt/jmdn/data/certs \ + /opt/jmdn/data/dlq \ + /opt/jmdn/data/storage/thebe-kv + +# TLS certs (security.cert_dir: "certs", resolved relative to WORKDIR) +# Mirrors Scripts/setup_certs.sh — generates a local CA + per-service certs. +# For production, mount real certs via -v /your/certs:/opt/jmdn/data/certs +if [ ! -f /opt/jmdn/data/certs/ca.crt ]; then + log "Generating self-signed TLS certs in /opt/jmdn/data/certs..." + CERT_DIR=/opt/jmdn/data/certs + + # CA + openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ + -keyout "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.crt" \ + -subj "/C=US/O=JMDN/CN=JMDN Dev Root CA" 2>/dev/null + + # Per-service certs (matches setup_certs.sh SERVICES list) + for SVC in cli_admin block_ingest_grpc block_ingest_http did_service \ + explorer_api mempool_service admin_client explorer_client; do + openssl genrsa -out "$CERT_DIR/$SVC.key" 2048 2>/dev/null + openssl req -new -key "$CERT_DIR/$SVC.key" \ + -out "$CERT_DIR/$SVC.csr" \ + -subj "/C=US/O=JMDN/CN=$SVC" 2>/dev/null + openssl x509 -req -in "$CERT_DIR/$SVC.csr" \ + -CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" -CAcreateserial \ + -out "$CERT_DIR/$SVC.crt" -days 365 -sha256 \ + -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1") 2>/dev/null + rm -f "$CERT_DIR/$SVC.csr" + done + log "TLS certs generated." +fi + # ── Step 2: Start ImmuDB ───────────────────────────────── log "Starting ImmuDB (dir: ${IMMUDB_DIR})..." immudb --dir "${IMMUDB_DIR}" & From eb73f2408f148e9f1f8840fff7fba295b8e1d314 Mon Sep 17 00:00:00 2001 From: Doc Date: Wed, 24 Jun 2026 14:45:36 +0530 Subject: [PATCH 4/4] Update Dockerfile and .gitignore for improved dependency management and build cleanliness - Enhanced Dockerfile by adding essential packages (netcat-openbsd, wget, openssl, python3, gawk) for better functionality. - Refactored entrypoint script execution order for improved startup reliability. - Updated .gitignore to exclude dead letter queue files and log files, enhancing repository cleanliness. --- .gitignore | 2 + Dockerfile | 53 +++++---- Scripts/bootstrap_sync.sh | 205 +++++++++++++++++++++++------------ Scripts/docker-entrypoint.sh | 93 ++++++++-------- 4 files changed, 212 insertions(+), 141 deletions(-) diff --git a/.gitignore b/.gitignore index 23c66394..1bff456c 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ internal/WAL/.tmp/* .cursor/* test_results/ +dlq/* +*.log diff --git a/Dockerfile b/Dockerfile index e82c07c9..8fa67e89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,6 +57,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ gnupg \ libc6 \ + netcat-openbsd \ + wget \ + openssl \ + python3 \ + gawk \ && mkdir -p /usr/local/apt-keys \ && gpg --fetch-keys https://neilalexander.s3.dualstack.eu-west-2.amazonaws.com/deb/key.txt \ && gpg --export 1C5162E133015D81A811239D1840CDAC6011C5EA \ @@ -65,10 +70,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ > /etc/apt/sources.list.d/yggdrasil.list \ && apt-get update && apt-get install -y --no-install-recommends \ yggdrasil \ - netcat-openbsd \ - wget \ - bzip2 \ - openssl \ && rm -rf /var/lib/apt/lists/* # Install ImmuDB @@ -79,32 +80,29 @@ RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \ -o /usr/local/bin/immudb && \ chmod +x /usr/local/bin/immudb -# Create non-root user -RUN groupadd -r jmdn && useradd -r -g jmdn -d /home/jmdn -s /bin/bash -m jmdn - # Create required directories (mirrors install_services.sh layout) RUN mkdir -p \ /etc/jmdn/certs \ /opt/jmdn/data/data \ /opt/jmdn/data/config \ /opt/jmdn/data/DB \ - /var/log/jmdn \ - && chown -R jmdn:jmdn /opt/jmdn /var/log/jmdn /etc/jmdn - -# Copy binary, scripts, and default config from builder -COPY --from=builder /src/jmdn /usr/local/bin/jmdn -COPY --from=builder /src/Scripts/start_jmdn_wrapper.sh /usr/local/bin/start_jmdn_wrapper.sh -COPY --from=builder /src/Scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -COPY --from=builder /src/Scripts/bootstrap_sync.sh /usr/local/bin/bootstrap_sync.sh + /var/log/jmdn + +# Copy binary and scripts from builder +COPY --from=builder /src/jmdn /usr/local/bin/jmdn +COPY --from=builder /src/Scripts/start_jmdn_wrapper.sh /usr/local/bin/start_jmdn_wrapper.sh +COPY --from=builder /src/Scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY --from=builder /src/Scripts/bootstrap_sync.sh /usr/local/bin/bootstrap_sync.sh RUN chmod +x /usr/local/bin/start_jmdn_wrapper.sh \ /usr/local/bin/docker-entrypoint.sh \ /usr/local/bin/bootstrap_sync.sh + # Copy default config as jmdn.yaml (mirrors setup_config.sh: cp jmdn_default.yaml → jmdn.yaml) COPY --from=builder /src/jmdn_default.yaml /etc/jmdn/jmdn.yaml -# peer.json must be at ./config/peer.json relative to WORKDIR (hardcoded in config/constants.go) -# WORKDIR is /opt/jmdn/data (volume) so it persists across restarts. -# Also kept at /etc/jmdn/peer.json as a fallback — bootstrap_sync wipes the volume, -# so the entrypoint restores from /etc/jmdn/peer.json if missing after bootstrap. + +# peer.json stored at two locations: +# /opt/jmdn/data/config/peer.json — runtime path (WORKDIR-relative, per config/constants.go) +# /etc/jmdn/peer.json — fallback used by entrypoint after bootstrap wipes the volume COPY --from=builder /src/config/peer.json /opt/jmdn/data/config/peer.json COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json @@ -119,18 +117,19 @@ COPY --from=builder /src/config/peer.json /etc/jmdn/peer.json EXPOSE 6090 16050 16055 16052 6545 6546 # Health check against actual API port (ports.api: 6090) -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ +# start-period extended to 60s to allow bootstrap sync on first run +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD curl -f http://localhost:6090/health || exit 1 -# Data volume for ImmuDB + node persistence +# Data volume — ImmuDB data, peer identity, certs, fastsync state VOLUME ["/opt/jmdn/data"] -# Run as root — required for bootstrap_sync.sh (chown after snapshot extract) -# WORKDIR matches where jmdn resolves ./config/peer.json (config/constants.go: PeerFile = "./config/peer.json") +# Run as root — bootstrap_sync.sh needs root for chown after snapshot extract +# WORKDIR must match where jmdn resolves ./config/peer.json +# (hardcoded in config/constants.go: PeerFile = "./config/peer.json") WORKDIR /opt/jmdn/data -# 1. bootstrap_sync.sh (first run only — downloads snapshot, writes sentinel) -# 2. immudb (starts in background) -# 3. start_jmdn_wrapper.sh → jmdn +# Startup order: ImmuDB → bootstrap sync → jmdn # Override config: -v /your/jmdn.yaml:/etc/jmdn/jmdn.yaml -CMD ["/usr/local/bin/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["-config", "/etc/jmdn/jmdn.yaml"] diff --git a/Scripts/bootstrap_sync.sh b/Scripts/bootstrap_sync.sh index f325aca6..a0e3d07e 100644 --- a/Scripts/bootstrap_sync.sh +++ b/Scripts/bootstrap_sync.sh @@ -1,90 +1,161 @@ #!/usr/bin/env bash -# bootstrap_sync.sh - First-run snapshot sync for JMDN Docker container +# bootstrap_sync.sh - JMDN Docker Bootstrap Sync # -# Downloads and extracts the latest data snapshot before the node starts. -# Runs ONLY on the first container start — guarded by a sentinel file on the -# volume (/opt/jmdn/data/.bootstrapped). Subsequent restarts skip this entirely. +# Mirrors bootstrap.yml Ansible playbook for Docker deployments. +# Runs ONCE — guarded by /opt/jmdn/data/.bootstrapped sentinel on the volume. # -# Override the snapshot URL via env var: -# -e BOOTSTRAP_TAR_URL=https://your-bucket/snapshot.tar.bz2 +# Flow (matches ansible task order): +# 1. List + download multipart files and checksums.md5 from GCS (public HTTP) +# 2. Normalise checksums (strip GCS paths → local basenames) and verify +# 3. Backup existing data → /opt/jmdn/backup/data_ +# 4. Clear immudb identity/state files +# 5. cat parts* | tar -xzf - into sandbox +# 6. Auto-discover real data root (parent dir of systemdb/) +# 7. Move to /opt/jmdn/data, fix permissions, clean up +# 8. Write sentinel # -# To force a re-sync (e.g. after wiping the volume): +# Env overrides: +# GCS_BUCKET GCS bucket name (default: jmzk-releases) +# GCS_PREFIX Path prefix in bucket (default: jmdn_bootstrap_2306) +# PARTS_PREFIX Part filename prefix (default: data_backup_23062026.part) +# CHECKSUM_FILE Checksum filename (default: checksums.md5) +# +# To force a re-sync after wiping the volume: # docker exec jmdn rm /opt/jmdn/data/.bootstrapped && docker restart jmdn -set -eo pipefail +set -euo pipefail + +# ── Config ─────────────────────────────────────────────── +GCS_BUCKET="${GCS_BUCKET:-jmzk-releases}" +GCS_PREFIX="${GCS_PREFIX:-jmdn_bootstrap_2306}" +PARTS_PREFIX="${PARTS_PREFIX:-data_backup_23062026.part}" +CHECKSUM_FILE="${CHECKSUM_FILE:-checksums.md5}" + +BASE_DIR="/opt/jmdn" +DATA_DIR="${BASE_DIR}/data" +WORK_DIR="${BASE_DIR}/bootstrap_tmp" +BACKUP_BASE="${BASE_DIR}/backup" +IMMUDB_STATE_DIR="${BASE_DIR}/.immudb_state" +SENTINEL="${DATA_DIR}/.bootstrapped" -SENTINEL="/opt/jmdn/data/.bootstrapped" -DEFAULT_TAR_URL="https://storage.googleapis.com/jmzk-releases/JMZK-Decentralised-Network/jmdn_data_20260604_145256.tar.bz2" -TAR_URL="${BOOTSTRAP_TAR_URL:-$DEFAULT_TAR_URL}" -TAR_FILE="/tmp/$(basename "$TAR_URL")" +GCS_HTTP="https://storage.googleapis.com" +GCS_API="${GCS_HTTP}/storage/v1/b/${GCS_BUCKET}/o" -log() { echo "[bootstrap] $*"; } -die() { echo "[bootstrap] ERROR: $*" >&2; exit 1; } +log() { echo "[bootstrap] $*"; } +die() { echo "[bootstrap] ERROR: $*" >&2; exit 1; } -# ── Guard: skip if already bootstrapped ────────────────── +# ── Guard ───────────────────────────────────────────────── if [ -f "$SENTINEL" ]; then - log "Sentinel found at $SENTINEL — skipping bootstrap." + log "Sentinel found — skipping bootstrap." exit 0 fi log "First run detected — starting bootstrap sync." -log "Snapshot URL: $TAR_URL" - -# ── Download ───────────────────────────────────────────── -log "Downloading snapshot..." -wget -q --show-progress -O "$TAR_FILE" "$TAR_URL" - -# ── Checksum verification ───────────────────────────────── -# Expects a checksums.md5 file in the same bucket directory as the snapshot. -# Format matches the original: md5sum data-patched.part* > checksums_local.md5 -# diff checksums.md5 checksums_local.md5 -SNAPSHOT_DIR="${TAR_URL%/*}" -CHECKSUM_URL="${SNAPSHOT_DIR}/checksums.md5" -CHECKSUM_REMOTE="/tmp/checksums_remote.md5" -CHECKSUM_LOCAL="/tmp/checksums_local.md5" - -log "Downloading remote checksum from: $CHECKSUM_URL" -if wget -q -O "$CHECKSUM_REMOTE" "$CHECKSUM_URL"; then - log "Computing local checksum..." - md5sum "$TAR_FILE" | awk -v fname="$(basename "$TAR_FILE")" '{print $1 " " fname}' > "$CHECKSUM_LOCAL" - - # Normalise: compare only filenames present in the local file - if diff \ - <(grep "$(basename "$TAR_FILE")" "$CHECKSUM_REMOTE" | awk '{print $1}') \ - <(awk '{print $1}' "$CHECKSUM_LOCAL") > /dev/null 2>&1; then - log "Checksum OK." - else - rm -f "$TAR_FILE" "$CHECKSUM_REMOTE" "$CHECKSUM_LOCAL" - die "Checksum mismatch — aborting bootstrap to prevent corrupt data." - fi - rm -f "$CHECKSUM_REMOTE" "$CHECKSUM_LOCAL" -else - log "WARNING: No checksums.md5 found at remote — skipping verification." -fi -# ── Wipe existing data (mirrors original script) ───────── -# /opt/jmdn/data is a Docker volume mount point — cannot rm the dir itself, -# only its contents. -log "Cleaning /opt/jmdn/data contents..." -find /opt/jmdn/data -mindepth 1 -delete +# ── Ensure required tools ───────────────────────────────── +for tool in curl wget awk md5sum tar python3; do + command -v "$tool" >/dev/null 2>&1 || die "$tool is required but not found." +done + +# ── Prepare work directory ─────────────────────────────── +log "Preparing work directory: $WORK_DIR" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" + +# ── List parts from GCS (public HTTP, no gsutil needed) ── +log "Listing parts from GCS: gs://${GCS_BUCKET}/${GCS_PREFIX}/${PARTS_PREFIX}*" +PARTS_JSON=$(curl -sf "${GCS_API}?prefix=${GCS_PREFIX}/${PARTS_PREFIX}" \ + || die "Failed to list objects from GCS. Check bucket name and network.") + +PART_NAMES=$(echo "$PARTS_JSON" | python3 -c " +import json, sys +data = json.load(sys.stdin) +items = data.get('items', []) +if not items: + raise SystemExit('No parts found matching prefix') +for item in sorted(items, key=lambda x: x['name']): + print(item['name']) +") || die "Failed to parse GCS listing." + +[ -z "$PART_NAMES" ] && die "No parts found for prefix ${GCS_PREFIX}/${PARTS_PREFIX}" + +log "Found parts:" +echo "$PART_NAMES" | while read -r p; do log " gs://${GCS_BUCKET}/$p"; done + +# ── Download parts ──────────────────────────────────────── +log "Downloading parts to $WORK_DIR..." +echo "$PART_NAMES" | while read -r part_path; do + fname=$(basename "$part_path") + log " $fname" + wget -q --show-progress -O "$WORK_DIR/$fname" "${GCS_HTTP}/${GCS_BUCKET}/${part_path}" +done +# ── Download checksums ──────────────────────────────────── +log "Downloading ${CHECKSUM_FILE}..." +wget -q -O "$WORK_DIR/$CHECKSUM_FILE" \ + "${GCS_HTTP}/${GCS_BUCKET}/${GCS_PREFIX}/${CHECKSUM_FILE}" \ + || die "Failed to download ${CHECKSUM_FILE} from GCS." + +# ── Verify checksums ────────────────────────────────────── +# Mirrors ansible: +# awk '{n=split($2,a,"/"); print $1 " " a[n]}' checksums.md5 > checksums_local.md5 +# md5sum -c checksums_local.md5 +log "Normalising and verifying checksums..." +awk '{n=split($2,a,"/"); print $1 " " a[n]}' \ + "$WORK_DIR/$CHECKSUM_FILE" > "$WORK_DIR/checksums_local.md5" + +(cd "$WORK_DIR" && md5sum -c checksums_local.md5) \ + || die "Checksum verification failed — aborting to prevent corrupt data." +log "Checksums OK." + +# ── Backup existing data ────────────────────────────────── +if [ -d "$DATA_DIR" ] && [ "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then + TIMESTAMP=$(date -u '+%Y%m%dT%H%M%S') + BACKUP_DIR="${BACKUP_BASE}/data_${TIMESTAMP}" + log "Backing up existing data → $BACKUP_DIR" + mkdir -p "$BACKUP_BASE" + mv "$DATA_DIR" "$BACKUP_DIR" +fi + +# ── Clear immudb identity/state files ──────────────────── log "Clearing immudb identity and state files..." -rm -rf /opt/jmdn/.immudb_state/.identity-* 2>/dev/null || true -rm -rf /opt/jmdn/.immudb_state/.state-* 2>/dev/null || true +rm -f "${IMMUDB_STATE_DIR}"/.identity-* 2>/dev/null || true +rm -f "${IMMUDB_STATE_DIR}"/.state-* 2>/dev/null || true + +# ── Extract into sandbox ────────────────────────────────── +# Mirrors ansible: cat parts* | tar -xzf - -C sandbox +SANDBOX="${BASE_DIR}/data_tmp/sandbox" +log "Extracting parts into sandbox: $SANDBOX" +mkdir -p "$SANDBOX" + +# shellcheck disable=SC2086 +cat "$WORK_DIR"/${PARTS_PREFIX}* | tar -xzf - -C "$SANDBOX" \ + || die "Extraction failed." + +# ── Auto-discover real data root ────────────────────────── +# Mirrors ansible: find systemdb dir → its parent is the real data root +log "Discovering data root (searching for systemdb/)..." +REAL_DATA_DIR=$(find "$SANDBOX" -type d -name "systemdb" 2>/dev/null \ + | head -n 1 | xargs -I{} dirname {}) + +if [ -z "$REAL_DATA_DIR" ] || [ "$REAL_DATA_DIR" = "." ]; then + die "Could not find systemdb in extracted sandbox. Check snapshot integrity." +fi +log "Data root found: $REAL_DATA_DIR" -# ── Extract ────────────────────────────────────────────── -log "Extracting snapshot to /..." -tar -xjf "$TAR_FILE" -C / +# ── Move to final location ──────────────────────────────── +log "Moving data to $DATA_DIR..." +mv "$REAL_DATA_DIR" "$DATA_DIR" +rm -rf "${BASE_DIR}/data_tmp" -# ── Permissions ────────────────────────────────────────── -log "Fixing permissions on /opt/jmdn/data..." -chown -R root:root /opt/jmdn/data +# ── Fix permissions ─────────────────────────────────────── +log "Fixing permissions on $DATA_DIR..." +chown -R root:root "$DATA_DIR" -# ── Cleanup ────────────────────────────────────────────── -log "Removing downloaded tar..." -rm -f "$TAR_FILE" +# ── Clean up work directory ─────────────────────────────── +log "Cleaning up work directory..." +rm -rf "$WORK_DIR" -# ── Write sentinel ─────────────────────────────────────── -# Sentinel lives on the volume → persists across container restarts +# ── Write sentinel ──────────────────────────────────────── touch "$SENTINEL" log "Bootstrap complete. Sentinel written → $SENTINEL" diff --git a/Scripts/docker-entrypoint.sh b/Scripts/docker-entrypoint.sh index 4f738cdf..fb1e78b2 100644 --- a/Scripts/docker-entrypoint.sh +++ b/Scripts/docker-entrypoint.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # docker-entrypoint.sh - Container startup orchestrator for JMDN # -# Order of operations: -# 1. Bootstrap sync (first run only — downloads + extracts snapshot) -# 2. ImmuDB (starts in background, waits for port 3322) -# 3. jmdn (exec via start_jmdn_wrapper.sh) +# Startup order (mirrors bootstrap.yml ansible play): +# 1. ImmuDB — start in background, wait for port 3322 +# 2. Bootstrap sync — first run only (download snapshot, verify, extract) +# 3. Restore paths — peer.json, certs, required dirs (wiped by bootstrap) +# 4. jmdn — exec only if all prior steps succeed -set -eo pipefail +set -euo pipefail IMMUDB_PORT="${IMMUDB_PORT:-3322}" IMMUDB_DIR="${IMMUDB_DIR:-/opt/jmdn/data}" @@ -14,39 +15,62 @@ IMMUDB_READY_TIMEOUT="${IMMUDB_READY_TIMEOUT:-30}" log() { echo "[entrypoint] $*"; } -# ── Step 1: Bootstrap sync (first run only) ────────────── -/usr/local/bin/bootstrap_sync.sh +# ── Step 1: Start ImmuDB ───────────────────────────────── +log "Starting ImmuDB (dir: ${IMMUDB_DIR})..." +immudb --dir "${IMMUDB_DIR}" & +IMMUDB_PID=$! + +log "Waiting for ImmuDB on port ${IMMUDB_PORT}..." +elapsed=0 +until nc -z 127.0.0.1 "${IMMUDB_PORT}" 2>/dev/null; do + if [ "${elapsed}" -ge "${IMMUDB_READY_TIMEOUT}" ]; then + log "ERROR: ImmuDB did not start within ${IMMUDB_READY_TIMEOUT}s" + kill "${IMMUDB_PID}" 2>/dev/null || true + exit 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +log "ImmuDB ready (${elapsed}s)" + +_shutdown() { + log "Shutting down..." + kill "${IMMUDB_PID}" 2>/dev/null || true +} +trap _shutdown TERM INT + +# ── Step 2: Bootstrap sync (first run only) ────────────── +if ! /usr/local/bin/bootstrap_sync.sh; then + log "ERROR: Bootstrap failed — not starting jmdn." + kill "${IMMUDB_PID}" 2>/dev/null || true + exit 1 +fi -# ── Restore / create required paths after bootstrap ────── -# Bootstrap wipes the volume — restore anything that must exist before jmdn starts. +# ── Step 3: Restore paths wiped by bootstrap ───────────── -# peer.json (hardcoded in config/constants.go: PeerFile = "./config/peer.json", -# resolved relative to WORKDIR /opt/jmdn/data) +# peer.json — hardcoded relative path in config/constants.go: +# PeerFile = "./config/peer.json" resolved from WORKDIR (/opt/jmdn/data) if [ ! -f /opt/jmdn/data/config/peer.json ]; then log "peer.json missing — restoring from /etc/jmdn/peer.json" mkdir -p /opt/jmdn/data/config cp /etc/jmdn/peer.json /opt/jmdn/data/config/peer.json fi -# Required directories (jmdn.yaml: cdc.dlq_path, thebe.kv_path) +# Required dirs (jmdn.yaml: security.cert_dir, cdc.dlq_path, thebe.kv_path) mkdir -p \ - /opt/jmdn/data/certs \ - /opt/jmdn/data/dlq \ - /opt/jmdn/data/storage/thebe-kv + /opt/jmdn/data/certs -# TLS certs (security.cert_dir: "certs", resolved relative to WORKDIR) -# Mirrors Scripts/setup_certs.sh — generates a local CA + per-service certs. -# For production, mount real certs via -v /your/certs:/opt/jmdn/data/certs +# TLS certs — mirrors Scripts/setup_certs.sh +# Generated only if missing. For production mount real certs: +# -v /your/certs:/opt/jmdn/data/certs if [ ! -f /opt/jmdn/data/certs/ca.crt ]; then - log "Generating self-signed TLS certs in /opt/jmdn/data/certs..." + log "Generating self-signed TLS certs..." CERT_DIR=/opt/jmdn/data/certs - # CA openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ -keyout "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.crt" \ -subj "/C=US/O=JMDN/CN=JMDN Dev Root CA" 2>/dev/null - # Per-service certs (matches setup_certs.sh SERVICES list) for SVC in cli_admin block_ingest_grpc block_ingest_http did_service \ explorer_api mempool_service admin_client explorer_client; do openssl genrsa -out "$CERT_DIR/$SVC.key" 2048 2>/dev/null @@ -62,31 +86,6 @@ if [ ! -f /opt/jmdn/data/certs/ca.crt ]; then log "TLS certs generated." fi -# ── Step 2: Start ImmuDB ───────────────────────────────── -log "Starting ImmuDB (dir: ${IMMUDB_DIR})..." -immudb --dir "${IMMUDB_DIR}" & -IMMUDB_PID=$! - -log "Waiting for ImmuDB on port ${IMMUDB_PORT}..." -elapsed=0 -until nc -z 127.0.0.1 "${IMMUDB_PORT}" 2>/dev/null; do - if [ "${elapsed}" -ge "${IMMUDB_READY_TIMEOUT}" ]; then - log "ERROR: ImmuDB did not start within ${IMMUDB_READY_TIMEOUT}s" - kill "${IMMUDB_PID}" 2>/dev/null || true - exit 1 - fi - sleep 1 - elapsed=$((elapsed + 1)) -done -log "ImmuDB ready (${elapsed}s)" - -# Forward SIGTERM/SIGINT to ImmuDB on shutdown -_shutdown() { - log "Shutting down..." - kill "${IMMUDB_PID}" 2>/dev/null || true -} -trap _shutdown TERM INT - -# ── Step 3: Start JMDN ─────────────────────────────────── +# ── Step 4: Start JMDN ─────────────────────────────────── log "Starting JMDN..." exec /usr/local/bin/start_jmdn_wrapper.sh "$@"