# Architecture Research: Unraid GraphQL API Integration **Domain:** n8n workflow system migration from Docker socket proxy to Unraid GraphQL API **Researched:** 2026-02-09 **Confidence:** HIGH ## Integration Architecture Overview ### Current Architecture (Docker Socket Proxy) ``` ┌─────────────────────────────────────────────────────────────┐ │ n8n Workflow System │ ├─────────────────────────────────────────────────────────────┤ │ Main Workflow (169 nodes) │ │ ├─── Execute Workflow → Update Sub-workflow │ │ ├─── Execute Workflow → Actions Sub-workflow │ │ ├─── Execute Workflow → Logs Sub-workflow │ │ ├─── Execute Workflow → Status Sub-workflow │ │ └─── Execute Workflow → Matching Sub-workflow │ │ │ │ Sub-workflows call Docker API: │ │ HTTP Request → docker-socket-proxy:2375/v1.47/... │ │ Execute Command → curl docker-socket-proxy:2375/... │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ docker-socket-proxy (tecnativa/...) │ │ Security layer: Exposes only safe endpoints │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Docker Engine │ │ /var/run/docker.sock │ └─────────────────────────────────────────────────────────────┘ ``` ### Target Architecture (Unraid GraphQL API) ``` ┌─────────────────────────────────────────────────────────────┐ │ n8n Workflow System │ ├─────────────────────────────────────────────────────────────┤ │ Main Workflow (169 nodes) │ │ ├─── Execute Workflow → Update Sub-workflow │ │ ├─── Execute Workflow → Actions Sub-workflow │ │ ├─── Execute Workflow → Logs Sub-workflow │ │ ├─── Execute Workflow → Status Sub-workflow │ │ └─── Execute Workflow → Matching Sub-workflow │ │ │ │ Sub-workflows call Unraid GraphQL API: │ │ HTTP Request → UNRAID_HOST/graphql (POST) │ │ Header Auth: x-api-key │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Unraid GraphQL API Endpoint │ │ https://192-168-x-x.hash.myunraid.net:8443/graphql │ │ Authentication: x-api-key header │ │ Permission: DOCKER:UPDATE_ANY │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Unraid Docker Service │ │ Native container management + status sync │ └─────────────────────────────────────────────────────────────┘ ``` **Key architectural change:** Single unified API replaces two-layer proxy system. Unraid GraphQL API provides both Docker operations AND native Unraid integration (update status sync). ## Component Modification Map ### Sub-workflows Requiring Modification | Sub-workflow | Docker API Nodes | Operations | Complexity | Priority | |--------------|------------------|------------|------------|----------| | **n8n-update.json** | 9 nodes | Pull, stop, remove, create, start, inspect, cleanup | HIGH | 1 | | **n8n-actions.json** | 4 nodes | List, start, stop, restart | LOW | 2 | | **n8n-status.json** | 3 nodes | List, inspect | LOW | 3 | | **n8n-logs.json** | 2 nodes (Execute Command) | Container logs retrieval | LOW | 4 | | **n8n-matching.json** | 0 nodes | Pure data transformation | NONE | N/A | | **n8n-batch-ui.json** | 5 nodes (inherited) | Calls Update/Actions | NONE | N/A | | **n8n-confirmation.json** | 0 nodes | Pure UI logic | NONE | N/A | **Total impact:** 4 sub-workflows modified (18 Docker API nodes), 3 sub-workflows unchanged ### Sub-workflows NOT Requiring Modification - **n8n-matching.json** — Container name matching logic operates on data shape, not API source - **n8n-batch-ui.json** — Pure Telegram keyboard UI, no direct Docker API calls - **n8n-confirmation.json** — Confirmation dialogs, no Docker operations These workflows receive transformed data from modified workflows and operate on contracts (input/output shapes) that remain stable. ## Node Type Changes ### HTTP Request Node Pattern (Actions, Status) **Before (Docker API):** ```javascript // n8n HTTP Request node { method: 'POST', url: 'http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/start', responseFormat: 'json' } ``` **After (Unraid GraphQL API):** ```javascript // n8n HTTP Request node { method: 'POST', url: '={{ $env.UNRAID_HOST }}/graphql', authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', credential: 'Unraid API Key', // Header Auth: x-api-key sendBody: true, bodyContentType: 'json', body: { query: 'mutation { docker { start(id: "{{ $json.unraidContainerId }}") { id state } } }' }, options: { ignoreSSLIssues: true } } ``` **Node type:** Same (HTTP Request), but parameters change **Count:** ~15 HTTP Request nodes across Actions, Status, Update workflows ### Execute Command → HTTP Request (Logs, Update) **Before (Execute Command with curl):** ```javascript // n8n Execute Command node (Pull Image) { command: '=curl -s --max-time 600 -X POST \'http://docker-socket-proxy:2375/v1.47/images/create?fromImage={{ encodeURIComponent($json.imageName) }}\' | tail -c 10000' } ``` **After (HTTP Request with GraphQL mutation):** ```javascript // n8n HTTP Request node { method: 'POST', url: '={{ $env.UNRAID_HOST }}/graphql', authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', credential: 'Unraid API Key', sendBody: true, bodyContentType: 'json', body: { query: 'mutation { docker { updateContainer(id: "{{ $json.unraidContainerId }}") { id imageId state } } }' }, timeout: 600000 // 10 minutes } ``` **Node type change:** Execute Command → HTTP Request **Count:** 3 Execute Command nodes (1 in Update for image pull, 2 in Logs for log retrieval) **Impact:** Simpler — no shell escaping, native n8n timeout handling, structured GraphQL errors ## Data Transformation Requirements ### Container ID Mapping **Docker API format:** - Container ID: 64-character hex string (`8a9907a245766012...`) - Container names: Array with `/` prefix (`["/n8n"]`) **Unraid GraphQL format:** - Container ID: `PrefixedID` scalar — `{server_hash}:{container_hash}` (128 chars + colon) - Example: `1639d2f04f44841b...a558d85071fa23e0:8a9907a245766012...840cefdec67af6b7` - Container names: Array WITH `/` prefix (`["/n8n"]`) — GraphQL returns Docker's raw value **Mapping strategy:** 1. **Container matching (Matching sub-workflow):** - User input: "n8n" (plain name) - Docker API query: Returns names `["/n8n"]` → strip `/` for comparison - Unraid GraphQL query: Returns names `["/n8n"]` → strip `/` for comparison - **No change needed** — name matching logic already strips `/` prefix 2. **Container identification:** - Docker API: Use Docker container ID (64 chars) - Unraid GraphQL: Use Unraid PrefixedID (128+ chars) - **Strategy:** Store Unraid ID from initial query, pass through workflow - **Where:** Matching sub-workflow output, Update/Actions/Status inputs 3. **Workflow contract stability:** - Field name stays `containerId` throughout all workflows - Value changes from Docker ID → Unraid PrefixedID - No contract changes needed — ID is opaque token to workflows ### Response Shape Transformation **Docker API response (GET /containers/json):** ```json [ { "Id": "8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925", "Names": ["/n8n"], "Image": "n8nio/n8n:latest", "ImageID": "sha256:abc123...", "State": "running", "Status": "Up 2 hours" } ] ``` **Unraid GraphQL response:** ```json { "data": { "docker": { "containers": [ { "id": "1639d2f04f44841b...a558d85071fa23e0:8a9907a245766012...840cefdec67af6b7", "names": ["/n8n"], "image": "n8nio/n8n:latest", "imageId": "sha256:abc123...", "state": "RUNNING", "status": "Up 2 hours", "isUpdateAvailable": false } ] } } } ``` **Key differences:** | Field | Docker API | Unraid GraphQL | Impact | |-------|------------|----------------|--------| | Container ID | `Id` | `id` | Lowercase (minor) | | ID format | 64 hex chars | PrefixedID (128+) | Length check updates | | Names | `Names` | `names` | Lowercase (minor) | | Image ID | `ImageID` | `imageId` | Lowercase (minor) | | State | `"running"` (lowercase) | `"RUNNING"` (uppercase) | Case handling in comparisons | | Status | `Status` | `status` | Lowercase (minor) | | Update available | Not available | `isUpdateAvailable` | NEW field (benefit!) | **Transformation pattern:** Add a **"Normalize Container Data"** Code node after Unraid GraphQL queries to map to workflow's expected shape: ```javascript // Code node: Normalize Unraid GraphQL Response const graphqlResponse = $input.item.json; // Check for GraphQL errors first if (graphqlResponse.errors) { throw new Error(`GraphQL error: ${graphqlResponse.errors.map(e => e.message).join(', ')}`); } // Extract containers from GraphQL response structure const containers = graphqlResponse.data?.docker?.containers || []; // Map to workflow's expected format return containers.map(c => ({ json: { containerId: c.id, // Unraid PrefixedID containerName: (c.names?.[0] || '').replace(/^\//, ''), // Strip leading / imageName: c.image, imageId: c.imageId, state: c.state.toLowerCase(), // RUNNING → running (for existing comparisons) status: c.status, updateAvailable: c.isUpdateAvailable || false // NEW } })); ``` **Where to add:** After every `docker.containers` GraphQL query in: - Status sub-workflow (container list query) - Matching sub-workflow (container list for matching) - Actions sub-workflow (container list before action) ## Integration Patterns ### Pattern 1: Direct Mutation (Simple Operations) **Use for:** Start, stop, restart, pause (single-step operations) **Before (Docker API):** ```javascript // HTTP Request node: POST /containers/{id}/start // Response: HTTP 204 No Content (empty body) // Success check: response.statusCode === 204 ``` **After (Unraid GraphQL):** ```javascript // HTTP Request node: POST /graphql // Body: { query: 'mutation { docker { start(id: "...") { id state } } }' } // Response: { data: { docker: { start: { id: "...", state: "RUNNING" } } } } // Success check: response.data?.docker?.start?.state === "RUNNING" ``` **Error handling:** - Docker API: HTTP status codes (404 = not found, 304 = already running) - Unraid GraphQL: `response.errors[]` array with structured error messages - **New pattern:** Check `response.errors` first, then validate `response.data` structure **Example implementation (Actions sub-workflow):** ```javascript // Code node: Validate Start Result const response = $input.item.json; // Check GraphQL errors if (response.errors) { const errorMsg = response.errors.map(e => e.message).join(', '); return { json: { success: false, message: `Start failed: ${errorMsg}`, action: 'start' } }; } // Validate response structure const startResult = response.data?.docker?.start; if (!startResult) { return { json: { success: false, message: 'Invalid GraphQL response: missing start result', action: 'start' } }; } // Success return { json: { success: true, action: 'start', containerId: startResult.id, state: startResult.state, message: 'Container started successfully' } }; ``` ### Pattern 2: Complex Multi-Step Operations (Update Workflow) **Docker API approach (9 nodes):** 1. Get container list → Find container 2. Inspect container → Extract config 3. Pull image (Execute Command with curl) 4. Inspect new image → Get digest 5. Stop container 6. Remove container 7. Create container (with old config) 8. Start container 9. Remove old image **Unraid GraphQL approach (1-2 nodes):** 1. Call `updateContainer` mutation → Unraid handles all steps atomically 2. (Optional) Query container status after update **Simplification:** ```javascript // Single HTTP Request node: Update Container { method: 'POST', url: '={{ $env.UNRAID_HOST }}/graphql', authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', credential: 'Unraid API Key', sendBody: true, bodyContentType: 'json', body: { query: 'mutation { docker { updateContainer(id: "{{ $json.containerId }}") { id imageId state isUpdateAvailable } } }' }, timeout: 600000 // 10 minutes (pull can be slow) } // Code node: Validate Update Result const response = $input.item.json; if (response.errors) { const errorMsg = response.errors.map(e => e.message).join(', '); return { json: { success: false, updated: false, message: `Update failed: ${errorMsg}` } }; } const updateResult = response.data?.docker?.updateContainer; if (!updateResult) { return { json: { success: false, updated: false, message: 'Invalid GraphQL response' } }; } // Check if update was needed if (updateResult.isUpdateAvailable === false) { return { json: { success: true, updated: false, message: 'Container is already up to date' } }; } // Success return { json: { success: true, updated: true, message: `Container updated successfully`, newImageId: updateResult.imageId, state: updateResult.state } }; ``` **Impact:** Update sub-workflow shrinks from 34 nodes to ~15-20 nodes (remove 9 Docker API nodes, add 1-2 GraphQL nodes + data normalization) **Benefit:** Unraid's `updateContainer` handles image pull, container stop/remove/create/start atomically AND syncs update status internally (solves v1.3's "apply update" badge issue automatically). ### Pattern 3: Log Retrieval (Logs Sub-workflow) **Before (Execute Command):** ```javascript // Execute Command node: Get Logs { command: '=curl -s "http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/logs?stdout=true&stderr=true&tail={{ $json.lineCount || 50 }}"' } ``` **After (Unraid GraphQL):** ```javascript // HTTP Request node: Query Logs { method: 'POST', url: '={{ $env.UNRAID_HOST }}/graphql', authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', credential: 'Unraid API Key', sendBody: true, bodyContentType: 'json', body: { query: 'query { docker { logs(id: "{{ $json.containerId }}", tail: {{ $json.lineCount || 50 }}) { entries } } }' } } // Code node: Format Logs const response = $input.item.json; if (response.errors) { throw new Error(`Log retrieval failed: ${response.errors.map(e => e.message).join(', ')}`); } const logData = response.data?.docker?.logs; if (!logData) { throw new Error('Invalid GraphQL response: missing logs'); } const logs = logData.entries || []; return { json: { success: true, logs: logs.join('\n'), lineCount: logs.length } }; ``` **Benefit:** Native GraphQL logs query returns structured data (array of log entries) instead of raw Docker log format with control characters. Simpler parsing. ## Modified vs. New Components ### Modified Components | Component | File | Modification Type | Node Count Change | Reason | |-----------|------|-------------------|-------------------|--------| | Container List Query | n8n-status.json | Replace HTTP Request URL + add normalization node | 3 → 4 nodes | GraphQL endpoint + response shape | | Container Actions | n8n-actions.json | Replace HTTP Request URLs + update mutation body | 4 → 5 nodes | GraphQL mutations + error handling | | Container Update | n8n-update.json | Replace 9 Docker API nodes with 2 GraphQL nodes | 34 → 27 nodes | Unraid atomic update | | Container Logs | n8n-logs.json | Replace Execute Command with HTTP Request | 2 → 3 nodes | Native logs query + formatting | | Container Matching | n8n-matching.json | Add normalization node after list query | 23 → 24 nodes | Data shape consistency | **Total node count change:** 290 nodes → ~284 nodes (net reduction of ~6 nodes) ### New Components | Component | Purpose | Where Added | Why New | |-----------|---------|-------------|---------| | Normalize Container Data | Map GraphQL response to workflow contract | All modified sub-workflows | Data shape consistency | | GraphQL Error Handler | Validate `response.errors` array | Inline in validation Code nodes | GraphQL-specific error handling | | Container ID Resolver | Map container name → Unraid PrefixedID | n8n-matching.json | ID format difference | **Pattern:** Each modified sub-workflow gains 1-2 new Code nodes for normalization/validation. ## Suggested Build Order ### Phase 1: Infrastructure (Low Risk) — Already Complete 1. **Credential setup** — Completed in Phase 14 - `.env.unraid-api` file - n8n Header Auth credential - Test query validated - Network connectivity proven (myunraid.net relay) 2. **Data normalization library** — Create reusable template - Build "Normalize Unraid Response" Code node - Test with Phase 14's test query - Template ready for copy-paste into sub-workflows ### Phase 2: Simple Sub-workflows (Low Complexity) **Order:** Actions → Status → Logs → Matching 3. **Actions sub-workflow** (4 Docker API nodes → 5 GraphQL nodes) - Start/stop/restart mutations - Direct 1:1 mapping - Add normalization + error handling - Test: `start `, `stop `, `restart ` 4. **Status sub-workflow** (3 Docker API nodes → 4 GraphQL nodes) - Container list query - Single container query - Add normalization - Test: `status`, tap container in list 5. **Logs sub-workflow** (2 Execute Command → 3 HTTP Request) - Logs query with tail parameter - Add formatting Code node - Test: `logs ` 6. **Matching sub-workflow** (0 API nodes → 1 normalization node) - Container list source changes (inherited from Status) - Add normalization if not already present - Test: All name matching patterns ### Phase 3: Complex Sub-workflow (High Impact) 7. **Update sub-workflow** (9 Docker API nodes → 2 GraphQL nodes) - Replace multi-step Docker API flow with single `updateContainer` mutation - Unraid handles atomic update AND status sync - Validate success/failure cases - Test: `update `, verify image pull + recreate + status cleared ### Phase 4: Dependency Cleanup (Post-Integration) 8. **Remove docker-socket-proxy dependency** - Update Docker Compose / Unraid template - Remove container from deployment - Remove volume mount for Docker socket - Update architecture docs ### Phase 5: Validation 9. **Full workflow testing** - All text commands (status, start, stop, restart, update, logs) - All inline keyboard flows (tap container → action) - Batch operations (stop multiple, update all) - Error cases (invalid container, network failure, API errors) - Verify Unraid update badges clear automatically after bot updates **Rationale:** Start simple (Actions = 4 nodes, low risk), build confidence, tackle complex Update workflow last when patterns are proven. ## What NOT to Change ### Stable Components (DO NOT MODIFY) 1. **Main workflow structure** — Keyword Router, Auth IF, Correlation ID generator - Reason: No Docker API calls, pure orchestration logic 2. **Sub-workflow contracts** — Input/output shapes remain the same - Example: Update sub-workflow still receives `{ containerId, containerName, chatId, messageId, responseMode }` - Reason: Preserves integration points with main workflow - Note: `containerId` value changes (Docker ID → Unraid PrefixedID) but field name/type stable 3. **Telegram response nodes** — All "Send Message" and "Edit Message" nodes - Reason: User-facing messages, no Docker/Unraid API dependency 4. **Matching logic algorithm** — Container name matching strategy - Reason: Operates on container names array, agnostic to API source - Note: Data SOURCE changes, algorithm does not 5. **Batch UI logic** — Selection keyboard, bitmap encoding - Reason: Pure UI state management, no Docker operations 6. **Confirmation dialogs** — Stop/update confirmation flows - Reason: Pure UI logic, orchestration only 7. **Error logging patterns** — Structured error returns, correlation IDs - Reason: API-agnostic observability infrastructure ### Anti-Patterns to Avoid - **Do NOT change sub-workflow contracts** — Main workflow dispatch nodes depend on stable input shapes - **Do NOT mix Docker API and Unraid GraphQL** in same workflow — Pick one, commit fully, test, ship - **Do NOT remove correlation IDs** — Observability is critical for debugging GraphQL errors - **Do NOT change Telegram message formats** — User-facing stability matters - **Do NOT optimize prematurely** — Get it working with Unraid GraphQL first, optimize later - **Do NOT skip data normalization** — Consistent data shapes prevent cascading failures ## Risk Assessment | Component | Risk Level | Mitigation | |-----------|------------|------------| | Actions sub-workflow | LOW | Simple 1:1 mutation mapping, test early | | Status sub-workflow | LOW | Query-only, add normalization layer | | Logs sub-workflow | LOW | GraphQL logs query is simpler than Docker API | | Update sub-workflow | MEDIUM | Complex flow → atomic mutation (big simplification but needs thorough testing) | | Container ID mapping | MEDIUM | ID format difference requires careful validation | | Matching sub-workflow | LOW | No API changes, only data source changes | | Main workflow | NONE | No modifications needed | | docker-socket-proxy removal | LOW | Remove after all sub-workflows migrated, test thoroughly | **Overall risk:** LOW-MEDIUM — Most changes are direct API replacements with simpler GraphQL equivalents. Update workflow is the only complex migration but Unraid's atomic operation actually simplifies the flow. ## Key Benefits of Migration 1. **Simplified Update Flow** — 9 Docker API nodes → 2 GraphQL nodes (atomic operation) 2. **Automatic Status Sync** — Unraid update badges clear automatically (solves v1.3's manual sync issue) 3. **Better Error Messages** — Structured GraphQL errors vs. HTTP status codes 4. **Native Integration** — Direct Unraid API vs. Docker proxy layer 5. **Update Detection** — `isUpdateAvailable` field enables proactive notifications 6. **Simpler Log Parsing** — Structured entries vs. raw Docker log format with control characters 7. **Security Improvement** — API key with granular permissions vs. Docker socket proxy 8. **Fewer Dependencies** — Remove docker-socket-proxy container from deployment ## Sources - [Unraid GraphQL Schema](https://raw.githubusercontent.com/unraid/api/main/api/generated-schema.graphql) — Complete API specification (HIGH confidence) - `.planning/phases/14-unraid-api-access/14-RESEARCH.md` — Phase 14 connectivity research (HIGH confidence) - `ARCHITECTURE.md` — Existing workflow architecture, contracts (HIGH confidence) - `n8n-workflow.json`, `n8n-*.json` — Actual workflow implementations (HIGH confidence) - [n8n GraphQL Node Documentation](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.graphql/) — HTTP Request patterns (HIGH confidence) - [Docker API Documentation](https://docs.docker.com/engine/api/v1.47/) — Current Docker API patterns (HIGH confidence) --- *Architecture research for: Unraid Docker Manager v1.4 — Unraid API Native* *Researched: 2026-02-09* *Confidence: HIGH — Unraid GraphQL schema verified, Phase 14 connectivity proven, workflow structure analyzed*