Files
unraid-docker-manager/.planning/research/ARCHITECTURE.md
2026-02-09 08:08:25 -05:00

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: 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):

[
  {
    "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.errors first, then validate response.data structure

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):

  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:

// 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

  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

  1. 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>
  2. Status sub-workflow (3 Docker API nodes → 4 GraphQL nodes)

    • Container list query
    • Single container query
    • Add normalization
    • Test: status, tap container in list
  3. Logs sub-workflow (2 Execute Command → 3 HTTP Request)

    • Logs query with tail parameter
    • Add formatting Code node
    • Test: logs <container>
  4. 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)

  1. 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 <container>, verify image pull + recreate + status cleared

Phase 4: Dependency Cleanup (Post-Integration)

  1. 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

  1. 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 DetectionisUpdateAvailable 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 — 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