{
  "name": "Self-healing containers with escalation",
  "nodes": [
    {
      "parameters": {
        "actions": [
          "die"
        ],
        "filters": {
          "label": "selfheal=true"
        },
        "eventTypes": [
          "container"
        ],
        "catchUp": true
      },
      "id": "b1000000-0000-4000-8000-000000000001",
      "name": "A container died",
      "type": "n8n-nodes-docker-api.dockerTrigger",
      "typeVersion": 1,
      "position": [
        -656,
        0
      ],
      "notes": "Opt in per container with the label selfheal=true, so this never touches something you did not mean it to. Catch-up is on: a container that dies while n8n itself is restarting is still handled, which is exactly when it tends to happen."
    },
    {
      "parameters": {
        "operation": "getLogs",
        "containerId": "={{ $json.name }}",
        "tail": 50,
        "timestamps": true,
        "retryPolicy": {},
        "resource": "container",
        "stream": "both",
        "sinceMinutes": 0
      },
      "id": "b1000000-0000-4000-8000-000000000002",
      "name": "Capture logs BEFORE restarting",
      "type": "n8n-nodes-docker-api.docker",
      "typeVersion": 1,
      "position": [
        -448,
        0
      ],
      "onError": "continueRegularOutput",
      "notes": "Taken first, deliberately. Restarting can rotate or discard the very output that explains the crash, so the evidence is collected while it still exists."
    },
    {
      "parameters": {
        "operation": "inspect",
        "containerId": "={{ $('A container died').item.json.name }}",
        "retryPolicy": {},
        "resource": "container"
      },
      "id": "b1000000-0000-4000-8000-000000000003",
      "name": "How many times has it restarted?",
      "type": "n8n-nodes-docker-api.docker",
      "typeVersion": 1,
      "position": [
        -224,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Restarting forever hides a real fault. After a few attempts, stop trying and\n// tell a human instead \u2014 a container that has died five times in a row is not\n// going to be fixed by a sixth restart.\nconst MAX_RESTARTS = 5;\n\nconst inspected = $input.first().json;\nconst event = $('A container died').first().json;\nconst logs = $('Capture logs BEFORE restarting').first().json;\n\nconst restartCount = inspected.state?.restartCount ?? 0;\nconst logLines = (logs.logs ?? [])\n  .map((l) => `[${l.stream}] ${l.message}`)\n  .join('\\n');\n\nreturn [{\n  json: {\n    container: event.name,\n    image: event.image ?? inspected.image ?? null,\n    exitCode: event.attributes?.exitCode ?? null,\n    diedAt: event.time ?? null,\n    restartCount,\n    giveUp: restartCount >= MAX_RESTARTS,\n    logTail: logLines || '(no output captured)',\n  },\n}];"
      },
      "id": "b1000000-0000-4000-8000-000000000004",
      "name": "Worth another try?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "g1",
              "leftValue": "={{ $json.giveUp }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000005",
      "name": "Under the restart limit?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        224,
        0
      ]
    },
    {
      "parameters": {
        "operation": "start",
        "containerId": "={{ $json.container }}",
        "retryPolicy": {},
        "resource": "container"
      },
      "id": "b1000000-0000-4000-8000-000000000006",
      "name": "Restart it",
      "type": "n8n-nodes-docker-api.docker",
      "typeVersion": 1,
      "position": [
        448,
        -128
      ]
    },
    {
      "parameters": {
        "operation": "waitForState",
        "containerId": "={{ $('Worth another try?').item.json.container }}",
        "retryPolicy": {},
        "resource": "container",
        "targetState": "running",
        "waitTimeout": 60
      },
      "id": "b1000000-0000-4000-8000-000000000007",
      "name": "Confirm it stayed up",
      "type": "n8n-nodes-docker-api.docker",
      "typeVersion": 1,
      "position": [
        672,
        -128
      ],
      "notes": "Starting is not the same as staying started. If the container has a health check, change the target to 'healthy' for a much stronger guarantee."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "r1",
              "leftValue": "={{ $json.reached }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000008",
      "name": "Did it recover?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        -128
      ]
    },
    {
      "parameters": {},
      "id": "b1000000-0000-4000-8000-000000000009",
      "name": "Recovered",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1104,
        -240
      ],
      "notes": "Worth a quiet notification rather than a loud one \u2014 but do send something. A container silently restarting nightly is a fault you want to know about eventually."
    },
    {
      "parameters": {},
      "id": "b1000000-0000-4000-8000-000000000010",
      "name": "ALERT: restart did not hold",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1104,
        -16
      ],
      "notes": "Replace with Slack, Discord or email. The item already carries container, exitCode, restartCount and logTail \u2014 everything needed to triage without opening a terminal."
    },
    {
      "parameters": {},
      "id": "b1000000-0000-4000-8000-000000000011",
      "name": "ALERT: giving up, needs a human",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        448,
        144
      ],
      "notes": "The restart limit was reached. Escalate loudly and stop restarting: past this point a restart loop only buries the cause and burns CPU."
    },
    {
      "parameters": {
        "content": "## \ud83c\udff7\ufe0f Containers opt in\n\nAdd the label `selfheal=true` to any container you want restarted automatically:\n\n```\ndocker run -l selfheal=true ...\n```\n\nNothing else is touched, so this can never restart something you stopped on purpose.",
        "height": 598,
        "width": 204,
        "color": 4
      },
      "id": "b9000000-0000-4000-8000-000000000001",
      "name": "Opt in per container",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -704,
        -432
      ]
    },
    {
      "parameters": {
        "content": "## \ud83d\udd0d Logs are taken first\n\nDeliberately **before** the restart. Restarting can rotate or discard the very output that explains the crash.\n\nThe alert carries the last 50 lines, each labelled with the stream it came from.",
        "height": 438,
        "width": 840,
        "color": 5
      },
      "id": "b9000000-0000-4000-8000-000000000002",
      "name": "Evidence before action",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        -272
      ]
    },
    {
      "parameters": {
        "content": "## \u2705 Starting is not recovering\n\nIf your container defines a health check, change **Confirm it stayed up** to wait for `healthy` instead of `running` \u2014 a much stronger guarantee.",
        "height": 294,
        "width": 588,
        "color": 6
      },
      "id": "b9000000-0000-4000-8000-000000000003",
      "name": "Confirm it held",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        432,
        -272
      ]
    },
    {
      "parameters": {
        "content": "\n\n\n\n\n\n\n\n\n## \ud83d\uded1 It gives up after 5 tries\n\nA container that has died five times will not be fixed by a sixth restart, and a restart loop buries the cause while burning CPU.\n\nChange the limit in **Worth another try?**",
        "height": 424,
        "width": 420,
        "color": 3
      },
      "id": "b9000000-0000-4000-8000-000000000004",
      "name": "Know when to stop",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        384,
        112
      ]
    }
  ],
  "connections": {
    "A container died": {
      "main": [
        [
          {
            "node": "Capture logs BEFORE restarting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture logs BEFORE restarting": {
      "main": [
        [
          {
            "node": "How many times has it restarted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "How many times has it restarted?": {
      "main": [
        [
          {
            "node": "Worth another try?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Worth another try?": {
      "main": [
        [
          {
            "node": "Under the restart limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Under the restart limit?": {
      "main": [
        [
          {
            "node": "Restart it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ALERT: giving up, needs a human",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restart it": {
      "main": [
        [
          {
            "node": "Confirm it stayed up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm it stayed up": {
      "main": [
        [
          {
            "node": "Did it recover?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Did it recover?": {
      "main": [
        [
          {
            "node": "Recovered",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ALERT: restart did not hold",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}