DEV Community

Cover image for Building a self-updating n8n instance with n8n workflows
Jonas Scholz
Jonas Scholz Subscriber

Posted on • Originally published at sliplane.io

6 5 5 5 4

Building a self-updating n8n instance with n8n workflows

Self-hosting n8n is absolutely fantastic. You have full control over your data, your workflows, and your integrations. BUT you know what is really annoying? Updating your n8n instance everytime a new version is released, right when you want to get some work done. Okay I'm a bit dramatic here, but I don't want to manually update my software, we're not in 2005 anymore!

In this blog post I will show you how to automate the process of updating your n8n instance with a n8n workflow! I'd suggest watching the video below and then come back to copy the workflow/code:

The workflow

So the gist of the workflow is:

  1. Check the current version of n8n
  2. Check the latest version of n8n on Docker Hub
  3. Compare the two versions
  4. If there is a new version, trigger a new deployment

That looks kinda like this:

workflow

Let's break down the workflow:

Step 1. Check current version

n8n has a public API endpoint at /metrics that returns metrics (duh) about the instance. You will have to enable the API in the settings first. Do that by setting the N8N_METRICS environment variable to true. Requesting the /metrics endpoint will return a prometheus formatted response that looks something like this:

# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
n8n_process_cpu_user_seconds_total 28.518922

# HELP nodejs_heap_space_size_total_bytes Process heap space size total from Node.js in bytes.
# TYPE nodejs_heap_space_size_total_bytes gauge
n8n_nodejs_heap_space_size_total_bytes{space="read_only"} 0
n8n_nodejs_heap_space_size_total_bytes{space="new"} 1048576
n8n_nodejs_heap_space_size_total_bytes{space="old"} 122646528
n8n_nodejs_heap_space_size_total_bytes{space="code"} 6553600
n8n_nodejs_heap_space_size_total_bytes{space="shared"} 0
n8n_nodejs_heap_space_size_total_bytes{space="new_large_object"} 0
n8n_nodejs_heap_space_size_total_bytes{space="large_object"} 7479296
n8n_nodejs_heap_space_size_total_bytes{space="code_large_object"} 155648
n8n_nodejs_heap_space_size_total_bytes{space="shared_large_object"} 0

# HELP nodejs_heap_space_size_used_bytes Process heap space size used from Node.js in bytes.
# TYPE nodejs_heap_space_size_used_bytes gauge
n8n_nodejs_heap_space_size_used_bytes{space="read_only"} 0
n8n_nodejs_heap_space_size_used_bytes{space="new"} 194144
n8n_nodejs_heap_space_size_used_bytes{space="old"} 112432088
n8n_nodejs_heap_space_size_used_bytes{space="code"} 5740432
n8n_nodejs_heap_space_size_used_bytes{space="shared"} 0

# HELP n8n_version_info n8n version info.
# TYPE n8n_version_info gauge
n8n_version_info{version="v1.88.0",major="1",minor="88",patch="0"} 1

# HELP n8n_active_workflow_count Total number of active workflows.
# TYPE n8n_active_workflow_count gauge
n8n_active_workflow_count 1
Enter fullscreen mode Exit fullscreen mode

I removed most of the metrics, but the important line is:

n8n_version_info{version="v1.88.0",major="1",minor="88",patch="0"} 1
Enter fullscreen mode Exit fullscreen mode

We can get that data by making a HTTP request node, and then parsing the response with a javascript code node.

The code to parse the response is:

const match = $input.first().json['data'].match(/n8n_version_info\{[^}]*version="(v[\d.]+)"/);
const version = match ? match[1] : '';

$input.first().json.version = version.slice(1);

return $input.all();
Enter fullscreen mode Exit fullscreen mode

If you need an explanation of the code I'd recommend giving it to ChatGPT :)

Step 2: Check the latest version

Now that we have the current version, we can check the latest version on Docker Hub.

For that, we use the public API of Docker Hub that is reachable at https://hub.docker.com/v2/repositories/n8nio/n8n/tags?ordering=last_updated

The output looks something like this:

