diff --git a/opensearch-coord1/Dockerfile b/opensearch-coord1/Dockerfile
new file mode 100644
index 0000000..c6e2a71
--- /dev/null
+++ b/opensearch-coord1/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-coord1/README.md b/opensearch-coord1/README.md
new file mode 100644
index 0000000..145b384
--- /dev/null
+++ b/opensearch-coord1/README.md
@@ -0,0 +1 @@
+Node: os-coord-1 roles:['coordinating_only'] heap:2g http:9203 transport:9303
diff --git a/opensearch-coord1/config.json b/opensearch-coord1/config.json
new file mode 100644
index 0000000..6695e7f
--- /dev/null
+++ b/opensearch-coord1/config.json
@@ -0,0 +1,98 @@
+{
+ "name": "OpenSearch os-coord-1",
+ "version": "1.0.0",
+ "slug": "opensearch-coord1",
+ "description": "OpenSearch server (es-compatible) for logs and metrics. Configurable JVM heap, node roles, cluster discovery and volumes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "host_pid": false,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ],
+ [
+ "data",
+ "/var/lib/opensearch",
+ "rw"
+ ]
+ ],
+ "options": {
+ "cluster_name": "hass-opensearch-cluster",
+ "node_name": "os-coord-1",
+ "node_roles": [
+ "coordinating_only"
+ ],
+ "discovery_type": "single-node",
+ "discovery_seed_hosts": [],
+ "initial_master_nodes": [],
+ "opensearch_heap": "2g",
+ "plugins": [],
+ "bootstrap_memory_lock": false,
+ "http_port": 9203,
+ "transport_port": 9303,
+ "network_host": "0.0.0.0",
+ "path_data": "/var/lib/opensearch",
+ "path_logs": "/var/log/opensearch",
+ "security_enabled": false
+ },
+ "schema": {
+ "cluster_name": "str",
+ "node_name": "str",
+ "node_roles": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "discovery_type": [
+ "str",
+ [
+ "single-node",
+ "zen",
+ "dns",
+ "ec2"
+ ]
+ ],
+ "discovery_seed_hosts": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "initial_master_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "opensearch_heap": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "bootstrap_memory_lock": "bool",
+ "http_port": "int",
+ "transport_port": "int",
+ "network_host": "str",
+ "path_data": "str",
+ "path_logs": "str",
+ "security_enabled": "bool"
+ },
+ "ports": {
+ "9203/tcp": 9203,
+ "9303/tcp": 9303
+ },
+ "environment": {
+ "OPENSEARCH_JAVA_OPTS": "-Xms${options[opensearch_heap]} -Xmx${options[opensearch_heap]}"
+ }
+}
\ No newline at end of file
diff --git a/opensearch-coord1/icon.svg b/opensearch-coord1/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-coord1/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-coord1/logo.png b/opensearch-coord1/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-coord1/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-coord1/rootfs/etc/cont-init.d/10-copy-config b/opensearch-coord1/rootfs/etc/cont-init.d/10-copy-config
new file mode 100644
index 0000000..33d1cef
--- /dev/null
+++ b/opensearch-coord1/rootfs/etc/cont-init.d/10-copy-config
@@ -0,0 +1,14 @@
+#!/command/with-contenv bash
+set -euo pipefail
+SRC_DIR=/rootfs-config
+DST_DIR=/usr/share/opensearch/config
+if [ -d "$SRC_DIR" ]; then
+ echo "[cont-init] Copying config snippets from $SRC_DIR to $DST_DIR"
+ for f in "$SRC_DIR"/*; do
+ base=$(basename "$f")
+ if [ ! -f "$DST_DIR/$base" ]; then
+ cp "$f" "$DST_DIR/"
+ echo "[cont-init] Copied $base"
+ fi
+ done
+fi
diff --git a/opensearch-coord1/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-coord1/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..ce0fe68
--- /dev/null
+++ b/opensearch-coord1/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing plugin: $plugin"
+ if /usr/share/opensearch/bin/opensearch-plugin install --batch "$plugin"; then
+ echo "[cont-init] Installed $plugin"
+ else
+ echo "[cont-init] Failed to install $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init b/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish b/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run b/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
new file mode 100644
index 0000000..e0e3fc2
--- /dev/null
+++ b/opensearch-coord1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
@@ -0,0 +1,33 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json || true
+NODE_NAME="os-coord-1"
+HEAP="2g"
+CLUSTER_NAME="hass-opensearch-cluster"
+HTTP_PORT=9203
+TRANSPORT_PORT=9303
+NODE_ROLES="coordinating_only"
+
+# override from options.json if present
+if [ -f "$OPTIONS_FILE" ]; then
+ NODE_NAME=$(jq -r '.node_name // "os-coord-1"' "$OPTIONS_FILE" 2>/dev/null || echo "os-coord-1")
+ HEAP=$(jq -r '.opensearch_heap // "2g"' "$OPTIONS_FILE" 2>/dev/null || echo "2g")
+ CLUSTER_NAME=$(jq -r '.cluster_name // "hass-opensearch-cluster"' "$OPTIONS_FILE" 2>/dev/null || echo "hass-opensearch-cluster")
+ HTTP_PORT=$(jq -r '.http_port // 9203' "$OPTIONS_FILE" 2>/dev/null || echo 9203)
+ TRANSPORT_PORT=$(jq -r '.transport_port // 9303' "$OPTIONS_FILE" 2>/dev/null || echo 9303)
+ NODE_ROLES=$(jq -r '.node_roles // ["coordinating_only"] | join(",")' "$OPTIONS_FILE" 2>/dev/null || echo "coordinating_only")
+fi
+
+export OPENSEARCH_JAVA_OPTS="-Xms$2g -Xmx$2g"
+
+echo "[s6-run] Starting OpenSearch node: $os-coord-1 roles:$coordinating_only heap:$2g http:$9203 transport:$9303"
+
+exec /usr/share/opensearch/bin/opensearch \
+ -E cluster.name=$hass-opensearch-cluster \
+ -E node.name=$os-coord-1 \
+ -E node.roles=[$coordinating_only] \
+ -E http.port=$9203 \
+ -E transport.port=$9303 \
+ -E network.host=0.0.0.0 \
+ -E discovery.seed_hosts=os-master,os-data-1,os-data-2,os-coord-1,os-coord-2 \
+ -E cluster.initial_master_nodes=os-master
diff --git a/opensearch-coord1/rootfs/rootfs-config/opensearch.yml b/opensearch-coord1/rootfs/rootfs-config/opensearch.yml
new file mode 100644
index 0000000..b4b0846
--- /dev/null
+++ b/opensearch-coord1/rootfs/rootfs-config/opensearch.yml
@@ -0,0 +1,8 @@
+cluster.name: hass-opensearch-cluster
+node.name: os-coord-1
+path.data: /var/lib/opensearch
+path.logs: /var/log/opensearch
+network.host: 0.0.0.0
+http.port: 9203
+transport.port: 9303
+node.roles: ['coordinating_only']
diff --git a/opensearch-coord2/Dockerfile b/opensearch-coord2/Dockerfile
new file mode 100644
index 0000000..c6e2a71
--- /dev/null
+++ b/opensearch-coord2/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-coord2/README.md b/opensearch-coord2/README.md
new file mode 100644
index 0000000..7009fd3
--- /dev/null
+++ b/opensearch-coord2/README.md
@@ -0,0 +1 @@
+Node: os-coord-2 roles:['coordinating_only'] heap:2g http:9204 transport:9304
diff --git a/opensearch-coord2/config.json b/opensearch-coord2/config.json
new file mode 100644
index 0000000..111cba9
--- /dev/null
+++ b/opensearch-coord2/config.json
@@ -0,0 +1,98 @@
+{
+ "name": "OpenSearch os-coord-2",
+ "version": "1.0.0",
+ "slug": "opensearch-coord2",
+ "description": "OpenSearch server (es-compatible) for logs and metrics. Configurable JVM heap, node roles, cluster discovery and volumes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "host_pid": false,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ],
+ [
+ "data",
+ "/var/lib/opensearch",
+ "rw"
+ ]
+ ],
+ "options": {
+ "cluster_name": "hass-opensearch-cluster",
+ "node_name": "os-coord-2",
+ "node_roles": [
+ "coordinating_only"
+ ],
+ "discovery_type": "single-node",
+ "discovery_seed_hosts": [],
+ "initial_master_nodes": [],
+ "opensearch_heap": "2g",
+ "plugins": [],
+ "bootstrap_memory_lock": false,
+ "http_port": 9204,
+ "transport_port": 9304,
+ "network_host": "0.0.0.0",
+ "path_data": "/var/lib/opensearch",
+ "path_logs": "/var/log/opensearch",
+ "security_enabled": false
+ },
+ "schema": {
+ "cluster_name": "str",
+ "node_name": "str",
+ "node_roles": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "discovery_type": [
+ "str",
+ [
+ "single-node",
+ "zen",
+ "dns",
+ "ec2"
+ ]
+ ],
+ "discovery_seed_hosts": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "initial_master_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "opensearch_heap": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "bootstrap_memory_lock": "bool",
+ "http_port": "int",
+ "transport_port": "int",
+ "network_host": "str",
+ "path_data": "str",
+ "path_logs": "str",
+ "security_enabled": "bool"
+ },
+ "ports": {
+ "9204/tcp": 9204,
+ "9304/tcp": 9304
+ },
+ "environment": {
+ "OPENSEARCH_JAVA_OPTS": "-Xms${options[opensearch_heap]} -Xmx${options[opensearch_heap]}"
+ }
+}
\ No newline at end of file
diff --git a/opensearch-coord2/icon.svg b/opensearch-coord2/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-coord2/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-coord2/logo.png b/opensearch-coord2/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-coord2/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-coord2/rootfs/etc/cont-init.d/10-copy-config b/opensearch-coord2/rootfs/etc/cont-init.d/10-copy-config
new file mode 100644
index 0000000..33d1cef
--- /dev/null
+++ b/opensearch-coord2/rootfs/etc/cont-init.d/10-copy-config
@@ -0,0 +1,14 @@
+#!/command/with-contenv bash
+set -euo pipefail
+SRC_DIR=/rootfs-config
+DST_DIR=/usr/share/opensearch/config
+if [ -d "$SRC_DIR" ]; then
+ echo "[cont-init] Copying config snippets from $SRC_DIR to $DST_DIR"
+ for f in "$SRC_DIR"/*; do
+ base=$(basename "$f")
+ if [ ! -f "$DST_DIR/$base" ]; then
+ cp "$f" "$DST_DIR/"
+ echo "[cont-init] Copied $base"
+ fi
+ done
+fi
diff --git a/opensearch-coord2/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-coord2/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..ce0fe68
--- /dev/null
+++ b/opensearch-coord2/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing plugin: $plugin"
+ if /usr/share/opensearch/bin/opensearch-plugin install --batch "$plugin"; then
+ echo "[cont-init] Installed $plugin"
+ else
+ echo "[cont-init] Failed to install $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init b/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish b/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run b/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
new file mode 100644
index 0000000..8eaab58
--- /dev/null
+++ b/opensearch-coord2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
@@ -0,0 +1,33 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json || true
+NODE_NAME="os-coord-2"
+HEAP="2g"
+CLUSTER_NAME="hass-opensearch-cluster"
+HTTP_PORT=9204
+TRANSPORT_PORT=9304
+NODE_ROLES="coordinating_only"
+
+# override from options.json if present
+if [ -f "$OPTIONS_FILE" ]; then
+ NODE_NAME=$(jq -r '.node_name // "os-coord-2"' "$OPTIONS_FILE" 2>/dev/null || echo "os-coord-2")
+ HEAP=$(jq -r '.opensearch_heap // "2g"' "$OPTIONS_FILE" 2>/dev/null || echo "2g")
+ CLUSTER_NAME=$(jq -r '.cluster_name // "hass-opensearch-cluster"' "$OPTIONS_FILE" 2>/dev/null || echo "hass-opensearch-cluster")
+ HTTP_PORT=$(jq -r '.http_port // 9204' "$OPTIONS_FILE" 2>/dev/null || echo 9204)
+ TRANSPORT_PORT=$(jq -r '.transport_port // 9304' "$OPTIONS_FILE" 2>/dev/null || echo 9304)
+ NODE_ROLES=$(jq -r '.node_roles // ["coordinating_only"] | join(",")' "$OPTIONS_FILE" 2>/dev/null || echo "coordinating_only")
+fi
+
+export OPENSEARCH_JAVA_OPTS="-Xms$2g -Xmx$2g"
+
+echo "[s6-run] Starting OpenSearch node: $os-coord-2 roles:$coordinating_only heap:$2g http:$9204 transport:$9304"
+
+exec /usr/share/opensearch/bin/opensearch \
+ -E cluster.name=$hass-opensearch-cluster \
+ -E node.name=$os-coord-2 \
+ -E node.roles=[$coordinating_only] \
+ -E http.port=$9204 \
+ -E transport.port=$9304 \
+ -E network.host=0.0.0.0 \
+ -E discovery.seed_hosts=os-master,os-data-1,os-data-2,os-coord-1,os-coord-2 \
+ -E cluster.initial_master_nodes=os-master
diff --git a/opensearch-coord2/rootfs/rootfs-config/opensearch.yml b/opensearch-coord2/rootfs/rootfs-config/opensearch.yml
new file mode 100644
index 0000000..5edc663
--- /dev/null
+++ b/opensearch-coord2/rootfs/rootfs-config/opensearch.yml
@@ -0,0 +1,8 @@
+cluster.name: hass-opensearch-cluster
+node.name: os-coord-2
+path.data: /var/lib/opensearch
+path.logs: /var/log/opensearch
+network.host: 0.0.0.0
+http.port: 9204
+transport.port: 9304
+node.roles: ['coordinating_only']
diff --git a/opensearch-dashboards/Dockerfile b/opensearch-dashboards/Dockerfile
new file mode 100644
index 0000000..ebe5ea9
--- /dev/null
+++ b/opensearch-dashboards/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch-dashboards:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-dashboards/README.md b/opensearch-dashboards/README.md
new file mode 100644
index 0000000..02cf310
--- /dev/null
+++ b/opensearch-dashboards/README.md
@@ -0,0 +1 @@
+OpenSearch Dashboards add-on. Point opensearch_host to coordinator nodes or a load balancer.
diff --git a/opensearch-dashboards/config.json b/opensearch-dashboards/config.json
new file mode 100644
index 0000000..57dbf89
--- /dev/null
+++ b/opensearch-dashboards/config.json
@@ -0,0 +1,47 @@
+{
+ "name": "OpenSearch Dashboards",
+ "version": "1.0.0",
+ "slug": "opensearch-dashboards",
+ "description": "OpenSearch Dashboards (Kibana compatible) to visualize OpenSearch data.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ]
+ ],
+ "options": {
+ "opensearch_host": "http://os-coord-1",
+ "opensearch_port": 9203,
+ "dashboards_port": 5601,
+ "opensearch_username": "",
+ "opensearch_password": "",
+ "kibana_index": ".kibana",
+ "plugins": []
+ },
+ "schema": {
+ "opensearch_host": "str",
+ "opensearch_port": "int",
+ "dashboards_port": "int",
+ "opensearch_username": "str",
+ "opensearch_password": "str",
+ "kibana_index": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ]
+ },
+ "ports": {
+ "5601/tcp": 5601
+ }
+}
\ No newline at end of file
diff --git a/opensearch-dashboards/icon.svg b/opensearch-dashboards/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-dashboards/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-dashboards/logo.png b/opensearch-dashboards/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-dashboards/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-dashboards/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-dashboards/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..048adb9
--- /dev/null
+++ b/opensearch-dashboards/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping dashboards plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No dashboards plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing dashboards plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing dashboards plugin: $plugin"
+ if /usr/share/opensearch-dashboards/bin/opensearch-dashboards-plugin install --allow-root "$plugin"; then
+ echo "[cont-init] Installed dashboards plugin $plugin"
+ else
+ echo "[cont-init] Failed to install dashboards plugin $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/dependencies.d/cont-init b/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/finish b/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/run b/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/run
new file mode 100644
index 0000000..c01eab1
--- /dev/null
+++ b/opensearch-dashboards/rootfs/etc/s6-overlay/s6-rc.d/dashboards/run
@@ -0,0 +1,8 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+HOST=$(jq -r '.opensearch_host // "http://localhost"' "$OPTIONS_FILE" 2>/dev/null || echo "http://localhost")
+PORT=$(jq -r '.opensearch_port // 9200' "$OPTIONS_FILE" 2>/dev/null || echo 9200)
+OPTS_HOSTS="${HOST}:${PORT}"
+echo "[s6-run] Starting OpenSearch Dashboards connecting to ${OPTS_HOSTS}"
+exec /usr/share/opensearch-dashboards/bin/opensearch-dashboards --opensearch.hosts="${OPTS_HOSTS}" --server.host=0.0.0.0
diff --git a/opensearch-dashboards/rootfs/rootfs-config/opensearch_dashboards.yml b/opensearch-dashboards/rootfs/rootfs-config/opensearch_dashboards.yml
new file mode 100644
index 0000000..08c775f
--- /dev/null
+++ b/opensearch-dashboards/rootfs/rootfs-config/opensearch_dashboards.yml
@@ -0,0 +1 @@
+server.host: 0.0.0.0
diff --git a/opensearch-data1/Dockerfile b/opensearch-data1/Dockerfile
new file mode 100644
index 0000000..c6e2a71
--- /dev/null
+++ b/opensearch-data1/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-data1/README.md b/opensearch-data1/README.md
new file mode 100644
index 0000000..8f5cf11
--- /dev/null
+++ b/opensearch-data1/README.md
@@ -0,0 +1 @@
+Node: os-data-1 roles:['data', 'ingest'] heap:5g http:9201 transport:9301
diff --git a/opensearch-data1/config.json b/opensearch-data1/config.json
new file mode 100644
index 0000000..0ae81d8
--- /dev/null
+++ b/opensearch-data1/config.json
@@ -0,0 +1,99 @@
+{
+ "name": "OpenSearch os-data-1",
+ "version": "1.0.0",
+ "slug": "opensearch-data1",
+ "description": "OpenSearch server (es-compatible) for logs and metrics. Configurable JVM heap, node roles, cluster discovery and volumes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "host_pid": false,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ],
+ [
+ "data",
+ "/var/lib/opensearch",
+ "rw"
+ ]
+ ],
+ "options": {
+ "cluster_name": "hass-opensearch-cluster",
+ "node_name": "os-data-1",
+ "node_roles": [
+ "data",
+ "ingest"
+ ],
+ "discovery_type": "single-node",
+ "discovery_seed_hosts": [],
+ "initial_master_nodes": [],
+ "opensearch_heap": "5g",
+ "plugins": [],
+ "bootstrap_memory_lock": false,
+ "http_port": 9201,
+ "transport_port": 9301,
+ "network_host": "0.0.0.0",
+ "path_data": "/var/lib/opensearch",
+ "path_logs": "/var/log/opensearch",
+ "security_enabled": false
+ },
+ "schema": {
+ "cluster_name": "str",
+ "node_name": "str",
+ "node_roles": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "discovery_type": [
+ "str",
+ [
+ "single-node",
+ "zen",
+ "dns",
+ "ec2"
+ ]
+ ],
+ "discovery_seed_hosts": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "initial_master_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "opensearch_heap": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "bootstrap_memory_lock": "bool",
+ "http_port": "int",
+ "transport_port": "int",
+ "network_host": "str",
+ "path_data": "str",
+ "path_logs": "str",
+ "security_enabled": "bool"
+ },
+ "ports": {
+ "9201/tcp": 9201,
+ "9301/tcp": 9301
+ },
+ "environment": {
+ "OPENSEARCH_JAVA_OPTS": "-Xms${options[opensearch_heap]} -Xmx${options[opensearch_heap]}"
+ }
+}
\ No newline at end of file
diff --git a/opensearch-data1/icon.svg b/opensearch-data1/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-data1/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-data1/logo.png b/opensearch-data1/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-data1/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-data1/rootfs/etc/cont-init.d/10-copy-config b/opensearch-data1/rootfs/etc/cont-init.d/10-copy-config
new file mode 100644
index 0000000..33d1cef
--- /dev/null
+++ b/opensearch-data1/rootfs/etc/cont-init.d/10-copy-config
@@ -0,0 +1,14 @@
+#!/command/with-contenv bash
+set -euo pipefail
+SRC_DIR=/rootfs-config
+DST_DIR=/usr/share/opensearch/config
+if [ -d "$SRC_DIR" ]; then
+ echo "[cont-init] Copying config snippets from $SRC_DIR to $DST_DIR"
+ for f in "$SRC_DIR"/*; do
+ base=$(basename "$f")
+ if [ ! -f "$DST_DIR/$base" ]; then
+ cp "$f" "$DST_DIR/"
+ echo "[cont-init] Copied $base"
+ fi
+ done
+fi
diff --git a/opensearch-data1/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-data1/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..ce0fe68
--- /dev/null
+++ b/opensearch-data1/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing plugin: $plugin"
+ if /usr/share/opensearch/bin/opensearch-plugin install --batch "$plugin"; then
+ echo "[cont-init] Installed $plugin"
+ else
+ echo "[cont-init] Failed to install $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init b/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish b/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run b/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
new file mode 100644
index 0000000..feafcb3
--- /dev/null
+++ b/opensearch-data1/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
@@ -0,0 +1,33 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json || true
+NODE_NAME="os-data-1"
+HEAP="5g"
+CLUSTER_NAME="hass-opensearch-cluster"
+HTTP_PORT=9201
+TRANSPORT_PORT=9301
+NODE_ROLES="data,ingest"
+
+# override from options.json if present
+if [ -f "$OPTIONS_FILE" ]; then
+ NODE_NAME=$(jq -r '.node_name // "os-data-1"' "$OPTIONS_FILE" 2>/dev/null || echo "os-data-1")
+ HEAP=$(jq -r '.opensearch_heap // "5g"' "$OPTIONS_FILE" 2>/dev/null || echo "5g")
+ CLUSTER_NAME=$(jq -r '.cluster_name // "hass-opensearch-cluster"' "$OPTIONS_FILE" 2>/dev/null || echo "hass-opensearch-cluster")
+ HTTP_PORT=$(jq -r '.http_port // 9201' "$OPTIONS_FILE" 2>/dev/null || echo 9201)
+ TRANSPORT_PORT=$(jq -r '.transport_port // 9301' "$OPTIONS_FILE" 2>/dev/null || echo 9301)
+ NODE_ROLES=$(jq -r '.node_roles // ["data,ingest"] | join(",")' "$OPTIONS_FILE" 2>/dev/null || echo "data,ingest")
+fi
+
+export OPENSEARCH_JAVA_OPTS="-Xms$5g -Xmx$5g"
+
+echo "[s6-run] Starting OpenSearch node: $os-data-1 roles:$data,ingest heap:$5g http:$9201 transport:$9301"
+
+exec /usr/share/opensearch/bin/opensearch \
+ -E cluster.name=$hass-opensearch-cluster \
+ -E node.name=$os-data-1 \
+ -E node.roles=[$data,ingest] \
+ -E http.port=$9201 \
+ -E transport.port=$9301 \
+ -E network.host=0.0.0.0 \
+ -E discovery.seed_hosts=os-master,os-data-1,os-data-2,os-coord-1,os-coord-2 \
+ -E cluster.initial_master_nodes=os-master
diff --git a/opensearch-data1/rootfs/rootfs-config/opensearch.yml b/opensearch-data1/rootfs/rootfs-config/opensearch.yml
new file mode 100644
index 0000000..7a47dcf
--- /dev/null
+++ b/opensearch-data1/rootfs/rootfs-config/opensearch.yml
@@ -0,0 +1,8 @@
+cluster.name: hass-opensearch-cluster
+node.name: os-data-1
+path.data: /var/lib/opensearch
+path.logs: /var/log/opensearch
+network.host: 0.0.0.0
+http.port: 9201
+transport.port: 9301
+node.roles: ['data', 'ingest']
diff --git a/opensearch-data2/Dockerfile b/opensearch-data2/Dockerfile
new file mode 100644
index 0000000..c6e2a71
--- /dev/null
+++ b/opensearch-data2/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-data2/README.md b/opensearch-data2/README.md
new file mode 100644
index 0000000..bf53bdb
--- /dev/null
+++ b/opensearch-data2/README.md
@@ -0,0 +1 @@
+Node: os-data-2 roles:['data', 'ingest'] heap:5g http:9202 transport:9302
diff --git a/opensearch-data2/config.json b/opensearch-data2/config.json
new file mode 100644
index 0000000..ee908c3
--- /dev/null
+++ b/opensearch-data2/config.json
@@ -0,0 +1,99 @@
+{
+ "name": "OpenSearch os-data-2",
+ "version": "1.0.0",
+ "slug": "opensearch-data2",
+ "description": "OpenSearch server (es-compatible) for logs and metrics. Configurable JVM heap, node roles, cluster discovery and volumes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "host_pid": false,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ],
+ [
+ "data",
+ "/var/lib/opensearch",
+ "rw"
+ ]
+ ],
+ "options": {
+ "cluster_name": "hass-opensearch-cluster",
+ "node_name": "os-data-2",
+ "node_roles": [
+ "data",
+ "ingest"
+ ],
+ "discovery_type": "single-node",
+ "discovery_seed_hosts": [],
+ "initial_master_nodes": [],
+ "opensearch_heap": "5g",
+ "plugins": [],
+ "bootstrap_memory_lock": false,
+ "http_port": 9202,
+ "transport_port": 9302,
+ "network_host": "0.0.0.0",
+ "path_data": "/var/lib/opensearch",
+ "path_logs": "/var/log/opensearch",
+ "security_enabled": false
+ },
+ "schema": {
+ "cluster_name": "str",
+ "node_name": "str",
+ "node_roles": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "discovery_type": [
+ "str",
+ [
+ "single-node",
+ "zen",
+ "dns",
+ "ec2"
+ ]
+ ],
+ "discovery_seed_hosts": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "initial_master_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "opensearch_heap": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "bootstrap_memory_lock": "bool",
+ "http_port": "int",
+ "transport_port": "int",
+ "network_host": "str",
+ "path_data": "str",
+ "path_logs": "str",
+ "security_enabled": "bool"
+ },
+ "ports": {
+ "9202/tcp": 9202,
+ "9302/tcp": 9302
+ },
+ "environment": {
+ "OPENSEARCH_JAVA_OPTS": "-Xms${options[opensearch_heap]} -Xmx${options[opensearch_heap]}"
+ }
+}
\ No newline at end of file
diff --git a/opensearch-data2/icon.svg b/opensearch-data2/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-data2/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-data2/logo.png b/opensearch-data2/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-data2/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-data2/rootfs/etc/cont-init.d/10-copy-config b/opensearch-data2/rootfs/etc/cont-init.d/10-copy-config
new file mode 100644
index 0000000..33d1cef
--- /dev/null
+++ b/opensearch-data2/rootfs/etc/cont-init.d/10-copy-config
@@ -0,0 +1,14 @@
+#!/command/with-contenv bash
+set -euo pipefail
+SRC_DIR=/rootfs-config
+DST_DIR=/usr/share/opensearch/config
+if [ -d "$SRC_DIR" ]; then
+ echo "[cont-init] Copying config snippets from $SRC_DIR to $DST_DIR"
+ for f in "$SRC_DIR"/*; do
+ base=$(basename "$f")
+ if [ ! -f "$DST_DIR/$base" ]; then
+ cp "$f" "$DST_DIR/"
+ echo "[cont-init] Copied $base"
+ fi
+ done
+fi
diff --git a/opensearch-data2/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-data2/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..ce0fe68
--- /dev/null
+++ b/opensearch-data2/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing plugin: $plugin"
+ if /usr/share/opensearch/bin/opensearch-plugin install --batch "$plugin"; then
+ echo "[cont-init] Installed $plugin"
+ else
+ echo "[cont-init] Failed to install $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init b/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish b/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run b/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
new file mode 100644
index 0000000..cf6316b
--- /dev/null
+++ b/opensearch-data2/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
@@ -0,0 +1,33 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json || true
+NODE_NAME="os-data-2"
+HEAP="5g"
+CLUSTER_NAME="hass-opensearch-cluster"
+HTTP_PORT=9202
+TRANSPORT_PORT=9302
+NODE_ROLES="data,ingest"
+
+# override from options.json if present
+if [ -f "$OPTIONS_FILE" ]; then
+ NODE_NAME=$(jq -r '.node_name // "os-data-2"' "$OPTIONS_FILE" 2>/dev/null || echo "os-data-2")
+ HEAP=$(jq -r '.opensearch_heap // "5g"' "$OPTIONS_FILE" 2>/dev/null || echo "5g")
+ CLUSTER_NAME=$(jq -r '.cluster_name // "hass-opensearch-cluster"' "$OPTIONS_FILE" 2>/dev/null || echo "hass-opensearch-cluster")
+ HTTP_PORT=$(jq -r '.http_port // 9202' "$OPTIONS_FILE" 2>/dev/null || echo 9202)
+ TRANSPORT_PORT=$(jq -r '.transport_port // 9302' "$OPTIONS_FILE" 2>/dev/null || echo 9302)
+ NODE_ROLES=$(jq -r '.node_roles // ["data,ingest"] | join(",")' "$OPTIONS_FILE" 2>/dev/null || echo "data,ingest")
+fi
+
+export OPENSEARCH_JAVA_OPTS="-Xms$5g -Xmx$5g"
+
+echo "[s6-run] Starting OpenSearch node: $os-data-2 roles:$data,ingest heap:$5g http:$9202 transport:$9302"
+
+exec /usr/share/opensearch/bin/opensearch \
+ -E cluster.name=$hass-opensearch-cluster \
+ -E node.name=$os-data-2 \
+ -E node.roles=[$data,ingest] \
+ -E http.port=$9202 \
+ -E transport.port=$9302 \
+ -E network.host=0.0.0.0 \
+ -E discovery.seed_hosts=os-master,os-data-1,os-data-2,os-coord-1,os-coord-2 \
+ -E cluster.initial_master_nodes=os-master
diff --git a/opensearch-data2/rootfs/rootfs-config/opensearch.yml b/opensearch-data2/rootfs/rootfs-config/opensearch.yml
new file mode 100644
index 0000000..5a5b167
--- /dev/null
+++ b/opensearch-data2/rootfs/rootfs-config/opensearch.yml
@@ -0,0 +1,8 @@
+cluster.name: hass-opensearch-cluster
+node.name: os-data-2
+path.data: /var/lib/opensearch
+path.logs: /var/log/opensearch
+network.host: 0.0.0.0
+http.port: 9202
+transport.port: 9302
+node.roles: ['data', 'ingest']
diff --git a/opensearch-master/Dockerfile b/opensearch-master/Dockerfile
new file mode 100644
index 0000000..c6e2a71
--- /dev/null
+++ b/opensearch-master/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=opensearchproject/opensearch:latest
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
diff --git a/opensearch-master/README.md b/opensearch-master/README.md
new file mode 100644
index 0000000..0c25193
--- /dev/null
+++ b/opensearch-master/README.md
@@ -0,0 +1 @@
+Node: os-master roles:['master'] heap:1.5g http:9200 transport:9300
diff --git a/opensearch-master/config.json b/opensearch-master/config.json
new file mode 100644
index 0000000..ce8f7e8
--- /dev/null
+++ b/opensearch-master/config.json
@@ -0,0 +1,98 @@
+{
+ "name": "OpenSearch os-master",
+ "version": "1.0.0",
+ "slug": "opensearch-master",
+ "description": "OpenSearch server (es-compatible) for logs and metrics. Configurable JVM heap, node roles, cluster discovery and volumes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "host_pid": false,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ],
+ [
+ "data",
+ "/var/lib/opensearch",
+ "rw"
+ ]
+ ],
+ "options": {
+ "cluster_name": "hass-opensearch-cluster",
+ "node_name": "os-master",
+ "node_roles": [
+ "master"
+ ],
+ "discovery_type": "single-node",
+ "discovery_seed_hosts": [],
+ "initial_master_nodes": [],
+ "opensearch_heap": "1.5g",
+ "plugins": [],
+ "bootstrap_memory_lock": false,
+ "http_port": 9200,
+ "transport_port": 9300,
+ "network_host": "0.0.0.0",
+ "path_data": "/var/lib/opensearch",
+ "path_logs": "/var/log/opensearch",
+ "security_enabled": false
+ },
+ "schema": {
+ "cluster_name": "str",
+ "node_name": "str",
+ "node_roles": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "discovery_type": [
+ "str",
+ [
+ "single-node",
+ "zen",
+ "dns",
+ "ec2"
+ ]
+ ],
+ "discovery_seed_hosts": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "initial_master_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "opensearch_heap": "str",
+ "plugins": [
+ "list",
+ [
+ "str"
+ ]
+ ],
+ "bootstrap_memory_lock": "bool",
+ "http_port": "int",
+ "transport_port": "int",
+ "network_host": "str",
+ "path_data": "str",
+ "path_logs": "str",
+ "security_enabled": "bool"
+ },
+ "ports": {
+ "9200/tcp": 9200,
+ "9300/tcp": 9300
+ },
+ "environment": {
+ "OPENSEARCH_JAVA_OPTS": "-Xms${options[opensearch_heap]} -Xmx${options[opensearch_heap]}"
+ }
+}
\ No newline at end of file
diff --git a/opensearch-master/icon.svg b/opensearch-master/icon.svg
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-master/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-master/logo.png b/opensearch-master/logo.png
new file mode 100644
index 0000000..2cf043b
--- /dev/null
+++ b/opensearch-master/logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/opensearch-master/rootfs/etc/cont-init.d/10-copy-config b/opensearch-master/rootfs/etc/cont-init.d/10-copy-config
new file mode 100644
index 0000000..33d1cef
--- /dev/null
+++ b/opensearch-master/rootfs/etc/cont-init.d/10-copy-config
@@ -0,0 +1,14 @@
+#!/command/with-contenv bash
+set -euo pipefail
+SRC_DIR=/rootfs-config
+DST_DIR=/usr/share/opensearch/config
+if [ -d "$SRC_DIR" ]; then
+ echo "[cont-init] Copying config snippets from $SRC_DIR to $DST_DIR"
+ for f in "$SRC_DIR"/*; do
+ base=$(basename "$f")
+ if [ ! -f "$DST_DIR/$base" ]; then
+ cp "$f" "$DST_DIR/"
+ echo "[cont-init] Copied $base"
+ fi
+ done
+fi
diff --git a/opensearch-master/rootfs/etc/cont-init.d/20-install-plugins b/opensearch-master/rootfs/etc/cont-init.d/20-install-plugins
new file mode 100644
index 0000000..ce0fe68
--- /dev/null
+++ b/opensearch-master/rootfs/etc/cont-init.d/20-install-plugins
@@ -0,0 +1,20 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json
+if [ ! -f "$OPTIONS_FILE" ]; then
+ echo "[cont-init] No options.json, skipping plugin install"
+ exit 0
+fi
+if ! jq -e '.plugins // [] | length > 0' "$OPTIONS_FILE" >/dev/null 2>&1; then
+ echo "[cont-init] No plugins configured, skipping"
+ exit 0
+fi
+echo "[cont-init] Installing plugins from options.json"
+jq -r '.plugins[]' "$OPTIONS_FILE" | while read -r plugin; do
+ echo "[cont-init] Installing plugin: $plugin"
+ if /usr/share/opensearch/bin/opensearch-plugin install --batch "$plugin"; then
+ echo "[cont-init] Installed $plugin"
+ else
+ echo "[cont-init] Failed to install $plugin (continuing)"
+ fi
+done
diff --git a/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init b/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish b/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run b/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
new file mode 100644
index 0000000..425fd11
--- /dev/null
+++ b/opensearch-master/rootfs/etc/s6-overlay/s6-rc.d/opensearch/run
@@ -0,0 +1,33 @@
+#!/command/with-contenv bash
+set -euo pipefail
+OPTIONS_FILE=/data/options.json || true
+NODE_NAME="os-master"
+HEAP="1.5g"
+CLUSTER_NAME="hass-opensearch-cluster"
+HTTP_PORT=9200
+TRANSPORT_PORT=9300
+NODE_ROLES="master"
+
+# override from options.json if present
+if [ -f "$OPTIONS_FILE" ]; then
+ NODE_NAME=$(jq -r '.node_name // "os-master"' "$OPTIONS_FILE" 2>/dev/null || echo "os-master")
+ HEAP=$(jq -r '.opensearch_heap // "1.5g"' "$OPTIONS_FILE" 2>/dev/null || echo "1.5g")
+ CLUSTER_NAME=$(jq -r '.cluster_name // "hass-opensearch-cluster"' "$OPTIONS_FILE" 2>/dev/null || echo "hass-opensearch-cluster")
+ HTTP_PORT=$(jq -r '.http_port // 9200' "$OPTIONS_FILE" 2>/dev/null || echo 9200)
+ TRANSPORT_PORT=$(jq -r '.transport_port // 9300' "$OPTIONS_FILE" 2>/dev/null || echo 9300)
+ NODE_ROLES=$(jq -r '.node_roles // ["master"] | join(",")' "$OPTIONS_FILE" 2>/dev/null || echo "master")
+fi
+
+export OPENSEARCH_JAVA_OPTS="-Xms$1.5g -Xmx$1.5g"
+
+echo "[s6-run] Starting OpenSearch node: $os-master roles:$master heap:$1.5g http:$9200 transport:$9300"
+
+exec /usr/share/opensearch/bin/opensearch \
+ -E cluster.name=$hass-opensearch-cluster \
+ -E node.name=$os-master \
+ -E node.roles=[$master] \
+ -E http.port=$9200 \
+ -E transport.port=$9300 \
+ -E network.host=0.0.0.0 \
+ -E discovery.seed_hosts=os-master,os-data-1,os-data-2,os-coord-1,os-coord-2 \
+ -E cluster.initial_master_nodes=os-master
diff --git a/opensearch-master/rootfs/rootfs-config/opensearch.yml b/opensearch-master/rootfs/rootfs-config/opensearch.yml
new file mode 100644
index 0000000..f4eb1a1
--- /dev/null
+++ b/opensearch-master/rootfs/rootfs-config/opensearch.yml
@@ -0,0 +1,8 @@
+cluster.name: hass-opensearch-cluster
+node.name: os-master
+path.data: /var/lib/opensearch
+path.logs: /var/log/opensearch
+network.host: 0.0.0.0
+http.port: 9200
+transport.port: 9300
+node.roles: ['master']
diff --git a/opensearch-nginx-lb/Dockerfile b/opensearch-nginx-lb/Dockerfile
new file mode 100644
index 0000000..41ac0bf
--- /dev/null
+++ b/opensearch-nginx-lb/Dockerfile
@@ -0,0 +1,3 @@
+ARG BASE_IMAGE=nginx:stable
+FROM ${BASE_IMAGE}
+COPY rootfs/ /
\ No newline at end of file
diff --git a/opensearch-nginx-lb/config.json b/opensearch-nginx-lb/config.json
new file mode 100644
index 0000000..7f9a304
--- /dev/null
+++ b/opensearch-nginx-lb/config.json
@@ -0,0 +1,40 @@
+{
+ "name": "OpenSearch NGINX Load Balancer",
+ "version": "1.0.0",
+ "slug": "opensearch-nginx-lb",
+ "description": "NGINX load balancer for OpenSearch coordinator nodes.",
+ "arch": [
+ "amd64",
+ "armv7",
+ "arm64"
+ ],
+ "startup": "services",
+ "boot": "auto",
+ "host_network": true,
+ "map": [
+ [
+ "config",
+ "./config",
+ "rw"
+ ]
+ ],
+ "options": {
+ "lb_port": 9200,
+ "coordinator_nodes": [
+ "os-coord-1",
+ "os-coord-2"
+ ]
+ },
+ "schema": {
+ "lb_port": "int",
+ "coordinator_nodes": [
+ "list",
+ [
+ "str"
+ ]
+ ]
+ },
+ "ports": {
+ "9200/tcp": 9200
+ }
+}
\ No newline at end of file
diff --git a/opensearch-nginx-lb/rootfs/config/nginx.conf b/opensearch-nginx-lb/rootfs/config/nginx.conf
new file mode 100644
index 0000000..c23d771
--- /dev/null
+++ b/opensearch-nginx-lb/rootfs/config/nginx.conf
@@ -0,0 +1,13 @@
+events {}
+http {
+ upstream opensearch_coord {
+ server os-coord-1:9200;
+ server os-coord-2:9200;
+ }
+ server {
+ listen 9200;
+ location / {
+ proxy_pass http://opensearch_coord;
+ }
+ }
+}
diff --git a/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/cont-init b/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/cont-init
new file mode 100644
index 0000000..e69de29
diff --git a/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish b/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish
new file mode 100644
index 0000000..599608c
--- /dev/null
+++ b/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish
@@ -0,0 +1,3 @@
+#!/command/execlineb -S0
+# Prevent restart loops; allow clean exit
+exit 0
diff --git a/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
new file mode 100644
index 0000000..ae1c6cd
--- /dev/null
+++ b/opensearch-nginx-lb/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
@@ -0,0 +1,4 @@
+#!/command/with-contenv bash
+set -euo pipefail
+echo "[s6-run] Starting NGINX load balancer"
+exec nginx -g 'daemon off;'