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

835 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
## 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):**
```bash
# 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:**
```bash
unraid-api apikey --list | grep "Docker Manager Bot"
# Should show: DOCKER:UPDATE_ANY permission
```
## Docker REST API → Unraid GraphQL Mapping
### Container List
**Docker API:**
```bash
curl http://docker-socket-proxy:2375/v1.47/containers/json?all=true
```
**Response format:**
```json
[
{
"Id": "8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
"Names": ["/n8n"],
"State": "running",
"Status": "Up 7 hours",
"Image": "n8nio/n8n"
}
]
```
**Unraid GraphQL:**
```graphql
query {
docker {
containers {
id
names
state
status
image
autoStart
}
}
}
```
**Response format:**
```json
{
"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:**
```bash
curl -X POST http://docker-socket-proxy:2375/v1.47/containers/{id}/start
# Response: 204 No Content (success) or 304 (already started)
```
**Unraid GraphQL:**
```graphql
mutation {
docker {
start(id: "server_hash:container_hash") {
id
names
state
}
}
}
```
**Response (success):**
```json
{
"data": {
"docker": {
"start": {
"id": "...",
"names": ["/container"],
"state": "RUNNING"
}
}
}
}
```
**Response (already started):**
```json
{
"errors": [
{
"message": "(HTTP code 304) container already started - ",
"extensions": { "code": "INTERNAL_SERVER_ERROR" }
}
],
"data": null
}
```
### Stop Container
**Docker API:**
```bash
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:**
```graphql
mutation {
docker {
stop(id: "server_hash:container_hash") {
id
names
state
}
}
}
```
**Response:**
```json
{
"data": {
"docker": {
"stop": {
"id": "...",
"names": ["/container"],
"state": "EXITED"
}
}
}
}
```
### Restart Container
**Docker API:**
```bash
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:
```graphql
# 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):**
```bash
# 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):**
```graphql
mutation {
docker {
updateContainer(id: "server_hash:container_hash") {
id
names
state
image
}
}
}
```
**Response:**
```json
{
"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:**
```graphql
mutation {
docker {
updateContainers(ids: [
"server_hash:container1_hash",
"server_hash:container2_hash"
]) {
id
names
state
}
}
}
```
**Alternative (update all with updates available):**
```graphql
mutation {
docker {
updateAllContainers {
id
names
state
}
}
}
```
### Container Logs
**Docker API:**
```bash
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)
```json
{
"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)
```json
{
"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):**
```json
{
"query": "query { docker { containers { id names state } } }"
}
```
**Mutations (write operations):**
```json
{
"query": "mutation { docker { start(id: \"...\") { id state } } }"
}
```
**Variables (optional, recommended for complex queries):**
```json
{
"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:**
```json
{
"data": {
"docker": {
"start": { "id": "...", "state": "RUNNING" }
}
}
}
```
**Already in state:**
```json
{
"errors": [
{
"message": "(HTTP code 304) container already started - ",
"extensions": { "code": "INTERNAL_SERVER_ERROR" }
}
],
"data": null
}
```
**Errors:**
```json
{
"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:**
```javascript
// 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:
```javascript
// 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:
```bash
# 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
- [Unraid API Documentation](https://docs.unraid.net/API/) — Official API overview
- [Using the Unraid API](https://docs.unraid.net/API/how-to-use-the-api/) — Authentication and usage guide
- [Unraid API GitHub Repository](https://github.com/unraid/api) — Source code monorepo
### Community Implementations
- [unraid-api-client by domalab](https://github.com/domalab/unraid-api-client/blob/main/UNRAIDAPI.md) — Python client documenting queries
- [unraid-mcp by jmagar](https://github.com/jmagar/unraid-mcp) — MCP server with Docker management tools
### Technical References
- [DeepWiki Unraid API](https://deepwiki.com/unraid/api) — Comprehensive technical documentation
- [DeepWiki Docker Integration](https://deepwiki.com/unraid/api/2.4-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).