23 KiB
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:
- Unraid GraphQL API provides mutations for start, stop, pause/unpause, update (single + batch)
- NO restart mutation — must chain stop + start operations
- NO logs query — Unraid API documentation explicitly states "Container output logs are NOT accessible via API and you must use docker logs via SSH"
- Container list query returns same data as Docker API but with different field formats (UPPERCASE state,
/-prefixed names) - All mutations verified working on live Unraid 7.2 server via testing
Recommended Stack Changes
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_HOSTalready set (v1.3)UNRAID_API_KEYalready 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 formatautoStart: 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×tamps=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_HOSTenvironment variable +/graphqlsuffix - Method: POST (not GET)
- Authentication: Header Auth credential (not none)
- Body: JSON with
queryfield 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:
PrefixedIDscalar —{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.errorsarray existence - Parse
response.errors[0].messagefor error details - Check
response.data !== nullfor 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=10grace 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
- Single mutation for updates: 5-step Docker API flow → 1 GraphQL mutation (80% fewer network calls)
- Batch operations: Native support for updating multiple containers in one call
- Structured errors: Consistent error format with field-level error paths
- Type safety: GraphQL schema provides validation at API level
Disadvantages of Unraid GraphQL
- No logs access: Must retain Docker socket proxy for logs (adds architectural complexity)
- No restart mutation: Must chain stop + start (doubles network calls for restart)
- No single-container query: Must fetch all containers and filter client-side
- Longer IDs: 129-char PrefixedID vs 64-char Docker ID (larger payloads)
- 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:
- Hybrid: Keep proxy for logs, use GraphQL for everything else
- SSH: Remove proxy entirely, use SSH +
docker logscommand
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
-
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
-
Batch update error handling: If one container fails, do others continue?
- Resolution: Test with intentionally failing container in batch
-
Restart timing: What's the optimal delay between stop and start mutations?
- Resolution: Test with various container types (fast-starting vs slow-starting)
-
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
- Unraid API Documentation — Official API overview
- Using the Unraid API — Authentication and usage guide
- Unraid API GitHub Repository — Source code monorepo
Community Implementations
- unraid-api-client by domalab — Python client documenting queries
- unraid-mcp by jmagar — MCP server with Docker management tools
Technical References
- DeepWiki Unraid API — Comprehensive technical documentation
- DeepWiki Docker Integration — Docker service internals
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).