From 267f8b032154107171543a6a9f88d630f6fc81f8 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 2 Sep 2026 10:53:17 +0000 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20vers=20?= =?UTF-8?q?"apps"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/config.json | 50 +++++++++++++ apps/description.md | 49 +++++++++++++ apps/docker-compose.yml | 159 ++++++++++++++++++++++++++++++++++++++++ apps/logo.jpg | Bin 0 -> 22314 bytes 4 files changed, 258 insertions(+) create mode 100644 apps/config.json create mode 100644 apps/description.md create mode 100644 apps/docker-compose.yml create mode 100644 apps/logo.jpg diff --git a/apps/config.json b/apps/config.json new file mode 100644 index 0000000..862594a --- /dev/null +++ b/apps/config.json @@ -0,0 +1,50 @@ +{ + "name": "n8n Sandbox Service", + "id": "n8n-sandbox", + "available": true, + "exposable": false, + "no_gui": true, + "dynamic_config": true, + "port": 8080, + "tipi_version": 1, + "min_tipi_version": "4.7.0", + "version": "1.3.0", + "author": "n8n", + "source": "https://github.com/n8n-io/n8n-sandbox-service", + "website": "https://docs.n8n.io/deploy/host-n8n/configure-n8n/set-up-ai-assistant", + "categories": ["automation", "development"], + "short_desc": "Sandbox d'execution de code pour l'AI Assistant de n8n (api + runner Docker-in-Docker).", + "description": "Stack n8n Sandbox Service (sandbox-certs, sandbox-api, sandbox-runner-1) en mTLS. Aucun port publie : n8n joint sandbox-api:8080 via tipi_main_network. Le runner tourne en privileged (Docker-in-Docker).", + "supported_architectures": ["amd64", "arm64"], + "form_fields": [ + { + "type": "random", + "encoding": "hex", + "min": 48, + "label": "Cle API sandbox (SANDBOX_API_KEYS)", + "hint": "A recopier dans l'app n8n : N8N_SANDBOX_SERVICE_API_KEY. Plusieurs cles possibles, separees par des virgules.", + "required": true, + "env_variable": "SANDBOX_API_KEYS" + }, + { + "type": "random", + "encoding": "hex", + "min": 48, + "label": "Jeton d'enregistrement du runner (SANDBOX_REGISTRATION_TOKEN)", + "hint": "Secret interne api <-> runner. Rien a recopier ailleurs.", + "required": true, + "env_variable": "SANDBOX_REGISTRATION_TOKEN" + }, + { + "type": "random", + "encoding": "hex", + "min": 48, + "label": "Cle API interne du runner (SANDBOX_RUNNER_KEY)", + "hint": "Secret interne api <-> runner. Rien a recopier ailleurs.", + "required": true, + "env_variable": "SANDBOX_RUNNER_KEY" + } + ], + "created_at": 1788345796000, + "updated_at": 1788345796000 +} diff --git a/apps/description.md b/apps/description.md new file mode 100644 index 0000000..022b1cb --- /dev/null +++ b/apps/description.md @@ -0,0 +1,49 @@ +# n8n Sandbox Service + +Sandbox auto-hébergé pour l'**AI Assistant** de n8n (setup « Self-host the sandbox manually » de la doc n8n). L'app déploie les trois services de la stack officielle : + +| Service | Rôle | +|---|---| +| `sandbox-certs` | Job one-shot : génère la CA privée et les certificats mTLS, puis s'arrête. | +| `sandbox-api` | Point d'entrée HTTP (`:8080`) que n8n appelle pour exécuter du code. | +| `sandbox-runner-1` | Docker-in-Docker **privileged** : crée et exécute les conteneurs sandbox. | + +## Après l'installation + +Dans l'app **n8n** (paramètres ou `app.env`), ajouter : + +``` +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= +``` + +Puis redémarrer n8n et vérifier depuis son conteneur : + +``` +wget -qO- http://sandbox-api:8080/healthz # {"status":"ok"} +``` + +## Données persistantes + +Tout est sous `app-data//n8n-sandbox/data/` : + +- `tls/` : certificats mTLS (contient la clé de la CA, à traiter comme un secret) +- `api/` : base SQLite de l'API +- `runner-state/` : base SQLite du runner +- `runner-docker/` : `/var/lib/docker` du DinD (cache de l'image sandbox, peut peser plusieurs Go ; vidable sans perte) + +## Sécurité + +- Aucun port n'est publié sur l'hôte. `sandbox-api:8080` est joignable par les autres apps Runtipi via `tipi_main_network`, protégé uniquement par la clé API. +- Le runner est `privileged` : équivalent root sur l'hôte. Ne jamais l'exposer. +- n8n recommande cette stack pour le développement/test et Daytona pour la production. + +## Notes + +- Les noms `sandbox-api` et `sandbox-runner-1` sont les SAN des certificats : ne pas les renommer. +- Les certificats ne se renouvellent pas seuls. Pour les régénérer, supprimer `data/tls/` et redémarrer l'app. +- L'image sandbox est téléchargée par le runner au premier usage. diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml new file mode 100644 index 0000000..9992c1c --- /dev/null +++ b/apps/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/apps/logo.jpg b/apps/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..925d0d19248d5b938704d158124a130476a5ec47 GIT binary patch literal 22314 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 literal 0 HcmV?d00001