{
  "count": 2013,
  "next": "https://hub.docker.com/v2/repositories/n8nio/n8n/tags?ordering=last_updated&page=2&page_size=10",
  "previous": null,
  "results": [
    {
      "creator": 6760745,
      "id": 877879608,
      "images": [
        {
          "architecture": "amd64",
          "features": "",
          "variant": null,
          "digest": "sha256:409baee827dee86faf5d7bda3caa0d2e534b1d4b21ecd67e8bd93e46ed750a83",
          "os": "linux",
          "os_features": "",
          "os_version": null,
          "size": 205454040,
          "status": "active",
          "last_pulled": "2025-04-11T19:42:09.702608133Z",
          "last_pushed": "2025-04-07T14:02:23.547489138Z"
        },
        {
          "architecture": "arm64",
          "features": "",
          "variant": null,
          "digest": "sha256:5b0d2604164885591c84aec73dc369d27f48d22e3afa6effbdce71a43058c8dd",
          "os": "linux",
          "os_features": "",
          "os_version": null,
          "size": 206102002,
          "status": "active",
          "last_pulled": "2025-04-11T15:28:05.725345972Z",
          "last_pushed": "2025-04-07T14:02:24.035538759Z"
        }
      ],
      "last_updated": "2025-04-07T14:02:24.799601Z",
      "last_updater": 6760745,
      "last_updater_username": "n8nio",
      "name": "1.87.0",
      "repository": 7303950,
      "full_size": 205454040,
      "v2": true,
      "tag_status": "active",
      "tag_last_pulled": "2025-04-11T21:11:11.70474394Z",
      "tag_last_pushed": "2025-04-07T14:02:24.799601Z",
      "media_type": "application/vnd.docker.distribution.manifest.list.v2+json",
      "content_type": "image",
      "digest": "sha256:55f76b8f0007ef6a73f909a5dcc0e79ffeae19ffa35cbe4bb117d87ae6771096"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

I removed most of the data, but for each version you get one object with a name property that is the license. We take that data and parse it to compare it with our current version. If there is a new version, we save it and forward it to the next node:

const data = $input.first();
const dockerData = data.json.results;
const currentVersion = $input.last().json.version;

function parseVersion(str) {
  const match = str.match(/^(\d+)\.(\d+)\.(\d+)$/);
  return match ? match.slice(1).map(Number) : null;
}

function isNewer(a, b) {
  for (let i = 0; i < 3; i++) {
    if (a[i] > b[i]) return true;
    if (a[i] < b[i]) return false;
  }
  return false;
}

const currentParsed = parseVersion(currentVersion);
let newestParsed = null;
let newVersion = '';

for (const tag of dockerData) {
  const parsed = parseVersion(tag.name);
  if (!parsed) continue;

  if (isNewer(parsed, currentParsed)) {
    if (!newestParsed || isNewer(parsed, newestParsed)) {
      newestParsed = parsed;
      newVersion = tag.name;
    }
  }
}

return [{ newVersion }];
Enter fullscreen mode Exit fullscreen mode

Step 3: Trigger a new deployment

In my case I am hosting on sliplane.io where I can trigger a new deployment by making a HTTP request to a secret deploy hook endpoint.

The url looks something like this: https://api.sliplane.io/deploy/service_v9w2dkz11xv3/long-secret-value?tag=1.88.0, where tag can be replaced with the new version!

Even if you're not using sliplane, many other hosting providers offer a similar feature. In the case of Sliplane, this will update the docker image backing your service and restart it.

If you want to host n8n on sliplane.io (9 euros per month), check out my previous blog post here. Only takes 2 minutes to get started!

The complete workflow

Now that you know how the workflow works, here is the complete workflow in JSON format. Make sure to replace the url with your own n8n instance url and the secret value with your own deploy hook secret.

{
  "name": "Update n8n",
  "nodes": [
    {
      "parameters": {
        "url": "https://your-n8n-instance.sliplane.app/metrics",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        80,
        200
      ],
      "id": "dd3ee216-c720-4a0a-b291-b6e067370658",
      "name": "Request Metrics"
    },
    {
      "parameters": {
        "jsCode": "const match = $input.first().json['data'].match(/n8n_version_info\\{[^}]*version=\"(v[\\d.]+)\"/);\nconst version = match ? match[1] : '';\n\n$input.first().json.version = version.slice(1);\n\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        200
      ],
      "id": "833a502c-d90d-4623-808c-06b7ff1361db",
      "name": "Parse Version"
    },
    {
      "parameters": {
        "url": "https://hub.docker.com/v2/repositories/n8nio/n8n/tags?ordering=last_updated",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        300,
        0
      ],
      "id": "4166069d-c969-4f90-897b-39637822c971",
      "name": "Request DockerHub"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.1,
      "position": [
        520,
        100
      ],
      "id": "362ccb3b-f370-44f7-b8b4-84870d002e45",
      "name": "Merge"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "e4c8b5e2-f4fa-4b83-b22e-0ffbf3ae5ccd",
              "leftValue": "={{ $json.newVersion }}",
              "rightValue": "\"\"",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        960,
        100
      ],
      "id": "83295a47-3411-4fa6-8bbc-9a4e22dec9c3",
      "name": "Has New Version"
    },
    {
      "parameters": {
        "url": "https://api.sliplane.io/health",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "tag",
              "value": "={{ $json.newVersion }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1180,
        100
      ],
      "id": "c0687380-0190-40dc-bb11-402384e289ef",
      "name": "Trigger Redeploy"
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first();\nconst dockerData = data.json.results;\nconst currentVersion = $input.last().json.version;\n\nfunction parseVersion(str) {\n  const match = str.match(/^(\\d+)\\.(\\d+)\\.(\\d+)$/);\n  return match ? match.slice(1).map(Number) : null;\n}\n\nfunction isNewer(a, b) {\n  for (let i = 0; i < 3; i++) {\n    if (a[i] > b[i]) return true;\n    if (a[i] < b[i]) return false;\n  }\n  return false;\n}\n\nconst currentParsed = parseVersion(currentVersion);\nlet newestParsed = null;\nlet newVersion = '';\n\nfor (const tag of dockerData) {\n  const parsed = parseVersion(tag.name);\n  if (!parsed) continue;\n\n  if (isNewer(parsed, currentParsed)) {\n    if (!newestParsed || isNewer(parsed, newestParsed)) {\n      newestParsed = parsed;\n      newVersion = tag.name;\n    }\n  }\n}\n\nreturn [{ newVersion }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        100
      ],
      "id": "d0c734c2-11e5-4048-9cc7-23ff11678502",
      "name": "Find New Version"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 6
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -140,
        100
      ],
      "id": "cd017a87-0524-49c0-943f-2f03ed904e7b",
      "name": "Schedule Trigger"
    }
  ],
  "pinData": {},
  "connections": {
    "Request Metrics": {
      "main": [
        [
          {
            "node": "Parse Version",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request DockerHub": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Version": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Find New Version",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has New Version": {
      "main": [
        [
          {
            "node": "Trigger Redeploy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find New Version": {
      "main": [
        [
          {
            "node": "Has New Version",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Request Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Request DockerHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1ab693ad-d248-4658-90a1-2414bfac0b02",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "d27af88f79cf5acec70fbd96be12ba4f4c37c61aa5c8774eab25678db572187b"
  },
  "id": "aZJ6zGrX9fPOM0RM",
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Cheers,

Jonas Co-Founder sliplane.io

Neon image

Resources for building AI applications with Neon Postgres 🤖

Core concepts, starter applications, framework integrations, and deployment guides. Use these resources to build applications like RAG chatbots, semantic search engines, or custom AI tools.

Explore AI Tools →

Top comments (2)

Collapse
 
wimadev profile image
Lukas Mauser • Edited

Cool Workflow! I suggest a modification:
Using the latest tag and run a redeploy every night/ once per week, when no other worklflow is running 👀

Collapse
 
code42cate profile image
Jonas Scholz

That was actually on purpose with the specific tags for future expansion! I would like to fetch the changelog of the version and then have an AI node check if there are breaking changes so I don’t accidentally brick stuff. With latest that would be harder to achieve

Billboard image

Try REST API Generation for Snowflake

DevOps for Private APIs. Automate the building, securing, and documenting of internal/private REST APIs with built-in enterprise security on bare-metal, VMs, or containers.

  • Auto-generated live APIs mapped from Snowflake database schema
  • Interactive Swagger API documentation
  • Scripting engine to customize your API
  • Built-in role-based access control

Learn more

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay