Files
2026-02-09 08:08:25 -05:00

23 KiB
Raw Permalink Blame History

Technology Stack — Unraid GraphQL API Migration

Project: Unraid Docker Manager v1.4 Researched: 2026-02-09 Overall confidence: HIGH

Executive Summary

Migration from Docker socket proxy to Unraid GraphQL API requires minimal stack additions and no new dependencies. All required container operations (status, start, stop, update) are available via GraphQL mutations except logs, which are NOT available via the Unraid API and must continue using Docker socket proxy.

This creates a hybrid architecture: Unraid GraphQL API for control operations, Docker socket proxy retained ONLY for logs retrieval.

Critical findings:

  1. Unraid GraphQL API provides mutations for start, stop, pause/unpause, update (single + batch)
  2. NO restart mutation — must chain stop + start operations
  3. NO logs query — Unraid API documentation explicitly states "Container output logs are NOT accessible via API and you must use docker logs via SSH"
  4. Container list query returns same data as Docker API but with different field formats (UPPERCASE state, /-prefixed names)
  5. All mutations verified working on live Unraid 7.2 server via testing

Keep (Unchanged)

Technology Version Purpose Why
n8n Current Workflow orchestration Already running, no changes needed
Telegram Bot API Current User interface Already integrated, no changes needed
Unraid GraphQL API 7.2+ Container control operations Already connected in v1.3, expand usage
myunraid.net cloud relay Current Unraid API access Verified working, no SSL issues
n8n Header Auth credential Current API key authentication Already configured for GraphQL
Environment variable auth Current UNRAID_HOST + UNRAID_API_KEY Already set on n8n container

Retain (Scope Reduced)

Technology Version Purpose Why Keep
docker-socket-proxy Current Log retrieval ONLY Logs not available via Unraid API

Critical: Docker socket proxy must remain deployed but reconfigured with minimal permissions (only CONTAINERS=1, remove all POST permissions).

Remove (Deprecated)

Component Current Use Replacement Notes
Docker API /containers/{id}/start Start containers mutation { docker { start(id: "...") } } GraphQL tested working
Docker API /containers/{id}/stop Stop containers mutation { docker { stop(id: "...") } } GraphQL tested working
Docker API /containers/{id}/restart Restart containers Sequential: stop mutation → start mutation No single restart mutation
Docker API /containers/json List containers query { docker { containers { ... } } } Different field formats
Docker API /containers/{id}/json Container details query { docker { containers { ... } } } with client-side filter No single-container query
Docker API /images/create Pull images mutation { docker { updateContainer(id: "...") } } Update handles pull + recreate
Docker API /containers/create Recreate container mutation { docker { updateContainer(id: "...") } } Update handles pull + recreate
Docker API /containers/{id} (DELETE) Remove old container mutation { docker { updateContainer(id: "...") } } Update handles cleanup

Add (New)

None. All required infrastructure already in place from v1.3.

Installation / Configuration Changes

No New Packages Required

No npm packages, no new n8n nodes, no new dependencies.

Configuration Updates Required

1. docker-socket-proxy reconfiguration (restrict to logs only):

# Unraid Docker container edit
# Remove all POST permissions, keep only:
CONTAINERS=1
POST=0  # Block all POST operations

2. n8n container — no changes needed:

  • UNRAID_HOST already set (v1.3)
  • UNRAID_API_KEY already exists as n8n credential (v1.3)

3. Unraid API key permissions — verify:

unraid-api apikey --list | grep "Docker Manager Bot"
# Should show: DOCKER:UPDATE_ANY permission

Docker REST API → Unraid GraphQL Mapping

Container List

Docker API:

curl http://docker-socket-proxy:2375/v1.47/containers/json?all=true

Response format:

[
  {
    "Id": "8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
    "Names": ["/n8n"],
    "State": "running",
    "Status": "Up 7 hours",
    "Image": "n8nio/n8n"
  }
]

Unraid GraphQL:

query {
  docker {
    containers {
      id
      names
      state
      status
      image
      autoStart
    }
  }
}

Response format:

