Files
Lucas Berger 893412f405 docs(03): create phase plan
Phase 03: Container Actions
- 4 plans in 4 waves (sequential due to shared workflow file)
- Ready for execution
2026-01-29 21:48:58 -05:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
03-container-actions 04 execute 4
03-01
n8n-workflow.json
true
truths artifacts key_links
User can update a container by name (pull new image, recreate)
Update detects if image actually changed
Version change shown when detectable from image labels
No notification if image was already up to date
path provides contains
n8n-workflow.json Container update workflow (pull + recreate) Update Container
from to via pattern
Route Message switch Update branch update <name> pattern update
from to via pattern
Docker inspect Docker create Config extraction and recreation containers/create
Implement container update action (pull new image + recreate container with same config).

Purpose: Allow users to update containers via "update plex" command. The workflow pulls the latest image, compares digests to detect changes, and recreates the container with the same configuration. Per CONTEXT.md, only notify if an actual update occurred.

Output: Extended n8n workflow with full container update flow.

<execution_context> @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-container-actions/03-CONTEXT.md @.planning/phases/03-container-actions/03-RESEARCH.md @.planning/phases/03-container-actions/03-01-SUMMARY.md @n8n-workflow.json Task 1: Add update command routing and container matching n8n-workflow.json Extend the "Route Message" Switch node to handle update commands:
  1. Add Update Route (to existing Switch node):

    • Pattern: message contains "update" (case-insensitive)
    • Route to new "Update Branch"
  2. Parse Update Command (Code node):

    const text = $json.message.text.toLowerCase().trim();
    const match = text.match(/^update\s+(.+)$/i);
    if (!match) {
      return { json: { error: 'Invalid update format', chatId: $json.message.chat.id } };
    }
    return {
      json: {
        containerQuery: match[1].trim(),
        chatId: $json.message.chat.id,
        messageId: $json.message.message_id
      }
    };
    
  3. Match Container (reuse existing matching pattern):

    • Docker List Containers (Execute Command)
    • Fuzzy match logic (Code node)
    • For updates: only single match supported (no batch update confirmation)
    • If 0 matches: "No container found" (can reuse suggestion flow from 03-02 if available)
    • If >1 matches: "Update requires exact container name. Found: sonarr, radarr, lidarr"
    • If 1 match: proceed to update flow
  4. Multiple Match Handler (for update only):

    const matches = $json.matches;
    const names = matches.map(m => m.Names[0].replace(/^\//, '')).join(', ');
    return {
      json: {
        message: `Update requires an exact container name.\n\nFound ${matches.length} matches: ${names}`,
        chatId: $json.chatId
      }
    };
    

    Then Send Message node.

  5. "update plex" (single match) → should proceed to update flow (may fail at execution, but routing works)

  6. "update arr" (multiple matches) → should show "requires exact name" message

  7. "update nonexistent" → should show "no container found" Update commands route correctly, single matches proceed to update flow

Task 2: Implement image pull and change detection n8n-workflow.json After single-match routing, implement the update steps:
  1. Inspect Container (Execute Command node):

    const containerId = $json.matches[0].Id;
    return {
      json: {
        cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/${containerId}/json'`,
        containerId,
        containerName: $json.matches[0].Names[0].replace(/^\//, ''),
        chatId: $json.chatId
      }
    };
    
  2. Parse Container Config (Code node): Parse the inspect output and extract what we need:

    const inspect = JSON.parse($json.stdout);
    const imageName = inspect.Config.Image;
    const currentImageId = inspect.Image;
    
    // Extract version from labels if available
    const labels = inspect.Config.Labels || {};
    const currentVersion = labels['org.opencontainers.image.version']
      || labels['version']
      || currentImageId.substring(7, 19);
    
    return {
      json: {
        imageName,
        currentImageId,
        currentVersion,
        containerConfig: inspect.Config,
        hostConfig: inspect.HostConfig,
        networkSettings: inspect.NetworkSettings,
        containerName: $json.containerName,
        containerId: $json.containerId,
        chatId: $json.chatId
      }
    };
    
  3. Pull Image (Execute Command node):

    const imageName = $json.imageName;
    return {
      json: {
        cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,
        ...($json) // Preserve all context
      }
    };
    
  4. Inspect New Image (Execute Command node):

    const imageName = $json.imageName;
    return {
      json: {
        cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`,
        ...($json)
      }
    };
    
  5. Compare Digests (Code node):

    // Parse new image inspect
    const newImage = JSON.parse($json.stdout);
    const newImageId = newImage.Id;
    const currentImageId = $('Parse Container Config').first().json.currentImageId;
    
    if (currentImageId === newImageId) {
      // No update needed - stay silent per CONTEXT.md
      return { json: { needsUpdate: false, chatId: $json.chatId } };
    }
    
    // Extract new version
    const labels = newImage.Config?.Labels || {};
    const newVersion = labels['org.opencontainers.image.version']
      || labels['version']
      || newImageId.substring(7, 19);
    
    return {
      json: {
        needsUpdate: true,
        currentImageId,
        newImageId,
        currentVersion: $('Parse Container Config').first().json.currentVersion,
        newVersion,
        containerConfig: $('Parse Container Config').first().json.containerConfig,
        hostConfig: $('Parse Container Config').first().json.hostConfig,
        networkSettings: $('Parse Container Config').first().json.networkSettings,
        containerName: $('Parse Container Config').first().json.containerName,
        containerId: $('Parse Container Config').first().json.containerId,
        chatId: $json.chatId
      }
    };
    
  6. Check If Update Needed (IF node):

    • Condition: {{ $json.needsUpdate }} equals true
    • True: proceed to recreate
    • False: do nothing (silent, no message)
  7. "update [container]" with no new image → no message sent (silent)

  8. Check workflow logs to confirm pull was attempted and digests compared Image pull works, change detection compares digests correctly

Task 3: Implement container recreation workflow n8n-workflow.json When update is needed, stop old container, remove it, create new one, start it:
  1. Stop Container (Execute Command node):

    const containerId = $json.containerId;
    return {
      json: {
        cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`,
        ...($json)
      }
    };
    
  2. Verify Stopped (Code node):

    const statusCode = parseInt($json.stdout.trim());
    if (statusCode !== 204 && statusCode !== 304) {
      return {
        json: {
          error: true,
          message: `Failed to stop container: HTTP ${statusCode}`,
          chatId: $json.chatId
        }
      };
    }
    return { json: { ...$json, stopped: true } };
    
  3. Remove Container (Execute Command node):

    const containerId = $json.containerId;
    return {
      json: {
        cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`,
        ...($json)
      }
    };
    
  4. Build Create Body (Code node): Build the container creation request from saved config:

    const config = $json.containerConfig;
    const hostConfig = $json.hostConfig;
    const networkSettings = $json.networkSettings;
    
    // Build NetworkingConfig from NetworkSettings
    const networks = {};
    for (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {
      networks[name] = {
        IPAMConfig: netConfig.IPAMConfig,
        Links: netConfig.Links,
        Aliases: netConfig.Aliases
      };
    }
    
    const createBody = {
      ...config,
      HostConfig: hostConfig,
      NetworkingConfig: {
        EndpointsConfig: networks
      }
    };
    
    // Remove fields that shouldn't be in create request
    delete createBody.Hostname; // Let Docker assign
    delete createBody.Domainname;
    
    return {
      json: {
        createBody: JSON.stringify(createBody),
        containerName: $json.containerName,
        currentVersion: $json.currentVersion,
        newVersion: $json.newVersion,
        chatId: $json.chatId
      }
    };
    
  5. Create Container (Execute Command node):

    const containerName = $json.containerName;
    const createBody = $json.createBody;
    
    // Write body to temp file to avoid shell escaping issues
    // Or use curl's -d option with proper escaping
    return {
      json: {
        cmd: `echo '${createBody.replace(/'/g, "'\\''")}' | curl -s -X POST --unix-socket /var/run/docker.sock -H "Content-Type: application/json" -d @- 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`,
        ...($json)
      }
    };
    

    Alternative approach if shell escaping is problematic:

    // Use a Code node with HTTP request instead of Execute Command
    // n8n Code nodes can make HTTP requests directly
    
  6. Parse Create Response (Code node):

    let response;
    try {
      response = JSON.parse($json.stdout);
    } catch (e) {
      return { json: { error: true, message: `Create failed: ${$json.stdout}`, chatId: $json.chatId } };
    }
    
    if (response.message) {
      // Error response
      return { json: { error: true, message: `Create failed: ${response.message}`, chatId: $json.chatId } };
    }
    
    return {
      json: {
        newContainerId: response.Id,
        currentVersion: $json.currentVersion,
        newVersion: $json.newVersion,
        containerName: $json.containerName,
        chatId: $json.chatId
      }
    };
    
  7. Start New Container (Execute Command node):

    const newContainerId = $json.newContainerId;
    return {
      json: {
        cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`,
        ...($json)
      }
    };
    
  8. Send Update Result (Telegram Send Message):

    const { containerName, currentVersion, newVersion } = $json;
    const message = `<b>${containerName}</b> updated: ${currentVersion}${newVersion}`;
    return { json: { text: message, chatId: $json.chatId } };
    
1. "update [container]" when update available → container recreated, version change message sent 2. Container restarts successfully with same ports, volumes, networks 3. Check container is running after update Container recreation workflow works, preserves configuration, reports version change End-to-end update verification:
  1. "update plex" (when update available):

    • Image pulled
    • Container stops
    • Container removed
    • New container created with same config
    • Container starts
    • Message: "plex updated: v1.32.0 → v1.32.1"
  2. "update plex" (when already up to date):

    • Image pulled
    • Digests compared
    • No further action
    • No message sent (silent per CONTEXT.md)
  3. "update arr" (multiple matches):

    • Message: "Update requires exact container name..."
  4. "update nonexistent":

    • Message: "No container found..."
  5. Post-update verification:

    • Container running
    • Same ports mapped
    • Same volumes mounted
    • Same network connections

Import updated workflow and test with a real container that has an update available.

<success_criteria>

  • Update command parses container name correctly
  • Image pull succeeds
  • Digest comparison detects changes accurately
  • Container recreation preserves Config, HostConfig, Networks
  • Version change displayed when detectable
  • Silent when no update available
  • All error cases report diagnostic details
  • Container runs correctly after update </success_criteria>
After completion, create `.planning/phases/03-container-actions/03-04-SUMMARY.md`