Phase 2: Docker Integration - Docker Engine API v1.53 identified as standard - Execute Command + curl recommended approach - Fuzzy matching patterns documented - Security considerations catalogued
24 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
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:
# 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=falseparameter 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=trueparameter:/containers/jsononly shows running containers by default; useall=trueto see stopped containers - Parsing Status string for uptime: Use
State.StartedAttimestamp 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.cachefor 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.executeCommandfrom 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:
-
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 curlin custom Dockerfile or use Execute Command withwget
-
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
-
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)
- Docker Engine API Official Documentation - API reference
- Docker Engine API Examples - Official usage examples
- Docker inspect CLI documentation - Container data structure
- Docker stats CLI documentation - Resource metrics
- n8n Docker documentation - n8n Docker setup
- n8n Docker Compose documentation - Deployment examples
- n8n Code node documentation - JavaScript execution
- n8n Execute Command documentation - Shell command execution
Secondary (MEDIUM confidence)
- Using curl with Docker Unix socket - Practical curl examples
- Docker Engine API container info guide - Comprehensive guide verified with official docs
- Docker stats in JSON format - Stats parsing examples
- n8n Execute Command tutorial - Usage patterns
- Fuse.js official documentation - Fuzzy matching library
- fast-fuzzy npm package - Lightweight alternative
- n8n modules in Code node - External package setup
Tertiary (LOW confidence)
- n8n community: Docker commands from n8n - Community discussion, verify approaches
- Docker socket security considerations - Security implications
- Container escape mitigation - Security best practices
- Docker container naming conventions - Community 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)