{
  "data": {
    "docker": {
      "containers": [
        {
          "id": "1639d2f04f44841bc62fec38d18e1869a558d85071fa23e0a8bf64d374b317fa:8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
          "names": ["/n8n"],
          "state": "RUNNING",
          "status": "Up 7 hours",
          "image": "n8nio/n8n",
          "autoStart": false
        }
      ]
    }
  }
}

Key differences:

  • id: Docker short ID (64 chars) → Unraid PrefixedID (server_hash:container_hash, 129 chars total)
  • State: lowercase (running) → UPPERCASE (RUNNING)
  • Names: Same format (includes / prefix)
  • Status: Same format
  • autoStart: Not in Docker API, available in Unraid API

Start Container

Docker API:

curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{id}/start
# Response: 204 No Content (success) or 304 (already started)

Unraid GraphQL:

mutation {
  docker {
    start(id: "server_hash:container_hash") {
      id
      names
      state
    }
  }
}

Response (success):

{
  "data": {
    "docker": {
      "start": {
        "id": "...",
        "names": ["/container"],
        "state": "RUNNING"
      }
    }
  }
}

Response (already started):

{
  "errors": [
    {
      "message": "(HTTP code 304) container already started -  ",
      "extensions": { "code": "INTERNAL_SERVER_ERROR" }
    }
  ],
  "data": null
}

Stop Container

Docker API:

curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{id}/stop?t=10
# Response: 204 No Content (success) or 304 (already stopped)

Unraid GraphQL:

mutation {
  docker {
    stop(id: "server_hash:container_hash") {
      id
      names
      state
    }
  }
}

Response:

{
  "data": {
    "docker": {
      "stop": {
        "id": "...",
        "names": ["/container"],
        "state": "EXITED"
      }
    }
  }
}

Restart Container

Docker API:

curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{id}/restart?t=10
# Response: 204 No Content

Unraid GraphQL: NO SINGLE MUTATION. Must chain stop + start:

# Step 1: Stop
mutation {
  docker {
    stop(id: "server_hash:container_hash") {
      id
      state
    }
  }
}

# Step 2: Start
mutation {
  docker {
    start(id: "server_hash:container_hash") {
      id
      state
    }
  }
}

Implementation note: n8n workflow must execute two sequential HTTP Request nodes for restart operations.

Update Container

Docker API (3-step process):

# 1. Pull new image
curl -X POST http://docker-socket-proxy:2375/v1.47/images/create?fromImage=image:tag

# 2. Stop container
curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{id}/stop

# 3. Remove old container
curl -X DELETE http://docker-socket-proxy:2375/v1.47/containers/{id}

# 4. Create new container
curl -X POST http://docker-socket-proxy:2375/v1.47/containers/create?name=... \
  -H "Content-Type: application/json" \
  -d '{ container config JSON }'

# 5. Start new container
curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{new_id}/start

Unraid GraphQL (single mutation):

mutation {
  docker {
    updateContainer(id: "server_hash:container_hash") {
      id
      names
      state
      image
    }
  }
}

Response:

{
  "data": {
    "docker": {
      "updateContainer": {
        "id": "...",
        "names": ["/container"],
        "state": "RUNNING",
        "image": "image:tag"
      }
    }
  }
}

Major simplification: 5-step Docker API process → single GraphQL mutation. Unraid handles pull, stop, remove, create, start automatically.

Batch Update

Docker API: Manual loop over containers (5 API calls × N containers).

Unraid GraphQL:

mutation {
  docker {
    updateContainers(ids: [
      "server_hash:container1_hash",
      "server_hash:container2_hash"
    ]) {
      id
      names
      state
    }
  }
}

Alternative (update all with updates available):

mutation {
  docker {
    updateAllContainers {
      id
      names
      state
    }
  }
}

Container Logs

Docker API:

curl http://docker-socket-proxy:2375/v1.47/containers/{id}/logs?stdout=1&stderr=1&tail=50&timestamps=1

Unraid GraphQL: NOT AVAILABLE. Official documentation: "Container output logs are NOT accessible via API and you must use docker logs via SSH."

Solution: Keep Docker socket proxy for logs retrieval only. No GraphQL replacement exists.

n8n HTTP Request Node Configuration

Docker API (Current)

