From 8f3b7b3b2bd5a6aa9559874470fb5fc6af030af2 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 2 Sep 2026 17:22:03 +0200 Subject: [PATCH 1/3] fix(apps): place n8n sandbox in app directory --- apps/docker-compose.yml | 159 ------------------ apps/{ => n8n-sandbox}/config.json | 0 apps/n8n-sandbox/docker-compose.json | 120 +++++++++++++ .../{ => n8n-sandbox/metadata}/description.md | 0 apps/{ => n8n-sandbox/metadata}/logo.jpg | Bin 5 files changed, 120 insertions(+), 159 deletions(-) delete mode 100644 apps/docker-compose.yml rename apps/{ => n8n-sandbox}/config.json (100%) create mode 100644 apps/n8n-sandbox/docker-compose.json rename apps/{ => n8n-sandbox/metadata}/description.md (100%) rename apps/{ => n8n-sandbox/metadata}/logo.jpg (100%) diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml deleted file mode 100644 index 9992c1c..0000000 --- a/apps/docker-compose.yml +++ /dev/null @@ -1,159 +0,0 @@ -# n8n Sandbox Service — app custom Runtipi (format docker-compose.yml + x-runtipi) -# -# Images : n8nio/n8n-sandbox-service-{api,runner-dind,sandbox}:1.3.0 -# (verifier au besoin : docker manifest inspect n8nio/n8n-sandbox-service-sandbox:1.3.0) -# -# Les 3 secrets ne sont PAS dans ce fichier : ce sont des form_fields du -# config.json (SANDBOX_API_KEYS, SANDBOX_REGISTRATION_TOKEN, SANDBOX_RUNNER_KEY), -# saisis/generes dans la GUI Runtipi a l'installation, modifiables ensuite -# dans les parametres de l'app, et injectes ici par ${...}. -# -# Cote app n8n (autre app Runtipi, doc "Set up AI Assistant", setup 2) : -# N8N_ENABLED_MODULES=instance-ai -# N8N_INSTANCE_AI_SANDBOX_ENABLED=true -# N8N_INSTANCE_AI_SANDBOX_PROVIDER=n8n-sandbox -# N8N_INSTANCE_AI_SANDBOX_IMAGE=n8nio/n8n-sandbox-service-sandbox:1.3.0 -# N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080 -# N8N_SANDBOX_SERVICE_API_KEY= -# Test depuis le conteneur n8n : wget -qO- http://sandbox-api:8080/healthz -# -# Les noms de services sandbox-api et sandbox-runner-1 sont fixes : ce sont -# les SAN des certificats. Si un nom ou NUM_RUNNERS change, supprimer -# ${APP_DATA_DIR}/data/tls (bootstrap-mtls.sh ne regenere rien tant que -# les fichiers existent). -# -# Donnees persistantes (toutes sous ${APP_DATA_DIR}/data, donc couvertes par -# les sauvegardes Runtipi — attention, runner-docker peut peser plusieurs Go) : -# tls/ certificats mTLS (CA + feuilles) -# api/ SQLite de l'API (etat des sandboxes) -# runner-state/ SQLite du runner -# runner-docker/ /var/lib/docker du DinD (cache de l'image sandbox) - -services: - sandbox-certs: - image: n8nio/n8n-sandbox-service-api:1.3.0 - # Obligatoire sous Runtipi : sans cette ligne le builder injecte - # restart: unless-stopped et ce job one-shot boucle en redemarrage. - restart: "no" - user: "0:0" - entrypoint: ["sh", "-c"] - # Genere les certs (idempotent) puis fixe les droits pour l'utilisateur - # sandbox-api de l'image : /tls/api et le repertoire SQLite de l'API - # (en bind mount, Docker cree le dossier en root sinon). - command: - - > - bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api - --control-san-prefix sandbox-runner && - chown -R sandbox-api:sandbox-api /tls/api /var/lib/n8n-sandbox-api - environment: - NUM_RUNNERS: "1" - volumes: - - ${APP_DATA_DIR}/data/tls:/tls - - ${APP_DATA_DIR}/data/api:/var/lib/n8n-sandbox-api - - sandbox-api: - image: n8nio/n8n-sandbox-service-api:1.3.0 - restart: unless-stopped - - depends_on: - sandbox-certs: - condition: service_completed_successfully - - environment: - SANDBOX_API_KEYS: "${SANDBOX_API_KEYS}" - SANDBOX_API_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}" - SANDBOX_API_RUNNER_API_KEY: "${SANDBOX_RUNNER_KEY}" - - SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt - SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key - SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt - - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key - SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1 - - volumes: - - ${APP_DATA_DIR}/data/tls:/tls:ro - # SANDBOX_API_DATA_DIR par defaut (SQLite). - - ${APP_DATA_DIR}/data/api:/var/lib/n8n-sandbox-api - - healthcheck: - # Forme chaine (et non tableau) : c'est la forme documentee par Runtipi - # et la seule acceptee par le formulaire de creation. - test: "wget -qO- http://localhost:8080/healthz" - interval: 5s - timeout: 3s - retries: 5 - start_period: 10s - - # is_main est OBLIGATOIRE : l'UI Runtipi refuse un compose sans - # exactement un service principal ("There must be exactly one main service"). - # Sans internal_port et sans cocher "open port" a l'installation, - # is_main ne publie aucun port et ne pose aucun label Traefik. - # Le service principal rejoint automatiquement tipi_main_network, ce qui - # rend sandbox-api:8080 joignable par les autres apps Runtipi (dont n8n), - # protege uniquement par SANDBOX_API_KEYS. Ne jamais publier 8080/9090. - x-runtipi: - is_main: true - add_to_main_network: true - - sandbox-runner-1: - image: n8nio/n8n-sandbox-service-runner-dind:1.3.0 - restart: unless-stopped - # Mode "dev/macOS" de l'upstream (qui attend sysbox-runc en production ; - # n8n recommande Daytona pour la prod). Equivalent root sur l'hote : - # ce service reste hors de tipi_main_network et ne publie aucun port. - privileged: true - - depends_on: - sandbox-api: - condition: service_healthy - - environment: - SANDBOX_RUNNER_API_KEYS: "${SANDBOX_RUNNER_KEY}" - SANDBOX_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}" - - SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 - # http:// et non https:// : le mTLS ne couvre que le gRPC - # (enregistrement + SandboxControl). Le trafic proxy exec/files de - # l'API vers le runner reste en HTTP clair authentifie par X-Api-Key - # (docs/configuration.md du depot et compose officiel n8n). - SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080 - - SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091" - SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 - - SANDBOX_RUNNER_ID: runner-1 - - SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: n8nio/n8n-sandbox-service-sandbox:1.3.0 - - SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt - SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt - SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key - SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api - - SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt - SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key - SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt - - volumes: - - ${APP_DATA_DIR}/data/tls:/tls:ro - # Etat SQLite du runner (SANDBOX_RUNNER_DATA_DIR par defaut). - - ${APP_DATA_DIR}/data/runner-state:/var/sandboxes - # Docker interne du DinD : sans ce montage, l'image sandbox est - # re-telechargee a chaque recreation de l'app. Necessite un app-data - # sur ext4/xfs/btrfs (overlay2 ne fonctionne pas sur NFS). - - ${APP_DATA_DIR}/data/runner-docker:/var/lib/docker - - healthcheck: - # /readyz passe au vert une fois le runner enregistre aupres de l'API - # (meme check que le compose du depot upstream). - test: "wget -qO- http://localhost:8080/readyz" - interval: 5s - timeout: 5s - retries: 24 - start_period: 20s - -x-runtipi: - schema_version: 2 diff --git a/apps/config.json b/apps/n8n-sandbox/config.json similarity index 100% rename from apps/config.json rename to apps/n8n-sandbox/config.json diff --git a/apps/n8n-sandbox/docker-compose.json b/apps/n8n-sandbox/docker-compose.json new file mode 100644 index 0000000..d9947c0 --- /dev/null +++ b/apps/n8n-sandbox/docker-compose.json @@ -0,0 +1,120 @@ +{ + "services": [ + { + "name": "sandbox-certs", + "image": "n8nio/n8n-sandbox-service-api:1.3.0", + "command": [ + "bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api --control-san-prefix sandbox-runner && chown -R sandbox-api:sandbox-api /tls/api /var/lib/n8n-sandbox-api\n" + ], + "entrypoint": [ + "sh", + "-c" + ], + "environment": { + "NUM_RUNNERS": "1" + }, + "user": "0:0", + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/tls", + "containerPath": "/tls" + }, + { + "hostPath": "${APP_DATA_DIR}/data/api", + "containerPath": "/var/lib/n8n-sandbox-api" + } + ] + }, + { + "name": "sandbox-api", + "image": "n8nio/n8n-sandbox-service-api:1.3.0", + "isMain": true, + "internalPort": 8080, + "addToMainNetwork": true, + "environment": { + "SANDBOX_API_KEYS": "${SANDBOX_API_KEYS}", + "SANDBOX_API_RUNNER_REGISTRATION_TOKEN": "${SANDBOX_REGISTRATION_TOKEN}", + "SANDBOX_API_RUNNER_API_KEY": "${SANDBOX_RUNNER_KEY}", + "SANDBOX_API_GRPC_TLS_CERT_FILE": "/tls/api/grpc-server.crt", + "SANDBOX_API_GRPC_TLS_KEY_FILE": "/tls/api/grpc-server.key", + "SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE": "/tls/api/ca.crt", + "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE": "/tls/api/ca.crt", + "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE": "/tls/api/control-grpc-api-client.crt", + "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE": "/tls/api/control-grpc-api-client.key", + "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME": "sandbox-runner-1" + }, + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/tls", + "containerPath": "/tls", + "readOnly": true + }, + { + "hostPath": "${APP_DATA_DIR}/data/api", + "containerPath": "/var/lib/n8n-sandbox-api" + } + ], + "healthCheck": { + "test": "wget -qO- http://localhost:8080/healthz", + "interval": "5s", + "timeout": "3s", + "retries": 5, + "startPeriod": "10s" + }, + "dependsOn": { + "sandbox-certs": { + "condition": "service_completed_successfully" + } + } + }, + { + "name": "sandbox-runner-1", + "image": "n8nio/n8n-sandbox-service-runner-dind:1.3.0", + "environment": { + "SANDBOX_RUNNER_API_KEYS": "${SANDBOX_RUNNER_KEY}", + "SANDBOX_RUNNER_REGISTRATION_TOKEN": "${SANDBOX_REGISTRATION_TOKEN}", + "SANDBOX_RUNNER_API_GRPC_ADDR": "sandbox-api:9090", + "SANDBOX_RUNNER_HTTP_BASE_URL": "http://sandbox-runner-1:8080", + "SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR": ":9091", + "SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR": "sandbox-runner-1:9091", + "SANDBOX_RUNNER_ID": "runner-1", + "SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE": "n8nio/n8n-sandbox-service-sandbox:1.3.0", + "SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE": "/tls/runner/ca.crt", + "SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE": "/tls/runner/grpc-client.crt", + "SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE": "/tls/runner/grpc-client.key", + "SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME": "sandbox-api", + "SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE": "/tls/runner/control-grpc-server.crt", + "SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE": "/tls/runner/control-grpc-server.key", + "SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE": "/tls/runner/ca.crt" + }, + "privileged": true, + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/tls", + "containerPath": "/tls", + "readOnly": true + }, + { + "hostPath": "${APP_DATA_DIR}/data/runner-state", + "containerPath": "/var/sandboxes" + }, + { + "hostPath": "${APP_DATA_DIR}/data/runner-docker", + "containerPath": "/var/lib/docker" + } + ], + "healthCheck": { + "test": "wget -qO- http://localhost:8080/readyz", + "interval": "5s", + "timeout": "5s", + "retries": 24, + "startPeriod": "20s" + }, + "dependsOn": { + "sandbox-api": { + "condition": "service_healthy" + } + } + } + ] +} diff --git a/apps/description.md b/apps/n8n-sandbox/metadata/description.md similarity index 100% rename from apps/description.md rename to apps/n8n-sandbox/metadata/description.md diff --git a/apps/logo.jpg b/apps/n8n-sandbox/metadata/logo.jpg similarity index 100% rename from apps/logo.jpg rename to apps/n8n-sandbox/metadata/logo.jpg From d24d0c194ba354a4cb3c4b4d86ee9787577464d9 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 2 Sep 2026 17:23:48 +0200 Subject: [PATCH 2/3] fix(apps): normalize environment mappings --- apps/mediamtx/docker-compose.json | 74 ++------- apps/nginx/docker-compose.json | 14 +- apps/wazuh-runtipi/docker-compose.json | 208 ++++++------------------- 3 files changed, 67 insertions(+), 229 deletions(-) diff --git a/apps/mediamtx/docker-compose.json b/apps/mediamtx/docker-compose.json index ce9814a..c038ecf 100644 --- a/apps/mediamtx/docker-compose.json +++ b/apps/mediamtx/docker-compose.json @@ -7,64 +7,22 @@ "hostname": "mediamtx", "isMain": true, "internalPort": "8889", - "environment": [ - { - "key": "MTX_PROTOCOLS", - "value": "tcp" - }, - { - "key": "MTX_LOGLEVEL", - "value": "info" - }, - { - "key": "MTX_LOGDESTINATIONS", - "value": "stdout" - }, - { - "key": "MTX_RTSPADDRESS", - "value": ":8554" - }, - { - "key": "MTX_RTMPADDRESS", - "value": ":1935" - }, - { - "key": "MTX_HLSADDRESS", - "value": ":8888" - }, - { - "key": "MTX_WEBRTCADDRESS", - "value": ":8889" - }, - { - "key": "MTX_SRTADDRESS", - "value": ":8890" - }, - { - "key": "MTX_APIADDRESS", - "value": ":9997" - }, - { - "key": "MTX_METRICSADDRESS", - "value": ":9998" - }, - { - "key": "MTX_AUTHINTERNALUSERS", - "value": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}" - }, - { - "key": "MTX_PATHDEFAULTS_RECORD", - "value": "${MTX_RECORD_ENABLED:-false}" - }, - { - "key": "MTX_PATHDEFAULTS_RECORDPATH", - "value": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f" - }, - { - "key": "MTX_PATHDEFAULTS_RECORDFORMAT", - "value": "fmp4" - } - ], + "environment": { + "MTX_PROTOCOLS": "tcp", + "MTX_LOGLEVEL": "info", + "MTX_LOGDESTINATIONS": "stdout", + "MTX_RTSPADDRESS": ":8554", + "MTX_RTMPADDRESS": ":1935", + "MTX_HLSADDRESS": ":8888", + "MTX_WEBRTCADDRESS": ":8889", + "MTX_SRTADDRESS": ":8890", + "MTX_APIADDRESS": ":9997", + "MTX_METRICSADDRESS": ":9998", + "MTX_AUTHINTERNALUSERS": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}", + "MTX_PATHDEFAULTS_RECORD": "${MTX_RECORD_ENABLED:-false}", + "MTX_PATHDEFAULTS_RECORDPATH": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f", + "MTX_PATHDEFAULTS_RECORDFORMAT": "fmp4" + }, "addPorts": [ { "containerPort": 8554, diff --git a/apps/nginx/docker-compose.json b/apps/nginx/docker-compose.json index 6cd866a..a0dfed5 100644 --- a/apps/nginx/docker-compose.json +++ b/apps/nginx/docker-compose.json @@ -29,16 +29,10 @@ "readOnly": true } ], - "environment": [ - { - "key": "NGINX_HOST", - "value": "${NGINX_SERVER_NAME:-localhost}" - }, - { - "key": "TZ", - "value": "${TZ:-Europe/Paris}" - } - ], + "environment": { + "NGINX_HOST": "${NGINX_SERVER_NAME:-localhost}", + "TZ": "${TZ:-Europe/Paris}" + }, "healthCheck": { "test": "curl --fail http://localhost:80 || exit 1", "interval": "30s", diff --git a/apps/wazuh-runtipi/docker-compose.json b/apps/wazuh-runtipi/docker-compose.json index 5da3f56..25cc73d 100644 --- a/apps/wazuh-runtipi/docker-compose.json +++ b/apps/wazuh-runtipi/docker-compose.json @@ -9,12 +9,9 @@ "sh", "/scripts/init-certs.sh" ], - "environment": [ - { - "key": "CERT_TOOL_VERSION", - "value": "4.14" - } - ], + "environment": { + "CERT_TOOL_VERSION": "4.14" + }, "volumes": [ { "hostPath": "${APP_DATA_DIR}/data/config", @@ -74,48 +71,18 @@ "condition": "service_healthy" } }, - "environment": [ - { - "key": "OPENSEARCH_JAVA_OPTS", - "value": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true" - }, - { - "key": "DISABLE_INSTALL_DEMO_CONFIG", - "value": "true" - }, - { - "key": "bootstrap.memory_lock", - "value": "true" - }, - { - "key": "network.host", - "value": "wazuh.indexer" - }, - { - "key": "node.name", - "value": "wazuh.indexer" - }, - { - "key": "cluster.initial_cluster_manager_nodes", - "value": "wazuh.indexer" - }, - { - "key": "node.max_local_storage_nodes", - "value": "1" - }, - { - "key": "plugins.security.allow_default_init_securityindex", - "value": "true" - }, - { - "key": "NODES_DN", - "value": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US" - }, - { - "key": "plugins.security.ssl.http.clientauth_mode", - "value": "OPTIONAL" - } - ], + "environment": { + "OPENSEARCH_JAVA_OPTS": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true", + "DISABLE_INSTALL_DEMO_CONFIG": "true", + "bootstrap.memory_lock": "true", + "network.host": "wazuh.indexer", + "node.name": "wazuh.indexer", + "cluster.initial_cluster_manager_nodes": "wazuh.indexer", + "node.max_local_storage_nodes": "1", + "plugins.security.allow_default_init_securityindex": "true", + "NODES_DN": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US", + "plugins.security.ssl.http.clientauth_mode": "OPTIONAL" + }, "ulimits": { "memlock": { "soft": -1, @@ -209,64 +176,22 @@ "condition": "service_healthy" } }, - "environment": [ - { - "key": "WAZUH_INDEXER_HOSTS", - "value": "wazuh.indexer:9200" - }, - { - "key": "WAZUH_NODE_NAME", - "value": "manager" - }, - { - "key": "WAZUH_NODE_TYPE", - "value": "master" - }, - { - "key": "WAZUH_CLUSTER_NODES", - "value": "wazuh.manager" - }, - { - "key": "WAZUH_CLUSTER_BIND_ADDR", - "value": "wazuh.manager" - }, - { - "key": "INDEXER_URL", - "value": "https://wazuh.indexer:9200" - }, - { - "key": "INDEXER_USERNAME", - "value": "${INDEXER_USERNAME:-admin}" - }, - { - "key": "INDEXER_PASSWORD", - "value": "${INDEXER_PASSWORD:-admin}" - }, - { - "key": "FILEBEAT_SSL_VERIFICATION_MODE", - "value": "full" - }, - { - "key": "SSL_CERTIFICATE_AUTHORITIES", - "value": "/var/ossec/etc/certs/root-ca.pem" - }, - { - "key": "SSL_CERTIFICATE", - "value": "/var/ossec/etc/certs/server.pem" - }, - { - "key": "SSL_KEY", - "value": "/var/ossec/etc/certs/server-key.pem" - }, - { - "key": "API_USERNAME", - "value": "wazuh-wui" - }, - { - "key": "API_PASSWORD", - "value": "${API_PASSWORD:-MyS3cr37P450r.*-}" - } - ], + "environment": { + "WAZUH_INDEXER_HOSTS": "wazuh.indexer:9200", + "WAZUH_NODE_NAME": "manager", + "WAZUH_NODE_TYPE": "master", + "WAZUH_CLUSTER_NODES": "wazuh.manager", + "WAZUH_CLUSTER_BIND_ADDR": "wazuh.manager", + "INDEXER_URL": "https://wazuh.indexer:9200", + "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}", + "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}", + "FILEBEAT_SSL_VERIFICATION_MODE": "full", + "SSL_CERTIFICATE_AUTHORITIES": "/var/ossec/etc/certs/root-ca.pem", + "SSL_CERTIFICATE": "/var/ossec/etc/certs/server.pem", + "SSL_KEY": "/var/ossec/etc/certs/server-key.pem", + "API_USERNAME": "wazuh-wui", + "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}" + }, "ulimits": { "memlock": { "soft": -1, @@ -368,60 +293,21 @@ "condition": "service_healthy" } }, - "environment": [ - { - "key": "SERVER_HOST", - "value": "0.0.0.0" - }, - { - "key": "OPENSEARCH_HOSTS", - "value": "https://wazuh.indexer:9200" - }, - { - "key": "SERVER_SSL_ENABLED", - "value": "true" - }, - { - "key": "INDEXER_USERNAME", - "value": "${INDEXER_USERNAME:-admin}" - }, - { - "key": "INDEXER_PASSWORD", - "value": "${INDEXER_PASSWORD:-admin}" - }, - { - "key": "WAZUH_API_URL", - "value": "https://wazuh.manager" - }, - { - "key": "DASHBOARD_USERNAME", - "value": "${DASHBOARD_USERNAME:-kibanaserver}" - }, - { - "key": "DASHBOARD_PASSWORD", - "value": "${DASHBOARD_PASSWORD:-kibanaserver}" - }, - { - "key": "API_USERNAME", - "value": "wazuh-wui" - }, - { - "key": "API_PASSWORD", - "value": "${API_PASSWORD:-MyS3cr37P450r.*-}" - }, - { - "key": "SERVER_SSL_CERTIFICATE", - "value": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem" - }, - { - "key": "SERVER_SSL_KEY", - "value": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem" - }, - { - "key": "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES", - "value": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem" - } - ], + "environment": { + "SERVER_HOST": "0.0.0.0", + "OPENSEARCH_HOSTS": "https://wazuh.indexer:9200", + "SERVER_SSL_ENABLED": "true", + "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}", + "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}", + "WAZUH_API_URL": "https://wazuh.manager", + "DASHBOARD_USERNAME": "${DASHBOARD_USERNAME:-kibanaserver}", + "DASHBOARD_PASSWORD": "${DASHBOARD_PASSWORD:-kibanaserver}", + "API_USERNAME": "wazuh-wui", + "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}", + "SERVER_SSL_CERTIFICATE": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem", + "SERVER_SSL_KEY": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem", + "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem" + }, "volumes": [ { "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", @@ -462,4 +348,4 @@ } } ] -} \ No newline at end of file +} From 0ded9c3a4d831c0f47737b8ab0f51956abcafdca Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 2 Sep 2026 17:27:56 +0200 Subject: [PATCH 3/3] fix(apps): preserve modern n8n compose format --- __tests__/apps.test.ts | 40 ++++++- apps/n8n-sandbox/docker-compose.json | 120 -------------------- apps/n8n-sandbox/docker-compose.yml | 159 +++++++++++++++++++++++++++ bun.lockb | Bin 9101 -> 9445 bytes package.json | 3 +- 5 files changed, 195 insertions(+), 127 deletions(-) delete mode 100644 apps/n8n-sandbox/docker-compose.json create mode 100644 apps/n8n-sandbox/docker-compose.yml diff --git a/__tests__/apps.test.ts b/__tests__/apps.test.ts index 4fd30c9..0f92ed1 100644 --- a/__tests__/apps.test.ts +++ b/__tests__/apps.test.ts @@ -3,6 +3,7 @@ import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas' import { fromError } from 'zod-validation-error'; import fs from 'node:fs' import path from 'node:path' +import YAML from 'yaml' const getApps = async () => { const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps')) @@ -29,7 +30,7 @@ describe("each app should have the required files", async () => { const apps = await getApps() for (const app of apps) { - const files = ['config.json', 'docker-compose.json', 'metadata/logo.jpg', 'metadata/description.md'] + const files = ['config.json', 'metadata/logo.jpg', 'metadata/description.md'] for (const file of files) { test(`app ${app} should have ${file}`, async () => { @@ -37,6 +38,12 @@ describe("each app should have the required files", async () => { expect(fileContent).not.toBeNull() }) } + + test(`app ${app} should have a compose file`, async () => { + const legacyCompose = await getFile(app, 'docker-compose.json') + const modernCompose = await getFile(app, 'docker-compose.yml') + expect(legacyCompose || modernCompose).not.toBeNull() + }) } }) @@ -58,17 +65,38 @@ describe("each app should have a valid config.json", async () => { } }) -describe("each app should have a valid docker-compose.json", async () => { +describe("modern compose files preserve runtime semantics", () => { + test("n8n-sandbox keeps its one-shot certificate service", async () => { + const fileContent = await getFile('n8n-sandbox', 'docker-compose.yml') + expect(fileContent).not.toBeNull() + + const parsed = YAML.parse(fileContent || '') + expect(parsed['x-runtipi']?.schema_version).toBe(2) + expect(parsed.services?.['sandbox-certs']?.restart).toBe('no') + expect(parsed.services?.['sandbox-api']?.['x-runtipi']?.is_main).toBe(true) + }) +}) + +describe("each app should have a valid compose file", async () => { const apps = await getApps() for (const app of apps) { - test(`app ${app} should have a valid docker-compose.json`, async () => { - const fileContent = await getFile(app, 'docker-compose.json') - const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}')) + test(`app ${app} should have a valid compose file`, async () => { + const legacyCompose = await getFile(app, 'docker-compose.json') + const modernCompose = await getFile(app, 'docker-compose.yml') + + if (modernCompose) { + const parsed = YAML.parse(modernCompose) + expect(parsed['x-runtipi']?.schema_version).toBeTypeOf('number') + expect(parsed.services).toBeTypeOf('object') + return + } + + const parsed = dynamicComposeSchema.safeParse(JSON.parse(legacyCompose || '{}')) if (!parsed.success) { const validationError = fromError(parsed.error); - console.error(`Error parsing docker-compose.json for app ${app}:`, validationError.toString()); + console.error(`Error parsing compose file for app ${app}:`, validationError.toString()); } expect(parsed.success).toBe(true) diff --git a/apps/n8n-sandbox/docker-compose.json b/apps/n8n-sandbox/docker-compose.json deleted file mode 100644 index d9947c0..0000000 --- a/apps/n8n-sandbox/docker-compose.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "services": [ - { - "name": "sandbox-certs", - "image": "n8nio/n8n-sandbox-service-api:1.3.0", - "command": [ - "bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api --control-san-prefix sandbox-runner && chown -R sandbox-api:sandbox-api /tls/api /var/lib/n8n-sandbox-api\n" - ], - "entrypoint": [ - "sh", - "-c" - ], - "environment": { - "NUM_RUNNERS": "1" - }, - "user": "0:0", - "volumes": [ - { - "hostPath": "${APP_DATA_DIR}/data/tls", - "containerPath": "/tls" - }, - { - "hostPath": "${APP_DATA_DIR}/data/api", - "containerPath": "/var/lib/n8n-sandbox-api" - } - ] - }, - { - "name": "sandbox-api", - "image": "n8nio/n8n-sandbox-service-api:1.3.0", - "isMain": true, - "internalPort": 8080, - "addToMainNetwork": true, - "environment": { - "SANDBOX_API_KEYS": "${SANDBOX_API_KEYS}", - "SANDBOX_API_RUNNER_REGISTRATION_TOKEN": "${SANDBOX_REGISTRATION_TOKEN}", - "SANDBOX_API_RUNNER_API_KEY": "${SANDBOX_RUNNER_KEY}", - "SANDBOX_API_GRPC_TLS_CERT_FILE": "/tls/api/grpc-server.crt", - "SANDBOX_API_GRPC_TLS_KEY_FILE": "/tls/api/grpc-server.key", - "SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE": "/tls/api/ca.crt", - "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE": "/tls/api/ca.crt", - "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE": "/tls/api/control-grpc-api-client.crt", - "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE": "/tls/api/control-grpc-api-client.key", - "SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME": "sandbox-runner-1" - }, - "volumes": [ - { - "hostPath": "${APP_DATA_DIR}/data/tls", - "containerPath": "/tls", - "readOnly": true - }, - { - "hostPath": "${APP_DATA_DIR}/data/api", - "containerPath": "/var/lib/n8n-sandbox-api" - } - ], - "healthCheck": { - "test": "wget -qO- http://localhost:8080/healthz", - "interval": "5s", - "timeout": "3s", - "retries": 5, - "startPeriod": "10s" - }, - "dependsOn": { - "sandbox-certs": { - "condition": "service_completed_successfully" - } - } - }, - { - "name": "sandbox-runner-1", - "image": "n8nio/n8n-sandbox-service-runner-dind:1.3.0", - "environment": { - "SANDBOX_RUNNER_API_KEYS": "${SANDBOX_RUNNER_KEY}", - "SANDBOX_RUNNER_REGISTRATION_TOKEN": "${SANDBOX_REGISTRATION_TOKEN}", - "SANDBOX_RUNNER_API_GRPC_ADDR": "sandbox-api:9090", - "SANDBOX_RUNNER_HTTP_BASE_URL": "http://sandbox-runner-1:8080", - "SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR": ":9091", - "SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR": "sandbox-runner-1:9091", - "SANDBOX_RUNNER_ID": "runner-1", - "SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE": "n8nio/n8n-sandbox-service-sandbox:1.3.0", - "SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE": "/tls/runner/ca.crt", - "SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE": "/tls/runner/grpc-client.crt", - "SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE": "/tls/runner/grpc-client.key", - "SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME": "sandbox-api", - "SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE": "/tls/runner/control-grpc-server.crt", - "SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE": "/tls/runner/control-grpc-server.key", - "SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE": "/tls/runner/ca.crt" - }, - "privileged": true, - "volumes": [ - { - "hostPath": "${APP_DATA_DIR}/data/tls", - "containerPath": "/tls", - "readOnly": true - }, - { - "hostPath": "${APP_DATA_DIR}/data/runner-state", - "containerPath": "/var/sandboxes" - }, - { - "hostPath": "${APP_DATA_DIR}/data/runner-docker", - "containerPath": "/var/lib/docker" - } - ], - "healthCheck": { - "test": "wget -qO- http://localhost:8080/readyz", - "interval": "5s", - "timeout": "5s", - "retries": 24, - "startPeriod": "20s" - }, - "dependsOn": { - "sandbox-api": { - "condition": "service_healthy" - } - } - } - ] -} diff --git a/apps/n8n-sandbox/docker-compose.yml b/apps/n8n-sandbox/docker-compose.yml new file mode 100644 index 0000000..9992c1c --- /dev/null +++ b/apps/n8n-sandbox/docker-compose.yml @@ -0,0 +1,159 @@ +# n8n Sandbox Service — app custom Runtipi (format docker-compose.yml + x-runtipi) +# +# Images : n8nio/n8n-sandbox-service-{api,runner-dind,sandbox}:1.3.0 +# (verifier au besoin : docker manifest inspect n8nio/n8n-sandbox-service-sandbox:1.3.0) +# +# Les 3 secrets ne sont PAS dans ce fichier : ce sont des form_fields du +# config.json (SANDBOX_API_KEYS, SANDBOX_REGISTRATION_TOKEN, SANDBOX_RUNNER_KEY), +# saisis/generes dans la GUI Runtipi a l'installation, modifiables ensuite +# dans les parametres de l'app, et injectes ici par ${...}. +# +# Cote app n8n (autre app Runtipi, doc "Set up AI Assistant", setup 2) : +# N8N_ENABLED_MODULES=instance-ai +# N8N_INSTANCE_AI_SANDBOX_ENABLED=true +# N8N_INSTANCE_AI_SANDBOX_PROVIDER=n8n-sandbox +# N8N_INSTANCE_AI_SANDBOX_IMAGE=n8nio/n8n-sandbox-service-sandbox:1.3.0 +# N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080 +# N8N_SANDBOX_SERVICE_API_KEY= +# Test depuis le conteneur n8n : wget -qO- http://sandbox-api:8080/healthz +# +# Les noms de services sandbox-api et sandbox-runner-1 sont fixes : ce sont +# les SAN des certificats. Si un nom ou NUM_RUNNERS change, supprimer +# ${APP_DATA_DIR}/data/tls (bootstrap-mtls.sh ne regenere rien tant que +# les fichiers existent). +# +# Donnees persistantes (toutes sous ${APP_DATA_DIR}/data, donc couvertes par +# les sauvegardes Runtipi — attention, runner-docker peut peser plusieurs Go) : +# tls/ certificats mTLS (CA + feuilles) +# api/ SQLite de l'API (etat des sandboxes) +# runner-state/ SQLite du runner +# runner-docker/ /var/lib/docker du DinD (cache de l'image sandbox) + +services: + sandbox-certs: + image: n8nio/n8n-sandbox-service-api:1.3.0 + # Obligatoire sous Runtipi : sans cette ligne le builder injecte + # restart: unless-stopped et ce job one-shot boucle en redemarrage. + restart: "no" + user: "0:0" + entrypoint: ["sh", "-c"] + # Genere les certs (idempotent) puis fixe les droits pour l'utilisateur + # sandbox-api de l'image : /tls/api et le repertoire SQLite de l'API + # (en bind mount, Docker cree le dossier en root sinon). + command: + - > + bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api + --control-san-prefix sandbox-runner && + chown -R sandbox-api:sandbox-api /tls/api /var/lib/n8n-sandbox-api + environment: + NUM_RUNNERS: "1" + volumes: + - ${APP_DATA_DIR}/data/tls:/tls + - ${APP_DATA_DIR}/data/api:/var/lib/n8n-sandbox-api + + sandbox-api: + image: n8nio/n8n-sandbox-service-api:1.3.0 + restart: unless-stopped + + depends_on: + sandbox-certs: + condition: service_completed_successfully + + environment: + SANDBOX_API_KEYS: "${SANDBOX_API_KEYS}" + SANDBOX_API_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}" + SANDBOX_API_RUNNER_API_KEY: "${SANDBOX_RUNNER_KEY}" + + SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt + SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key + SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt + + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key + SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1 + + volumes: + - ${APP_DATA_DIR}/data/tls:/tls:ro + # SANDBOX_API_DATA_DIR par defaut (SQLite). + - ${APP_DATA_DIR}/data/api:/var/lib/n8n-sandbox-api + + healthcheck: + # Forme chaine (et non tableau) : c'est la forme documentee par Runtipi + # et la seule acceptee par le formulaire de creation. + test: "wget -qO- http://localhost:8080/healthz" + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + + # is_main est OBLIGATOIRE : l'UI Runtipi refuse un compose sans + # exactement un service principal ("There must be exactly one main service"). + # Sans internal_port et sans cocher "open port" a l'installation, + # is_main ne publie aucun port et ne pose aucun label Traefik. + # Le service principal rejoint automatiquement tipi_main_network, ce qui + # rend sandbox-api:8080 joignable par les autres apps Runtipi (dont n8n), + # protege uniquement par SANDBOX_API_KEYS. Ne jamais publier 8080/9090. + x-runtipi: + is_main: true + add_to_main_network: true + + sandbox-runner-1: + image: n8nio/n8n-sandbox-service-runner-dind:1.3.0 + restart: unless-stopped + # Mode "dev/macOS" de l'upstream (qui attend sysbox-runc en production ; + # n8n recommande Daytona pour la prod). Equivalent root sur l'hote : + # ce service reste hors de tipi_main_network et ne publie aucun port. + privileged: true + + depends_on: + sandbox-api: + condition: service_healthy + + environment: + SANDBOX_RUNNER_API_KEYS: "${SANDBOX_RUNNER_KEY}" + SANDBOX_RUNNER_REGISTRATION_TOKEN: "${SANDBOX_REGISTRATION_TOKEN}" + + SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 + # http:// et non https:// : le mTLS ne couvre que le gRPC + # (enregistrement + SandboxControl). Le trafic proxy exec/files de + # l'API vers le runner reste en HTTP clair authentifie par X-Api-Key + # (docs/configuration.md du depot et compose officiel n8n). + SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080 + + SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091" + SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 + + SANDBOX_RUNNER_ID: runner-1 + + SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: n8nio/n8n-sandbox-service-sandbox:1.3.0 + + SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt + SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt + SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key + SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api + + SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt + SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key + SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt + + volumes: + - ${APP_DATA_DIR}/data/tls:/tls:ro + # Etat SQLite du runner (SANDBOX_RUNNER_DATA_DIR par defaut). + - ${APP_DATA_DIR}/data/runner-state:/var/sandboxes + # Docker interne du DinD : sans ce montage, l'image sandbox est + # re-telechargee a chaque recreation de l'app. Necessite un app-data + # sur ext4/xfs/btrfs (overlay2 ne fonctionne pas sur NFS). + - ${APP_DATA_DIR}/data/runner-docker:/var/lib/docker + + healthcheck: + # /readyz passe au vert une fois le runner enregistre aupres de l'API + # (meme check que le compose du depot upstream). + test: "wget -qO- http://localhost:8080/readyz" + interval: 5s + timeout: 5s + retries: 24 + start_period: 20s + +x-runtipi: + schema_version: 2 diff --git a/bun.lockb b/bun.lockb index a28598447958537019db5053a477e7276aeca197..f5a23fe4c9d2a0a0d0a0213fca8926f10ee1526e 100755 GIT binary patch delta 979 zcmeBmf9g3wPqW*Zb@uC0(fP7&8ZrDQ3d0nOn}2THzx~R&R)H&X{j5HQsxW|o_{4B| zCTsDD6%!OI6LWJwVjvlA28M=fYzz!MKw1jO=K|7^lNA}KbC~?%*&9A}W8CH)j3q3b z20(EppgiZ~hm6{j6__{}=T6pSvuAuXIg`zvRhgZE!EEwcCTrFtAj=xWVq7^{lij)= z$YlUwMj++@Vh{k)KzkX4K^!2U1r2hmCCC@l%3VSvVN1O+R0 zPclt@$}SIdB^Kvx-pTQW)!_#BL(|%4`DgCFoU>cu%3H6QrQ&h!(My-Fvej9enWC{> zGb#RoK{IE!cH8QtdCF<5N1Y#+1=QWyZhGl>kKUyp1(VQ+sr7im=PG4Ec~z(k(8OImzz~Qd7(hy<}ZR?ER!E_7z#Cj!VU#1#YL15i~G5|ihNS$P4I7C7udQlNMi0b*cUW8eg00U!pY z13n-Ir3ro@<^^IQAO@yG20tGf-smd2xR!n9pFTnS;>?Ce66s6Jnn%~I0q*aSfC(bv@nQMyKYmU@#Fiibd4Gq7S@bBIDSBU`(#3qKz zGueqvte7CF0uq@{p-E+DNrS(9=4<`ax1ER$>4Y#299-pOXq`WdLm zZ1P?vYgQw61_o;oi?MQYCA)P!P=W!38G#rm1qL7*Xf%Tmgi8~e323$m0|OHS<78b9 z`FfD`5>V?v6rG&~@hOOex(nhgRGfUzyZYkKn#pq23{ZrMGhYj1LK@Q5QsT}SOAC}#DN45EC3m$ z#0oM-cygeGzZl3cU`T@%Uw|rdkea+rLT&N`3BS!Al2;fRmra(HmSohNY%XoAG69s5 zfIvVRLPMONnO9trn3JPdoSIvfS~Pj0v^on&dh&MZLaAJ!n?Omh#lrLqV+G$o2dK`k QN}HWz*0F6?P(I2C0JOVq2><{9 diff --git a/package.json b/package.json index 925fe34..8dbf660 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "license": "ISC", "devDependencies": { "@types/bun": "latest", - "@types/node": "^22.14.1" + "@types/node": "^22.14.1", + "yaml": "2" }, "dependencies": { "@runtipi/common": "^0.8.0",