Files
unraid-docker-manager/.planning/phases/02-docker-integration/02-RESEARCH.md
T
Lucas Berger aeae1e4377 docs(02): research phase domain with Context7
Phase 02: Docker Integration
- Docker Engine API endpoints documented (Context7 /docker/docs)
- n8n Execute Command and Code node patterns (Context7 /n8n-io/n8n-docs)
- Custom Dockerfile needed for curl in n8n container
- Fuzzy matching and response formatting patterns
2026-01-29 08:22:32 -05:00

632 lines
23 KiB
Markdown

# Phase 2: Docker Integration - Research
**Researched:** 2026-01-29
**Domain:** Docker API integration with n8n workflows
**Confidence:** HIGH
## Summary
This phase connects n8n to Docker via the Docker Engine API to query container information. The research reveals three viable approaches: (1) mounting the Docker socket and using n8n's Execute Command node with curl, (2) using the HTTP Request node with Unix socket support (requires community node), or (3) using the Code node with axios for HTTP requests to a TCP-exposed Docker API.
The recommended approach is **mounting the Docker socket and using the Execute Command node with curl** because it's the most straightforward, doesn't require external npm packages, and curl is already available in the n8n Docker image. The Docker Engine API provides comprehensive endpoints for listing containers, inspecting details, and retrieving statistics in JSON format.
Key security consideration: Mounting the Docker socket grants root-equivalent access to the host, but this is acceptable for a single-user bot running on a dedicated server where the bot owner is also the server owner.
**Primary recommendation:** Mount `/var/run/docker.sock` into n8n container, use Execute Command node with curl to query Docker API v1.53 endpoints, parse JSON responses in Code node with JavaScript.
## Standard Stack
The established libraries/tools for this domain:
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Docker Engine API | v1.53 | Query container info | Official Docker API, current version as of 2026 |
| curl | 7.50+ | HTTP requests to Unix socket | Built into n8n image, supports `--unix-socket` flag |
| n8n Execute Command | Latest | Run shell commands | Built-in node, no additional setup required |
| n8n Code node | Latest | Parse JSON, implement fuzzy matching | Built-in node, full JavaScript support |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| fast-fuzzy | 1.x | Fuzzy string matching | For container name matching - lightweight (npm install required) |
| Fuse.js | 7.x | Advanced fuzzy search | Alternative if more features needed (npm install required) |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Execute Command + curl | Unix Socket Bridge community node | Community node adds dependency, but provides more features for socket communication |
| Execute Command + curl | Code node + axios | Requires TCP-exposed Docker API (less secure) or npm package setup |
| Execute Command + curl | HTTP Request node | Doesn't support Unix sockets natively |
**Installation:**
```yaml
# docker-compose.yml addition for n8n
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket
# If using fuzzy matching libraries (optional):
# docker exec -u node n8n npm install fast-fuzzy
# Add to environment:
# NODE_FUNCTION_ALLOW_EXTERNAL=fast-fuzzy
```
## Architecture Patterns
### Recommended Workflow Structure
```
Telegram Message
n8n Telegram Trigger
Code Node: Parse Intent (delegate to Claude API)
Switch Node: Route by Intent
┌─────────────────────────────────────────┐
│ Query Container Status Branch │
├─────────────────────────────────────────┤
│ 1. Code Node: Extract container name │
│ 2. Execute Command: curl Docker API │
│ 3. Code Node: Parse JSON, fuzzy match │
│ 4. Code Node: Format response │
│ 5. Telegram Send Message │
└─────────────────────────────────────────┘
```
### Pattern 1: Query Docker API via Unix Socket
**What:** Use curl with `--unix-socket` flag to make HTTP requests to Docker Engine API
**When to use:** For all container queries (list, inspect, stats)
**Example:**
```bash
# List all containers
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/json?all=true'
# List only running containers
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/json?filters={"status":["running"]}'
# Inspect specific container
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/<id>/json'
# Get container stats (one-shot, no streaming)
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/<id>/stats?stream=false'
```
**Sources:**
- [Docker Engine API Examples](https://docs.docker.com/reference/api/engine/sdk/examples/)
- [Using curl with Docker Unix socket](https://sleeplessbeastie.eu/2021/12/13/how-to-query-docker-socket-using-curl/)
- [Docker API via Unix socket guide](https://www.baeldung.com/ops/docker-engine-api-container-info)
### Pattern 2: Parse Container Information
**What:** Extract useful information from Docker API JSON responses
**When to use:** After every Docker API call
**Key JSON Paths:**
```javascript
// From /containers/json response
containers.forEach(c => {
const name = c.Names[0].replace(/^\//, ''); // Remove leading slash
const state = c.State; // "running", "exited", "paused"
const status = c.Status; // "Up 2 days", "Exited (0) 3 hours ago"
const image = c.Image; // Image name
const id = c.Id.substring(0, 12); // Short ID
});
// From /containers/<id>/json response (inspect)
const startedAt = container.State.StartedAt; // "2026-01-27T15:30:00.000Z"
const healthStatus = container.State.Health?.Status; // "healthy", "unhealthy", "starting"
const uptime = Date.now() - new Date(startedAt).getTime();
// From /containers/<id>/stats response
const memUsage = stats.memory_stats.usage;
const memLimit = stats.memory_stats.limit;
const memPercent = (memUsage / memLimit) * 100;
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const cpuPercent = (cpuDelta / systemDelta) * 100;
```
**Sources:**
- [Docker inspect format documentation](https://docs.docker.com/reference/cli/docker/inspect/)
- [Docker stats JSON format](https://kylewbanks.com/blog/docker-stats-memory-cpu-in-json-format)
### Pattern 3: Fuzzy Container Name Matching
**What:** Match user input like "plex" to container names like "plex-server" or "linuxserver-plex"
**When to use:** When user provides partial or informal container name
**Approach 1: Simple JavaScript (no library):**
```javascript
function fuzzyMatch(input, containerNames) {
const normalized = input.toLowerCase().trim();
// Strip common prefixes
const stripPrefixes = (name) => {
return name.replace(/^(linuxserver[-_]|binhex[-_])/i, '');
};
// Exact match first
let matches = containerNames.filter(name =>
stripPrefixes(name).toLowerCase() === normalized
);
// Substring match
if (matches.length === 0) {
matches = containerNames.filter(name =>
stripPrefixes(name).toLowerCase().includes(normalized)
);
}
return matches;
}
```
**Approach 2: Using fast-fuzzy (if installed):**
```javascript
// In Code node (requires NODE_FUNCTION_ALLOW_EXTERNAL=fast-fuzzy)
const { search } = require('fast-fuzzy');
const matches = search(userInput, containerNames, {
threshold: 0.6,
ignoreCase: true,
keySelector: (name) => name.replace(/^(linuxserver[-_]|binhex[-_])/i, '')
});
```
**Sources:**
- [Fuse.js - lightweight fuzzy search](https://www.fusejs.io/)
- [fast-fuzzy - high performance fuzzy matching](https://www.npmjs.com/package/fast-fuzzy)
### Pattern 4: Format Response for Telegram
**What:** Convert container data into user-friendly Telegram messages with emoji
**When to use:** Before sending any container information to user
**Example:**
```javascript
function formatContainerStatus(container, detailed = false) {
// Emoji mapping
const stateEmoji = {
'running': '✅',
'exited': '❌',
'paused': '⏸️',
'restarting': '🔄',
'dead': '💀'
};
const healthEmoji = {
'healthy': '💚',
'unhealthy': '🔴',
'starting': '🟡',
'none': ''
};
const emoji = stateEmoji[container.State] || '❓';
const name = container.Names[0].replace(/^\//, '');
if (!detailed) {
// Simple status
return `${emoji} ${name}: ${container.Status}`;
}
// Detailed status
const health = container.State.Health
? `\n${healthEmoji[container.State.Health.Status]} Health: ${container.State.Health.Status}`
: '';
const uptime = formatUptime(container.State.StartedAt);
return `${emoji} **${name}**
State: ${container.State}
Uptime: ${uptime}${health}
Image: ${container.Image}
ID: ${container.Id.substring(0, 12)}`;
}
function formatUptime(startedAt) {
const ms = Date.now() - new Date(startedAt).getTime();
const days = Math.floor(ms / (24 * 60 * 60 * 1000));
const hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h`;
return 'just started';
}
```
### Anti-Patterns to Avoid
- **Streaming stats requests:** Use `?stream=false` parameter to get one-shot stats, not continuous stream
- **Not handling socket permissions:** Ensure n8n container user can read/write to socket (usually works by default)
- **Forgetting `all=true` parameter:** `/containers/json` only shows running containers by default; use `all=true` to see stopped containers
- **Parsing Status string for uptime:** Use `State.StartedAt` timestamp instead of parsing "Up 2 days" strings
- **Case-sensitive container name matching:** Always normalize to lowercase for matching
## Don't Hand-Roll
Problems that look simple but have existing solutions:
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Fuzzy string matching | Custom edit distance algorithm | fast-fuzzy or Fuse.js | Edge cases, Unicode handling, performance optimization |
| Date/time formatting | String manipulation | Built-in Date methods or date-fns | Timezone handling, edge cases |
| JSON parsing from curl | Regex extraction | `JSON.parse()` in Code node | Proper error handling, nested data |
| Docker authentication | Custom auth headers | n8n Credentials (if using TCP API) | Secure storage, token refresh |
**Key insight:** The Docker API is well-documented and consistent. Don't try to parse docker CLI output; always use the API directly. The JSON responses are structured and predictable.
## Common Pitfalls
### Pitfall 1: Docker Socket Permission Denied
**What goes wrong:** Execute Command node returns "Permission denied" when trying to access `/var/run/docker.sock`
**Why it happens:** The socket file requires specific permissions, and the n8n container user may not have access
**How to avoid:**
- Ensure socket is mounted with correct permissions in docker-compose
- If needed, add n8n user to docker group (not recommended for security)
- Alternative: Run n8n container with `--user root` (security tradeoff)
**Warning signs:** Error message containing "Permission denied" or "dial unix /var/run/docker.sock"
### Pitfall 2: Container Names Have Leading Slash
**What goes wrong:** Docker API returns container names as `["/plex-server"]` with leading slash, breaking string matching
**Why it happens:** Docker internally prefixes container names with `/` to distinguish them from network aliases
**How to avoid:**
```javascript
const name = container.Names[0].replace(/^\//, '');
```
**Warning signs:** Container names displaying with slash in Telegram messages, matching failing
### Pitfall 3: Health Status May Not Exist
**What goes wrong:** Accessing `container.State.Health.Status` throws error because not all containers have health checks
**Why it happens:** Health checks are optional in Docker; containers without HEALTHCHECK directive don't have the Health object
**How to avoid:**
```javascript
const health = container.State?.Health?.Status || 'none';
// Or
if (container.State.Health) {
// Process health status
}
```
**Warning signs:** Error in Code node: "Cannot read property 'Status' of undefined"
### Pitfall 4: Memory Stats Calculation Differs from docker stats CLI
**What goes wrong:** Memory usage calculated from API doesn't match `docker stats` output
**Why it happens:** Docker CLI applies cache memory adjustments that aren't obvious from raw API data
**How to avoid:**
- Use `memory_stats.usage - memory_stats.stats.cache` for more accurate representation
- Or document that values may differ slightly from CLI output
- For this use case, raw usage is acceptable for relative comparisons
**Warning signs:** User reports "memory usage doesn't match what I see in Portainer"
**Source:** [Docker memory usage inconsistency](https://github.com/moby/moby/issues/45727)
### Pitfall 5: Execute Command Node Disabled by Default (n8n 2.0+)
**What goes wrong:** Execute Command node doesn't appear in node list or workflows fail with "node not found"
**Why it happens:** n8n 2.0+ disables Execute Command node by default for security reasons
**How to avoid:**
- Set environment variable: `NODES_EXCLUDE=""` (empty string enables all nodes)
- Or explicitly enable: Remove `n8n-nodes-base.executeCommand` from exclusion list
**Warning signs:** Execute Command node missing from node palette
**Source:** [How to Enable Execute Command](https://community.n8n.io/t/how-to-enable-execute-command/249009)
### Pitfall 6: Docker Socket Security Risks
**What goes wrong:** Security-conscious user questions mounting Docker socket due to container escape risks
**Why it happens:** Docker socket access grants root-equivalent privileges to the host
**How to avoid:**
- Acknowledge the risk: Document that this grants significant access
- Justify for this use case: Single-user bot on dedicated server, bot owner = server owner
- Consider alternatives if multi-user: Docker API over TCP with TLS authentication
- Keep n8n updated: Prevents exploitation of n8n vulnerabilities that could leverage Docker access
**Warning signs:** Security audit flags Docker socket mount
**Sources:**
- [Docker socket security risks](https://0xn3va.gitbook.io/cheat-sheets/container/escaping/exposed-docker-socket)
- [Container escape mitigation strategies](https://www.startupdefense.io/cyberattacks/docker-escape)
- [Docker security best practices](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html)
## Code Examples
Verified patterns from official sources:
### List All Containers (Including Stopped)
```bash
# Execute Command node
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/json?all=true'
```
**Response format:**
```json
[
{
"Id": "8dfafdbc3a40",
"Names": ["/plex-server"],
"Image": "plexinc/pms-docker:latest",
"State": "running",
"Status": "Up 2 days",
"Created": 1738080000,
"Ports": [{"PrivatePort": 32400, "PublicPort": 32400, "Type": "tcp"}]
}
]
```
**Source:** [Docker Engine API Examples](https://docs.docker.com/reference/api/engine/sdk/examples/)
### Filter Containers by State
```bash
# Only running containers
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/json?filters=%7B%22status%22%3A%5B%22running%22%5D%7D'
# URL-decoded filter: {"status":["running"]}
# Both running and stopped
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/json?filters=%7B%22status%22%3A%5B%22running%22%2C%22exited%22%5D%7D'
# URL-decoded filter: {"status":["running","exited"]}
```
**Source:** [Docker API filtering documentation](https://forums.docker.com/t/docker-engine-api-via-curl-to-get-json-output-for-list-of-running-container-id-only/139325)
### Inspect Single Container (Detailed Info)
```bash
# Execute Command node (replace <id> with container ID or name)
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/<id>/json'
```
**Response includes:**
```json
{
"Id": "8dfafdbc3a40...",
"Name": "/plex-server",
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"StartedAt": "2026-01-27T15:30:00.000000000Z",
"Health": {
"Status": "healthy",
"FailingStreak": 0
}
},
"Config": {
"Image": "plexinc/pms-docker:latest"
}
}
```
**Source:** [Docker inspect documentation](https://docs.docker.com/reference/cli/docker/inspect/)
### Get Container Resource Usage
```bash
# Execute Command node (one-shot stats, no streaming)
curl --unix-socket /var/run/docker.sock \
'http://localhost/v1.53/containers/<id>/stats?stream=false'
```
**Response includes:**
```json
{
"memory_stats": {
"usage": 209715200,
"limit": 34359738368,
"stats": {
"cache": 10485760
}
},
"cpu_stats": {
"cpu_usage": {
"total_usage": 1500000000
},
"system_cpu_usage": 50000000000
}
}
```
**Usage calculation:**
```javascript
// In Code node
const json = JSON.parse(items[0].json.stdout);
const memUsageMB = (json.memory_stats.usage / 1024 / 1024).toFixed(0);
const memLimitMB = (json.memory_stats.limit / 1024 / 1024).toFixed(0);
const memPercent = ((json.memory_stats.usage / json.memory_stats.limit) * 100).toFixed(1);
return [{
json: {
memory: `${memUsageMB} MB / ${memLimitMB} MB (${memPercent}%)`
}
}];
```
**Source:** [Docker stats in JSON format](https://kylewbanks.com/blog/docker-stats-memory-cpu-in-json-format)
### Parse and Match Container Names (Code Node)
```javascript
// items[0].json.stdout contains JSON from curl command
const containers = JSON.parse(items[0].json.stdout);
const userInput = $('Telegram Trigger').item.json.message.text;
// Extract container name from user query (assuming Claude API extracted it)
const requestedName = userInput.toLowerCase().trim();
// Normalize container names (remove leading slash and common prefixes)
function normalizeName(name) {
return name
.replace(/^\//, '') // Remove leading slash
.replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes
.toLowerCase();
}
// Find matching containers
const matches = containers.filter(c => {
const normalized = normalizeName(c.Names[0]);
return normalized.includes(requestedName) || requestedName.includes(normalized);
});
if (matches.length === 0) {
return [{
json: {
error: `No container found matching "${userInput}"`,
message: 'Container not found. Try listing all containers with "status".'
}
}];
}
if (matches.length > 1) {
const names = matches.map(c => c.Names[0].replace(/^\//, '')).join(', ');
return [{
json: {
error: 'multiple_matches',
matches: names,
message: `Found multiple matches: ${names}. Please be more specific.`
}
}];
}
// Single match found
return matches.map(c => ({
json: {
id: c.Id,
name: c.Names[0].replace(/^\//, ''),
state: c.State,
status: c.Status,
image: c.Image
}
}));
```
### Format Summary Response (Many Containers)
```javascript
// items[0].json.stdout contains JSON array of containers
const containers = JSON.parse(items[0].json.stdout);
// Count by state
const counts = containers.reduce((acc, c) => {
acc[c.State] = (acc[c.State] || 0) + 1;
return acc;
}, {});
// Format summary
const parts = [];
if (counts.running) parts.push(`${counts.running} running`);
if (counts.exited) parts.push(`${counts.exited} stopped`);
if (counts.paused) parts.push(`${counts.paused} paused`);
const summary = parts.join(', ');
const message = `📊 Container summary: ${summary}\n\nReply with a container name for details, or "list running" to see all.`;
return [{
json: {
summary: counts,
message: message
}
}];
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Docker CLI parsing | Docker Engine API | Always preferred | API provides structured JSON, CLI is for humans |
| docker-py library | Direct API calls | n8n context | No need for Python, curl + JSON works fine |
| TCP API exposure | Unix socket mount | Security best practice | Socket is more secure, no network exposure |
| Custom JSON parsing | Built-in JSON.parse() | Always | Reliable, handles edge cases |
**Deprecated/outdated:**
- **Portainer API as proxy:** Adds unnecessary layer; Docker API is sufficient
- **docker exec container parsing:** Fragile; always use API
- **Screen scraping docker ps:** Never do this; API exists for a reason
## Open Questions
Things that couldn't be fully resolved:
1. **Does n8n Docker image include curl by default?**
- What we know: Some Docker images (Alpine-based) don't include curl by default
- What's unclear: Current n8n Docker image (n8nio/n8n:latest) base image and included tools
- Recommendation: Test in environment; if curl missing, use `apk add curl` in custom Dockerfile or use Execute Command with `wget`
2. **Optimal fuzzy matching threshold for container names**
- What we know: Different libraries have different scoring algorithms
- What's unclear: What threshold provides best UX for this use case
- Recommendation: Start with simple substring matching; add library-based fuzzy matching only if users report issues
3. **Rate limiting on Docker API**
- What we know: Docker Hub has rate limits; local Docker Engine API does not
- What's unclear: Whether rapid API calls could cause performance issues on N100 CPU
- Recommendation: No artificial rate limiting needed initially; add if performance issues observed
## Sources
### Primary (HIGH confidence) — Context7 MCP
**Docker API** (`/docker/docs`):
- `GET /containers/json` — List containers with filtering (all, status, before, limit)
- `GET /containers/(id)/stats` — Resource usage (memory_stats, cpu_stats, network)
- Container response schema: Id, Names, Image, State, Status, Ports, NetworkSettings
- curl unix socket pattern: `curl --unix-socket /var/run/docker.sock http://localhost/v1.xx/containers/json`
**n8n Documentation** (`/n8n-io/n8n-docs`):
- Execute Command node — Disabled by default in n8n 2.0+, runs in container not host
- Custom Dockerfile needed for curl: `FROM n8nio/n8n; USER root; RUN apk add curl; USER node`
- Code node external modules: `NODE_FUNCTION_ALLOW_EXTERNAL=moment,uuid` environment variable
- Built-in modules: `NODE_FUNCTION_ALLOW_BUILTIN=crypto` for Node.js builtins
### Secondary (MEDIUM confidence)
- [Fuse.js official documentation](https://www.fusejs.io/) - Fuzzy matching library
- [fast-fuzzy npm package](https://www.npmjs.com/package/fast-fuzzy) - Lightweight alternative
### Tertiary (LOW confidence)
- [Docker socket security considerations](https://0xn3va.gitbook.io/cheat-sheets/container/escaping/exposed-docker-socket) - Security implications
- [Container escape mitigation](https://www.startupdefense.io/cyberattacks/docker-escape) - Security best practices
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - Docker API is official and stable, curl is standard tool
- Architecture: HIGH - Patterns verified with official documentation and examples
- Pitfalls: MEDIUM - Based on community reports and issue trackers, not personal experience
**Research date:** 2026-01-29
**Valid until:** 2026-04-29 (90 days - Docker API is stable, n8n updates monthly)