{
  "name": "Docker API Call",
  "type": "n8n-nodes-base.httpRequest",
  "parameters": {
    "url": "http://docker-socket-proxy:2375/v1.47/containers/json",
    "method": "GET",
    "authentication": "none"
  }
}

Unraid GraphQL API (New)

{
  "name": "Unraid GraphQL Call",
  "type": "n8n-nodes-base.httpRequest",
  "parameters": {
    "url": "={{ $env.UNRAID_HOST }}/graphql",
    "method": "POST",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendBody": true,
    "contentType": "application/json",
    "body": "={{ JSON.stringify({ query: 'mutation { docker { start(id: \"' + $json.containerId + '\") { id state } } }' }) }}",
    "options": {
      "timeout": 30000
    }
  },
  "credentials": {
    "httpHeaderAuth": {
      "id": "...",
      "name": "Unraid API Key"
    }
  }
}

Key differences:

  • URL: UNRAID_HOST environment variable + /graphql suffix
  • Method: POST (not GET)
  • Authentication: Header Auth credential (not none)
  • Body: JSON with query field containing GraphQL query/mutation
  • Timeout: 30 seconds (mutations can take time)

GraphQL Query/Mutation Structure

Queries (read-only):

{
  "query": "query { docker { containers { id names state } } }"
}

Mutations (write operations):

{
  "query": "mutation { docker { start(id: \"...\") { id state } } }"
}

Variables (optional, recommended for complex queries):

{
  "query": "mutation($id: PrefixedID!) { docker { start(id: $id) { id state } } }",
  "variables": {
    "id": "server_hash:container_hash"
  }
}

Container ID Format Differences

Docker API

  • Format: 64-character SHA256 hex string
  • Example: 8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925
  • Usage: Can use short form (12 chars) for most operations

Unraid GraphQL API

  • Format: PrefixedID scalar — {server_hash}:{container_hash}
  • Example: 1639d2f04f44841bc62fec38d18e1869a558d85071fa23e0a8bf64d374b317fa:8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925
  • Components:
    • First 64 chars: Server hash (same for all containers on this Unraid server)
    • Colon separator
    • Last 64 chars: Container hash (matches Docker API container ID)
  • Usage: Must use full PrefixedID (129 chars total) — no short form supported

Migration impact: All container ID references must switch from Docker short IDs to Unraid PrefixedIDs. Extract from Unraid container list query, not Docker API.

Error Handling Differences

Docker API

Success:

  • HTTP 204 No Content (empty body)
  • HTTP 200 OK (JSON body)

Already in state:

  • HTTP 304 Not Modified (start already running, stop already stopped)

