Files
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

23 KiB

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:

# 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

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:

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

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:

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

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

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

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

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:

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:

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:

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

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

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:

Code Examples

Verified patterns from official sources:

List All Containers (Including Stopped)

# Execute Command node
curl --unix-socket /var/run/docker.sock \
  'http://localhost/v1.53/containers/json?all=true'

Response format:

[
  {
    "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

Filter Containers by State

# 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

Inspect Single Container (Detailed Info)

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

{
  "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

Get Container Resource Usage

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

{
  "memory_stats": {
    "usage": 209715200,
    "limit": 34359738368,
    "stats": {
      "cache": 10485760
    }
  },
  "cpu_stats": {
    "cpu_usage": {
      "total_usage": 1500000000
    },
    "system_cpu_usage": 50000000000
  }
}

Usage calculation:

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

Parse and Match Container Names (Code Node)

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

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

Tertiary (LOW confidence)

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)