26 KiB
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):
// n8n HTTP Request node
{
method: 'POST',
url: 'http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/start',
responseFormat: 'json'
}
After (Unraid GraphQL API):
// 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):
// 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):
// 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:
PrefixedIDscalar —{server_hash}:{container_hash}(128 chars + colon)- Example:
1639d2f04f44841b...a558d85071fa23e0:8a9907a245766012...840cefdec67af6b7
- Example:
- Container names: Array WITH
/prefix (["/n8n"]) — GraphQL returns Docker's raw value
Mapping strategy:
-
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
-
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
-
Workflow contract stability:
- Field name stays
containerIdthroughout all workflows - Value changes from Docker ID → Unraid PrefixedID
- No contract changes needed — ID is opaque token to workflows
- Field name stays
Response Shape Transformation
Docker API response (GET /containers/json):
[
{
"Id": "8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
"Names": ["/n8n"],
"Image": "n8nio/n8n:latest",
"ImageID": "sha256:abc123...",
"State": "running",
"Status": "Up 2 hours"
}
]
Unraid GraphQL response:
{
"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:
// 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):
// HTTP Request node: POST /containers/{id}/start
// Response: HTTP 204 No Content (empty body)
// Success check: response.statusCode === 204
After (Unraid GraphQL):
// 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.errorsfirst, then validateresponse.datastructure
Example implementation (Actions sub-workflow):
// 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):
- Get container list → Find container
- Inspect container → Extract config
- Pull image (Execute Command with curl)
- Inspect new image → Get digest
- Stop container
- Remove container
- Create container (with old config)
- Start container
- Remove old image
Unraid GraphQL approach (1-2 nodes):
- Call
updateContainermutation → Unraid handles all steps atomically - (Optional) Query container status after update
Simplification:
// 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):
// 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):
// 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
-
Credential setup — Completed in Phase 14
.env.unraid-apifile- n8n Header Auth credential
- Test query validated
- Network connectivity proven (myunraid.net relay)
-
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
-
Actions sub-workflow (4 Docker API nodes → 5 GraphQL nodes)
- Start/stop/restart mutations
- Direct 1:1 mapping
- Add normalization + error handling
- Test:
start <container>,stop <container>,restart <container>
-
Status sub-workflow (3 Docker API nodes → 4 GraphQL nodes)
- Container list query
- Single container query
- Add normalization
- Test:
status, tap container in list
-
Logs sub-workflow (2 Execute Command → 3 HTTP Request)
- Logs query with tail parameter
- Add formatting Code node
- Test:
logs <container>
-
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)
- Update sub-workflow (9 Docker API nodes → 2 GraphQL nodes)
- Replace multi-step Docker API flow with single
updateContainermutation - Unraid handles atomic update AND status sync
- Validate success/failure cases
- Test:
update <container>, verify image pull + recreate + status cleared
- Replace multi-step Docker API flow with single
Phase 4: Dependency Cleanup (Post-Integration)
- 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
- 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)
-
Main workflow structure — Keyword Router, Auth IF, Correlation ID generator
- Reason: No Docker API calls, pure orchestration logic
-
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:
containerIdvalue changes (Docker ID → Unraid PrefixedID) but field name/type stable
- Example: Update sub-workflow still receives
-
Telegram response nodes — All "Send Message" and "Edit Message" nodes
- Reason: User-facing messages, no Docker/Unraid API dependency
-
Matching logic algorithm — Container name matching strategy
- Reason: Operates on container names array, agnostic to API source
- Note: Data SOURCE changes, algorithm does not
-
Batch UI logic — Selection keyboard, bitmap encoding
- Reason: Pure UI state management, no Docker operations
-
Confirmation dialogs — Stop/update confirmation flows
- Reason: Pure UI logic, orchestration only
-
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
- Simplified Update Flow — 9 Docker API nodes → 2 GraphQL nodes (atomic operation)
- Automatic Status Sync — Unraid update badges clear automatically (solves v1.3's manual sync issue)
- Better Error Messages — Structured GraphQL errors vs. HTTP status codes
- Native Integration — Direct Unraid API vs. Docker proxy layer
- Update Detection —
isUpdateAvailablefield enables proactive notifications - Simpler Log Parsing — Structured entries vs. raw Docker log format with control characters
- Security Improvement — API key with granular permissions vs. Docker socket proxy
- Fewer Dependencies — Remove docker-socket-proxy container from deployment
Sources
- Unraid GraphQL Schema — 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 — HTTP Request patterns (HIGH confidence)
- Docker API Documentation — 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