Errors:

  • HTTP 404 Not Found (container doesn't exist)
  • HTTP 500 Internal Server Error (Docker daemon error)

n8n detection: Check HTTP status code in HTTP Request node settings → "Always Output Data" + "Ignore HTTP Status Errors" → check $json.statusCode

Unraid GraphQL API

Success:

{
  "data": {
    "docker": {
      "start": { "id": "...", "state": "RUNNING" }
    }
  }
}

Already in state:

{
  "errors": [
    {
      "message": "(HTTP code 304) container already started -  ",
      "extensions": { "code": "INTERNAL_SERVER_ERROR" }
    }
  ],
  "data": null
}

Errors:

{
  "errors": [
    {
      "message": "Error message here",
      "locations": [{ "line": 1, "column": 21 }],
      "path": ["docker", "start"],
      "extensions": { "code": "ERROR_CODE" }
    }
  ],
  "data": null
}

n8n detection:

  • HTTP 200 always returned (even for errors)
  • Check response.errors array existence
  • Parse response.errors[0].message for error details
  • Check response.data !== null for success

Implementation pattern:

// n8n Code node after GraphQL call
const response = $json;

if (response.errors && response.errors.length > 0) {
  const error = response.errors[0];

  // HTTP 304 = already in desired state (treat as success)
  if (error.message.includes('HTTP code 304')) {
    return {
      json: {
        success: true,
        alreadyInState: true,
        message: 'Container already in desired state'
      }
    };
  }

  // Other errors = failure
  return {
    json: {
      success: false,
      error: error.message
    }
  };
}

// Success case
return {
  json: {
    success: true,
    data: response.data.docker
  }
};

Field Format Differences

State Field

Source Format Values
Docker API lowercase string running, exited, paused, restarting, dead
Unraid GraphQL UPPERCASE enum RUNNING, EXITED, PAUSED

Migration: Update all state comparisons from lowercase to UPPERCASE:

// OLD (Docker API)
if (container.State === 'running') { ... }

// NEW (Unraid GraphQL)
if (container.state === 'RUNNING') { ... }

Names Field

Both APIs return same format: array with /-prefixed names.

  • Docker: ["/n8n"]
  • Unraid: ["/n8n"]

No migration needed (already handling / prefix stripping).

Image Field

Both APIs return same format.

  • Docker: "n8nio/n8n"
  • Unraid: "n8nio/n8n"

No migration needed.

Timeout Considerations

Docker API

  • List containers: 5 seconds
  • Start/stop/restart: 10 seconds (includes ?t=10 grace period)
  • Logs: 10 seconds
  • Pull image: 120 seconds (large images)
  • Update (full flow): 180 seconds (pull + recreate)

Unraid GraphQL API

  • List containers: 5 seconds
  • Start/stop: 30 seconds (includes retry logic with 5 attempts @ 500ms)
  • Restart (stop + start): 60 seconds (two operations)
  • Update single: 180 seconds (pull + stop + remove + create + start)
  • Update batch: 300 seconds (5 minutes for multiple containers)

Implementation: Update n8n HTTP Request node timeout settings to match longer Unraid API timeouts.

Credential Management

Current (Docker API)

  • Type: None (unauthenticated proxy)
  • Setup: Docker socket proxy allows connections from dockernet network

New (Unraid GraphQL API)

  • Type: Header Auth
  • Credential name: "Unraid API Key"
  • Header name: x-api-key
  • Header value: Unraid API key (from unraid-api apikey --create)
  • Setup: Already configured in v1.3

No additional credentials needed.

Performance Implications

Advantages of Unraid GraphQL

  1. Single mutation for updates: 5-step Docker API flow → 1 GraphQL mutation (80% fewer network calls)
  2. Batch operations: Native support for updating multiple containers in one call
  3. Structured errors: Consistent error format with field-level error paths
  4. Type safety: GraphQL schema provides validation at API level

Disadvantages of Unraid GraphQL

  1. No logs access: Must retain Docker socket proxy for logs (adds architectural complexity)
  2. No restart mutation: Must chain stop + start (doubles network calls for restart)
  3. No single-container query: Must fetch all containers and filter client-side
  4. Longer IDs: 129-char PrefixedID vs 64-char Docker ID (larger payloads)
  5. HTTP 304 as error: "Already in state" returns error response instead of success

Net Performance Impact

Neutral to slightly positive. Update operations much faster (single mutation), but restart operations slightly slower (two mutations). Logs unchanged (still using Docker API).

Architecture Changes

Current Architecture

Telegram Bot
  ↓
n8n Workflows
  ↓
docker-socket-proxy (all operations)
  ↓
Docker Engine

New Architecture (v1.4)

Telegram Bot
  ↓
n8n Workflows
  ↓
  ├─→ Unraid GraphQL API (list, start, stop, restart, update)
  │     ↓
  │   Docker Engine
  │
  └─→ docker-socket-proxy (logs only, read-only)
        ↓
      Docker Engine

Hybrid Rationale

Logs are not available via Unraid GraphQL API. Two options:

  1. Hybrid: Keep proxy for logs, use GraphQL for everything else
  2. SSH: Remove proxy entirely, use SSH + docker logs command

Decision: Hybrid approach.

  • Pros: No SSH key management, no shell escaping, same logs implementation
  • Cons: Retains docker-socket-proxy container (but with minimal permissions)

Proxy Reconfiguration

Restrict proxy to read-only logs access:

# Unraid Docker container environment variables
CONTAINERS=1    # Allow container list (needed for name → ID lookup)
POST=0          # Block all POST operations (start, stop, create, etc.)
INFO=0          # Block info endpoints
IMAGES=0        # Block image operations
VOLUMES=0       # Block volume operations
NETWORKS=0      # Block network operations

Result: Proxy can only read container list and logs. All control operations blocked.

Migration Strategy

Phase 1: Container List (Status Query)

Replace Docker API /containers/json with GraphQL containers query.

  • Complexity: Low (field mapping only)
  • Risk: Low (read-only operation)
  • Rollback: Trivial (revert HTTP Request node URL)

Phase 2: Start/Stop Operations

Replace Docker API /containers/{id}/start and /stop with GraphQL mutations.

  • Complexity: Medium (error handling changes)
  • Risk: Medium (control operations, test thoroughly)
  • Rollback: Easy (revert to Docker API URLs)

Phase 3: Restart Operations

Replace Docker API /containers/{id}/restart with sequential stop + start mutations.

  • Complexity: High (two sequential HTTP Request nodes)
  • Risk: Medium (timing between stop and start)
  • Rollback: Easy (revert to single Docker API call)

Phase 4: Update Operations

Replace 5-step Docker API update flow with single GraphQL mutation.

  • Complexity: Low (major simplification)
  • Risk: Medium (critical operation, test thoroughly)
  • Rollback: Hard (5-step flow complex to restore)

Phase 5: Logs (No Change)

Keep Docker socket proxy for logs retrieval.

  • Complexity: None (no changes)
  • Risk: None (unchanged)
  • Rollback: N/A (nothing to rollback)

Phase 6: Cleanup

Remove docker-socket-proxy POST permissions, update documentation.

  • Complexity: Low (configuration change)
  • Risk: Low (proxy already unused for control ops)
  • Rollback: Easy (restore POST=1)

Testing Verification

Test Cases for Each Operation

Container List:

  • Query returns all containers
  • State field is UPPERCASE
  • Names include / prefix
  • Container ID is PrefixedID format

Start:

  • Start stopped container → state becomes RUNNING
  • Start running container → HTTP 304 error (treat as success)
  • Start non-existent container → error response

Stop:

  • Stop running container → state becomes EXITED
  • Stop stopped container → HTTP 304 error (treat as success)
  • Stop non-existent container → error response

Restart:

  • Stop mutation succeeds → state becomes EXITED
  • Start mutation succeeds → state becomes RUNNING
  • Restart preserves container configuration

Update:

  • Update container with new image available → new image pulled + running
  • Update container already up-to-date → success (or error?)
  • Update preserves container configuration (ports, volumes, env vars)

Logs:

  • Logs query via Docker API still works
  • Timestamps included
  • Tail limit respected
  • Both stdout and stderr captured

Load Testing

Batch updates:

  • Update 5 containers → all succeed
  • Update 10 containers → timeout handling
  • Update with one failure → partial success handling

Confidence Assessment

Area Confidence Evidence
Container list query HIGH Tested on live Unraid 7.2, matches schema
Start mutation HIGH Tested working, HTTP 304 for already-started
Stop mutation HIGH Tested working, state transitions confirmed
Restart approach MEDIUM No native mutation, chaining required
Update mutation HIGH Schema documented, not tested live
Batch update MEDIUM Schema documented, not tested live
Logs unavailability HIGH Official docs state "not accessible via API"
Error handling HIGH Tested GraphQL error format
Container ID format HIGH Tested PrefixedID extraction
Hybrid architecture MEDIUM Logs limitation forces retention of proxy

Open Questions

  1. Update mutation behavior when already up-to-date: Does it return success immediately or pull image again?

    • Resolution: Test on non-critical container before production use
  2. Batch update error handling: If one container fails, do others continue?

    • Resolution: Test with intentionally failing container in batch
  3. Restart timing: What's the optimal delay between stop and start mutations?

    • Resolution: Test with various container types (fast-starting vs slow-starting)
  4. Proxy removal timeline: Can docker-socket-proxy be fully removed in a future phase?

    • Resolution: Depends on Unraid API adding logs support (monitor roadmap)

Sources

Official Documentation

Community Implementations

Technical References

Schema Discovery

  • Unraid GraphQL generated schema (introspection disabled on production)
  • Live testing on Unraid 7.2 server (2026-02-09)
  • Container operations verified via direct API calls

Confidence level: HIGH for tested operations (list, start, stop), MEDIUM for untested operations (update, batch), HIGH for logs unavailability (official documentation).