<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: R</title>
    <description>The latest articles on DEV Community by R (@hubschrauber).</description>
    <link>https://dev.to/hubschrauber</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2070503%2F23116ff1-1582-447c-9d9b-6d2555b4c908.jpg</url>
      <title>DEV Community: R</title>
      <link>https://dev.to/hubschrauber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hubschrauber"/>
    <language>en</language>
    <item>
      <title>Using Discourse to Collaborate on n8n Workflows</title>
      <dc:creator>R</dc:creator>
      <pubDate>Wed, 01 Oct 2025 04:08:50 +0000</pubDate>
      <link>https://dev.to/hubschrauber/using-discourse-to-collaborate-on-n8n-workflows-3492</link>
      <guid>https://dev.to/hubschrauber/using-discourse-to-collaborate-on-n8n-workflows-3492</guid>
      <description>&lt;p&gt;This should have been simple to set up, and if it were, I wouldn't be writing this.  If you've spent any time on &lt;a href="https://community.n8n.io" rel="noopener noreferrer"&gt;the n8n community forums&lt;/a&gt; then you've probably seen a really nifty feature where a forum post will render a workflow, right in the post, as if you had a mini-n8n instance embedded there to let you poke around in the workflow-node settings, copy part of the posted workflow for your own use, zoom in and pan around to take a closer look at things, etc.  That's interesting on its own, but you can also set that up for your own internal purposes.  For instance, if you wanted to enable a development team to share tips and workflow fragments that cannot (or should not) be shared on a public forum, you could set up a private instance of the same software n8n uses for their community forums.&lt;/p&gt;

&lt;p&gt;Enough about the what and why... on to the how...&lt;/p&gt;

&lt;h2&gt;
  
  
  Discourse
&lt;/h2&gt;

&lt;p&gt;Discourse is the software used by n8n for their community forums, and if you've spent much time searching/reading online discussions lately, you'll recognize Discourse as a VERY common choice.  I'll have to limit the coverage of installing Discourse to "pre-requisite" status in this article, because the Discourse install/setup is a convoluted mess that has already been &lt;a href="https://meta.discourse.org/t/can-discourse-ship-frequent-docker-images-that-do-not-need-to-be-bootstrapped/33205" rel="noopener noreferrer"&gt;argued to death&lt;/a&gt; and it just can't be summarized into a few simple steps.  However, you'll find information about how to get Discourse running &lt;a href="https://github.com/discourse/discourse/blob/main/docs/INSTALL-cloud.md#6-install-discourse" rel="noopener noreferrer"&gt;here&lt;/a&gt; (good luck).  Once you have that done, and you're logged in as an administrator, you're ready to continue setting up the n8n-demo component and plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n-demo component
&lt;/h2&gt;

&lt;p&gt;Before jumping through the hoops to integrate the demo component into your Discourse server, it will probably help to check it out standalone.  To do that, you just need to grab the source JSON from a workflow in n8n, and follow the instructions &lt;a href="https://n8n-io.github.io/n8n-demo-webcomponent/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.  Getting that working amounts to adding a few &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags and a &lt;code&gt;&amp;lt;n8n-demo&amp;gt;&lt;/code&gt; tag to an html page, and loading it in a browser.  This isn't a huge challenge, but it will probably help to see that in action to understand better how the component works within Discourse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discourse plugin
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/n8n-io/discourse-n8n-wf" rel="noopener noreferrer"&gt;discourse-n8n-wf plugin&lt;/a&gt; is a wrapper for (part of) the n8n-demo component, which enables it in a Discourse forum post by intercepting the handling of a markdown code-block.  Getting the plugin installed and configured in Discourse required quite a bit of guessing and reading between the lines in the Discourse documentation &lt;strong&gt;and&lt;/strong&gt; the plugin documentation (which is a bit sparse).  Also, heads up, the plugin is &lt;strong&gt;only&lt;/strong&gt; a wrapper for the &lt;code&gt;&amp;lt;n8n-demo&amp;gt;&lt;/code&gt; tag part of the &lt;code&gt;n8n-demo component&lt;/code&gt;;  the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags need to be configured separately in the Discourse admin dashboard to get everything working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;p&gt;I couldn't find anywhere that had all of this in one place, so that's the main point of this article.  Hoping this serves to eliminate some head scratching (or pounding) for someone.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up Discourse, register the admin login, and be sure you can get to the Admin Dashboard

&lt;ul&gt;
&lt;li&gt;Click your &lt;strong&gt;Avatar&lt;/strong&gt; -&amp;gt; Click the Discourse UI -&amp;gt; Avatar -&amp;gt; person icon -&amp;gt; Preferences -&amp;gt; Admin button -&amp;gt; Click &lt;strong&gt;Preferences&lt;/strong&gt; -&amp;gt; Click the &lt;strong&gt;Admin button&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add the plugin to the Discourse app.yml (find this block, and add the last line shown here)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git
          - git clone https://github.com/n8n-io/discourse-n8n-wf.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Rebuild the Discourse container

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;launcher rebuild app&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Verify that the plugin is installed.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard&lt;/strong&gt; -&amp;gt; &lt;strong&gt;PLUGINS&lt;/strong&gt; left-menu -&amp;gt; &lt;strong&gt;Installed Plugins&lt;/strong&gt; sub-menu -&amp;gt; Search "N8n wf"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configure the plugin in 2 places, neither of which is in the grayed-out "Settings" button on the "Installed Plugins" list item.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard&lt;/strong&gt; -&amp;gt; &lt;strong&gt;SECURITY&lt;/strong&gt; left-menu -&amp;gt; &lt;strong&gt;Security&lt;/strong&gt; sub-menu -&amp;gt; filter: "iframes"&lt;/li&gt;
&lt;li&gt;Add an "Allowed iframes" item with &lt;code&gt;https://my-n8n-server.example.com/workflows/demo&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;/workflows/demo&lt;/code&gt; part is fixed&lt;/li&gt;
&lt;li&gt;match the protocol (http or https) and host name to your own server.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard&lt;/strong&gt; -&amp;gt; &lt;strong&gt;All site settings&lt;/strong&gt; left-menu -&amp;gt; filter: "N8n-core-url"&lt;/li&gt;
&lt;li&gt;Fill in just the base URL matching the "Allowed iframes" URL above, like &lt;code&gt;https://my-n8n-server.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Note: Couldn't find a way to navigate directly to this setting, so it's sorta hidden / hard to find.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add a theme "component" to all themes with the script tags for the &lt;code&gt;n8n-demo component&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard&lt;/strong&gt; -&amp;gt; &lt;strong&gt;APPEARANCE&lt;/strong&gt; left-menu -&amp;gt; &lt;strong&gt;Themes &amp;amp; compo...&lt;/strong&gt; sub-menu -&amp;gt; &lt;strong&gt;Components&lt;/strong&gt; "tab"&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Install&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;+ Create new&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;Enter Name: &lt;code&gt;n8n-demo component&lt;/code&gt; (or a similar name you like with at least 4 characters apparently)&lt;/li&gt;
&lt;li&gt;Leave "Component" selected as the type.&lt;/li&gt;
&lt;li&gt;Click the &lt;code&gt;Create&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Add all themes&lt;/code&gt; and then the green check button.&lt;/li&gt;
&lt;li&gt;Click the &lt;code&gt;Edit Code&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;Switch to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tab&lt;/li&gt;
&lt;li&gt;Paste in the script tags from &lt;a href="https://n8n-io.github.io/n8n-demo-webcomponent/install/" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;code&gt;Save&lt;/code&gt; button&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Navigate back to the forum&lt;/li&gt;
&lt;li&gt;To verify that the rendering component is working, create a post with workflow JSON enclosed between 2 separate lines with the markdown code-block (3-back-tic) delimiters like:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;```&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "ad04e21c-5d86-4463-8f03-0cd375224997",
      "name": "When clicking ‘Test workflow’"
    },
    {
      "parameters": {
        "url": "https://httpbin.org/get",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "pure",
              "value": "magic"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "id": "10394cdf-c137-4cc9-8f17-c1510bb5380b",
      "name": "HTTP Request"
    }
  ],
  "connections": {
    "When clicking ‘Test workflow’": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "instanceId": "1234"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;```&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering On Your Own n8n
&lt;/h2&gt;

&lt;p&gt;By default, the n8n-demo tag generated by the n8n-discourse-wf plugin submits the workflow source to &lt;code&gt;https://n8n-preview-service.internal.n8n.cloud/workflows/demo&lt;/code&gt;, via iframe, to be rendered in graphical (editor) form.  Not only is this a potential "leak" of your workflow data, but it consumes resources that n8n hosts to support their own community forums, and is an external element over which you have no control, for capacity, availability, or security.  There is currently no official, documented way to override this, but it should be possible to change the &lt;code&gt;src&lt;/code&gt; attribute and point it instead to the URL specified in the &lt;code&gt;N8n-core-url&lt;/code&gt; / &lt;code&gt;Allowed iframes&lt;/code&gt; settings mentioned above.  I'll add an update here when/if I find a way to do that.  Please leave a comment if you find a way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UPDATE: When Discourse installs the plugin, it clones the files from a git repository.  One way to override the src attribute in the &lt;code&gt;&amp;lt;n8n-demo&amp;gt;&lt;/code&gt; tag is to fork the plugin repo and edit the code to hard code the URL of the n8n instance where rendering should be done.&lt;/li&gt;
&lt;li&gt;UPDATE: There is an environment variable that tells n8n to run in "preview mode" &lt;a href="https://docs.n8n.io/hosting/configuration/environment-variables/deployment/#:~:text=N8N_PREVIEW_MODE" rel="noopener noreferrer"&gt;here&lt;/a&gt; which is necessary to make the &lt;code&gt;/workflows/demo&lt;/code&gt; URL path work without requiring a login.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>discourse</category>
      <category>n8n</category>
      <category>docker</category>
      <category>collaborate</category>
    </item>
    <item>
      <title>Wemos D1 Mini w/ Waveshare e-Paper 2.13 HAT</title>
      <dc:creator>R</dc:creator>
      <pubDate>Tue, 16 Sep 2025 00:20:37 +0000</pubDate>
      <link>https://dev.to/hubschrauber/wemos-d1-mini-w-waveshare-e-paper-213-hat-laf</link>
      <guid>https://dev.to/hubschrauber/wemos-d1-mini-w-waveshare-e-paper-213-hat-laf</guid>
      <description>&lt;p&gt;AI flat-out lied to me several times while trying to get this working, so now I guess I'm giving AI something to read to (maybe... but I doubt it) get it right when someone else goes looking for this specific info.&lt;/p&gt;

&lt;p&gt;The title already tells you this will cover a fairly specific set of circumstances.  I imagine it could be expanded to apply to similar things, but I'll bet this is an exact match for what someone is looking for.&lt;/p&gt;

&lt;p&gt;So... I was trying to line up all the planets to get a Wemos D1 Mini working with a Waveshare 2.13" e-Paper HAT display.  If you found this, you probably already know what a nightmare it can be to figure out which pins to use on a microcontroller / development board, since it is apparently just too much to ask that any 2 given examples of hardware or software reference anything by the same names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciling Pin Names
&lt;/h2&gt;

&lt;p&gt;For each of the following where a D1 Mini pin connects to a line on the e-Paper HAT, the convention is SPI_Common_Name / D1_Mini_Label ~ D1_Mini_AltLabel (if any) / Raw_Pin / GPIO_Pin_Name / Arduino_Macro_Constant (if any) -&amp;gt; Harness_Wire_Color -&amp;gt; e-Paper HAT Pin/Label&lt;/p&gt;

&lt;h3&gt;
  
  
  +3.3v -&amp;gt; Gray -&amp;gt; VCC
&lt;/h3&gt;

&lt;p&gt;Supply and signal voltage should be 3.3v for everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ground -&amp;gt; Brown -&amp;gt; GND
&lt;/h3&gt;

&lt;p&gt;Be sure the ground on the D1 Mini and the e-Paper HAT are connected to each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  MOSI/MOSI~13/13/GPIO13/D7 -&amp;gt; Blue -&amp;gt; DIN (SDI)
&lt;/h3&gt;

&lt;p&gt;The first source of confusion is that the interface for the e-Paper HAT is SPI, but data only flows &lt;strong&gt;from&lt;/strong&gt; the microcontroller &lt;strong&gt;to&lt;/strong&gt; the display,  so there is DIN, but no DOUT/SDO to connect to MISO.  DIN (Data-IN, sometimes labeled SDI for "Serial Data In") connects to the pin that has traditionally been called MOSI.  In fact, the screen print on the D1 Mini labels it as such (and also 13).  The D7 designation is from the confusing Arduino pins naming system.  This and other pins are sometimes referenced using the "digital pin" number so this applies to some of the other connections too.  In code, you should be able to literally specify &lt;code&gt;13&lt;/code&gt; and you &lt;strong&gt;may&lt;/strong&gt; find that there is a &lt;a href="https://gcc.gnu.org/onlinedocs/cpp/Macros.html" rel="noopener noreferrer"&gt;macro&lt;/a&gt; named &lt;code&gt;D7&lt;/code&gt; that substitutes to &lt;code&gt;13&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  SCLK/SCK~14/14/GPIO14/D5 -&amp;gt; Yellow -&amp;gt; CLK
&lt;/h3&gt;

&lt;p&gt;This is assumed within the GxEDP2 library.  Sketch code doesn't need to reference this, but the hardware needs to be connected correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  SS/SS~15/15/GPIO15/D8 -&amp;gt; Orange -&amp;gt; CS
&lt;/h3&gt;

&lt;p&gt;This is another unnecessary name change from the traditional abbreviation.  CS supposedly stands for "Chip Select" or some references may say it means "Channel Select"  In any case, it's the part of SPI that allows daisy-chaining multiple devices.  Oops... shouldn't have mentioned anything involving a chain either.  Word/thought police are en-route for sure now.&lt;/p&gt;

&lt;h3&gt;
  
  
  ???/0/0/GPIO0/D3 -&amp;gt; Green -&amp;gt; DC
&lt;/h3&gt;

&lt;p&gt;This is another source of confusion since the "DC" or "Data/Command" pin isn't part of a normal SPI connection.  This is apparently part of the protocol for controlling the e-Paper HAT where the driver can tell the display device whether the data being sent to DIN is Data, or a Command.&lt;/p&gt;

&lt;h3&gt;
  
  
  ???/2/2/GPIO2/D4 -&amp;gt; White -&amp;gt; RST
&lt;/h3&gt;

&lt;p&gt;This pin is what the driver uses to reset the e-Paper HAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  ???/4~SDA/4/GPIO4/D2 -&amp;gt; Purple -&amp;gt; BUSY
&lt;/h3&gt;

&lt;p&gt;This pin is what the driver uses to determine if the e-Paper HAT is ready to receive more data or commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware Connection Special Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;D8/GPIO15 pull-down.  Without a 10K resistor connecting this pin to ground, the level converters in the "HAT" board will probably interfere with the D1 Mini boot process.&lt;/li&gt;
&lt;li&gt;D4/GPIO2 pull-up.  Without a 1K resistor connecting this pin to +3.3v, the level converters in the "HAT" board will probably interfere with the D1 Mini load/programming/reset process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;

&lt;p&gt;Once you have the hardware hooked up, you'll need a working example of the code.  Instead of duplicating the details here, you can check all that out in &lt;a href="https://github.com/rthomas67/wemos_d1_mini_ws_epaper213" rel="noopener noreferrer"&gt;this git repository&lt;/a&gt; .&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The README.md file there has links to libraries and other references that might help too.&lt;/li&gt;
&lt;li&gt;Note: The source is set up for use in VSCode with the excellent PlatformIO extension, not the Arduino IDE.  If you haven't tried that yet, you don't know what you're missing... give it a go.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>arduino</category>
      <category>esp8266</category>
      <category>cpp</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Using AWS SDK from n8n Code Node</title>
      <dc:creator>R</dc:creator>
      <pubDate>Sun, 18 May 2025 21:00:43 +0000</pubDate>
      <link>https://dev.to/hubschrauber/using-aws-sdk-from-n8n-code-node-id8</link>
      <guid>https://dev.to/hubschrauber/using-aws-sdk-from-n8n-code-node-id8</guid>
      <description>&lt;h2&gt;
  
  
  HTTP Request vs. JS Client Code
&lt;/h2&gt;

&lt;p&gt;Many of the service-integration nodes in n8n are just customized wrappers for a RESTful, http based API.  If the entire API is not supported in a particular service-integration node, it is often possible to use an &lt;code&gt;HTTP Request&lt;/code&gt; node to implement other RESTful service API calls.  It is even possible to "borrow" the credentials item from the service-integration node and use it in the &lt;code&gt;HTTP Request&lt;/code&gt; node.&lt;/p&gt;

&lt;p&gt;Some services, like (Amazon) AWS services are (apparently) are too complex to support with a simple http based client, so they generally require the use of an SDK client library, and cannot be implemented within n8n by simply using an &lt;code&gt;HTTP Request&lt;/code&gt; node.  n8n also has a &lt;code&gt;Code&lt;/code&gt; node that can use npm based libraries/modules with a bit of special configuration in the n8n runtime environment.  This article shows how to use AWS SDK libraries in an n8n &lt;code&gt;Code&lt;/code&gt; node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;n8n self-hosted, preferably running in Docker / Docker Compose&lt;/li&gt;
&lt;li&gt;AWS Account + AWS IAM User + AWS Access Key for the IAM User&lt;/li&gt;
&lt;li&gt;Basic knowledge of Javascript and JS Object Notation&lt;/li&gt;
&lt;li&gt;Basic knowledge of Docker, &lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker compose&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Overview / Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get an Access Key &amp;amp; Access Secret for an AWS IAM User.&lt;/li&gt;
&lt;li&gt;Assign Permissions (Policies), to the IAM User, for the SDK to be used.&lt;/li&gt;
&lt;li&gt;Create an n8n container with the SDK npm libraries installed.&lt;/li&gt;
&lt;li&gt;Configure the Docker runtime (compose) with required environment vars.&lt;/li&gt;
&lt;li&gt;Use an n8n &lt;code&gt;Code&lt;/code&gt; node to run SDK client code in a workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Example SDK: Polly Text-To-Speech
&lt;/h2&gt;

&lt;p&gt;To demonstrate the use of an AWS SDK library, and handling a binary data response, the example use-case here will call the Polly Text-to-Speech (TTS) service to generate an audio file from text input.  The general approach to installing, enabling and using AWS SDK libraries should be applicable to other SDKs, but it's sometimes difficult to understand instructions given in generalized/abstract terms, so hopefully it will be easier to understand this concrete example.&lt;/p&gt;

&lt;h3&gt;
  
  
  SDK Access Policy
&lt;/h3&gt;

&lt;p&gt;Before using any of the various AWS services via SDK, the authenticated user needs to have access to the service.  AWS manages this primarily through policies.  Access to the Polly API service can be granted by adding the &lt;code&gt;AWSPollyFullAccess&lt;/code&gt; policy to the IAM user to which the Access Key/Secret belongs.  (Links below to the AWS user/policy management page.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the n8n Runtime Environment
&lt;/h2&gt;

&lt;p&gt;This assumes n8n is running self-hosted, in a Docker container.  There are 3 elements of the n8n runtime environment that need to be customized in order to get an AWS SDK client to work.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the SDK module/library, and its dependencies&lt;/li&gt;
&lt;li&gt;Allow n8n to use the SDK module/library (and dependencies)&lt;/li&gt;
&lt;li&gt;Provide configuration/environment-variable-values required by the SDK.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installing (Polly) SDK and AWS CredentialsProviders npm modules/libraries
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;docker build&lt;/code&gt; with a Dockerfile like the following to create a customized n8n (Docker) container image with the AWS SDK npm libraries added.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Dockerfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ARG tag=latest
  FROM n8nio/n8n:$tag
  USER root
  RUN npm install -g @aws-sdk/credential-providers
  RUN npm install -g @aws-sdk/client-polly
  USER node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Allow n8n to use the AWS SDK modules/libraries
&lt;/h3&gt;

&lt;p&gt;Use the image created in the first step instead of the base, published n8n image, specify (or add) the npm library/module names in the &lt;code&gt;NODE_FUNCTION_ALLOW_EXTERNAL&lt;/code&gt; environment variable's csv list&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  n8n:
    image: n8n-with-aws-sdk-addons
  ...
    environment:
      - N8N_HOST=n8n
      - N8N_PORT=5678
      ...
      - NODE_FUNCTION_ALLOW_EXTERNAL=@aws-sdk/credential-providers,@aws-sdk/client-polly
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Set AWS SDK credentials environment variables
&lt;/h3&gt;

&lt;p&gt;In this case, the &lt;code&gt;Code&lt;/code&gt; node will use the &lt;code&gt;fromEnv()&lt;/code&gt; credentials provider, so it will need environment variables with the access key and secret.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  n8n:
  ...
    environment:
      - N8N_HOST=n8n
      - N8N_PORT=5678
      ...
      - AWS_ACCESS_KEY_ID={your-aws-access-key}
      - AWS_SECRET_ACCESS_KEY={your-aws-secret}
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a &lt;code&gt;Code&lt;/code&gt; node to use the SDK Library/Module
&lt;/h2&gt;

&lt;p&gt;The following example code takes a number of input items and, for each one, calls the Polly AWS service to convert the input text to a generated audio file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The input item must include the text-to-speech input text in an attribute named &lt;code&gt;ttsTextInput&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "otherAttribute": "any other json content",
  "ttsTextInput": "What would a wood chuck chuck if a wood chuck could chuck wood."
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Code node Javascript&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { PollyClient, SynthesizeSpeechCommand } = require("@aws-sdk/client-polly");
const { fromEnv } = require("@aws-sdk/credential-providers");

/*
 * This gets the credentials from environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
 */
const config = {
  region: "us-east-1",
  credentials: fromEnv()
}

const client = new PollyClient(config);

async function sendPollyRequest(ttsTextInput) {
  const input = {
    OutputFormat: "mp3",
    Text: ttsTextInput,
    VoiceId: "Joanna"
  };
  const command = new SynthesizeSpeechCommand(input);
  const response = await client.send(command);
  const pollyResponseAudioBytes = await response.AudioStream.transformToByteArray();
  // console.log(`Received polly response with ${pollyResponseBytes.length} bytes`);
  return pollyResponseAudioBytes;
}

for (const item of $input.all()) {
  const pollyResponseAudioBytes = await sendPollyRequest(item.json.ttsTextInput);
  const pollyResponseAudioBytesBuffer = Buffer.from(pollyResponseAudioBytes);
  const n8nBinaryDataPollyAudio = {
       data: await this.helpers.prepareBinaryData(pollyResponseAudioBytesBuffer,
            'polly-audio-response.mp3', 'audio/mp3')
  }

  // console.log(`Binary data item: ${JSON.stringify(n8nBinaryDataPollyAudio)}`);
  item.binary = n8nBinaryDataPollyAudio;
}


return $input.all();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links and References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/API_Reference.html" rel="noopener noreferrer"&gt;AWS SDK Polly documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromenv" rel="noopener noreferrer"&gt;AWS SDK credentials providers documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://us-east-1.console.aws.amazon.com/billing/home#/bills" rel="noopener noreferrer"&gt;AWS Billing Console Page&lt;/a&gt; - To Monitor Polly API Charges &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#home:dashboards/Polly" rel="noopener noreferrer"&gt;AWS Cloudwatch Console- Polly Dashboard&lt;/a&gt; - To Monitor Polly API Usage&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://us-east-1.console.aws.amazon.com/iam/home?region=us-west-2#/users" rel="noopener noreferrer"&gt;AWS IAM User Management Dashboard&lt;/a&gt; (Note: Users may be defined in a different region depending on your account.)
** &lt;a href="https://us-east-1.console.aws.amazon.com/iam/home?region=us-west-2#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonPollyFullAccess?section=permissions" rel="noopener noreferrer"&gt;AWSPollyFullAccess Policy&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/code/code-node/" rel="noopener noreferrer"&gt;n8n Code Node Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.n8n.io/code/cookbook/code-node/get-binary-data-buffer/" rel="noopener noreferrer"&gt;n8n Binary Data Buffer Info&lt;/a&gt; (Note: This isn't used in the code above, but may provide some help if things change in n8n regarding binary data handling).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awss3/" rel="noopener noreferrer"&gt;n8n AWS S3 Node Documentation&lt;/a&gt; - Example of an AWS "built in" node.  Others include SES, SQS, Lambda, DynamoDB, etc. (but not Polly as of this writing).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This article does &lt;strong&gt;not&lt;/strong&gt; cover connectivity issues.  If n8n is unable to reach AWS, the answers will not be here.&lt;/li&gt;
&lt;li&gt;This article does &lt;strong&gt;not&lt;/strong&gt; offer any advise on how to manage the cost of AWS services.  Consuming API services without monitoring usage can lead to unexpected charges.&lt;/li&gt;
&lt;li&gt;There are alternatives in n8n that might be easier to implement, like installing the AWS SDK CLI (command line interface) and using an &lt;code&gt;Execute Command&lt;/code&gt; node in the workflow, or running a "sidecar" service that wraps AWS SDK functions with a RESTful/http-based API service and using an &lt;code&gt;HTTP Request&lt;/code&gt; node to call that service instead of directly accessing AWS.&lt;/li&gt;
&lt;li&gt;Docker Compose could be used to run the Docker build and create a container image from the &lt;code&gt;Dockerfile&lt;/code&gt; automatically, instead of using &lt;code&gt;docker build&lt;/code&gt; separately, but that is another topic that is covered in the &lt;a href="https://docs.docker.com/reference/compose-file/build/" rel="noopener noreferrer"&gt;Docker documentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>n8n</category>
      <category>aws</category>
      <category>javascript</category>
      <category>rest</category>
    </item>
    <item>
      <title>Watching HTTP Traffic from n8n with mitmproxy</title>
      <dc:creator>R</dc:creator>
      <pubDate>Tue, 22 Apr 2025 16:05:30 +0000</pubDate>
      <link>https://dev.to/hubschrauber/watching-http-traffic-from-n8n-with-mitmproxy-672</link>
      <guid>https://dev.to/hubschrauber/watching-http-traffic-from-n8n-with-mitmproxy-672</guid>
      <description>&lt;p&gt;Console logging and error messages in n8n sometimes include very little useful information for troubleshooting http requests that a workflow sends to one of the integrated services.  Fortunately, most (if not all) of the http requests n8n sends, use Axios, and Axios can be configured with environment variables to use a proxy for http and/or https. Then, mitmproxy can be run in a docker container to forward those requests and capture/display the details in an easy-to-use web UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;n8n running in Docker, preferably started using docker-compose&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;Set up mitmproxy to run in Docker, and configure n8n to use mitmproxy for https/http requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps (overview)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set up mitmproxy to run in docker (using docker compose)&lt;/li&gt;
&lt;li&gt;Adjust the runtime configuration (docker-compose.yml) of n8n to include environment variables that instruct Axios to use mitmproxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  mitmproxy docker-compose.yml
&lt;/h2&gt;

&lt;p&gt;This docker-compose.yml file was adapted from &lt;a href="https://github.com/agmt/mitmproxy_docker_compose/" rel="noopener noreferrer"&gt;this example&lt;/a&gt;, but...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The DNS parts were removed&lt;/li&gt;
&lt;li&gt;The networks were adjusted to actual &lt;a href="https://www.rfc-editor.org/rfc/rfc1918" rel="noopener noreferrer"&gt;RFC 1918&lt;/a&gt; private networks.

&lt;ul&gt;
&lt;li&gt;172.201.&lt;em&gt;.&lt;/em&gt; and 172.202.&lt;em&gt;.&lt;/em&gt; overlap public, "real" address ranges and are blocked by default in mitmproxy.&lt;/li&gt;
&lt;li&gt;Reported &lt;a href="https://github.com/agmt/mitmproxy_docker_compose/issues/1" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Some other adjustments (e.g. &lt;code&gt;tty: true&lt;/code&gt;) were made so that the current Python-based mitmproxy code will start correctly in the docker container.&lt;/li&gt;

&lt;li&gt;A default password is specified for the mitmproxy web UI so it won't be necessary to watch the console for a generated password.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;docker-compose.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  mitmproxy:
    image: mitmproxy/mitmproxy
    hostname: mitmproxy
    restart: always
    tty: true
    volumes:
      - ./mitmproxy_data:/home/mitmproxy/.mitmproxy
      # - './cert:/root/.mitmproxy'
    entrypoint: mitmweb
    command: '--web-host 0.0.0.0 -m regular@80 -m regular@443 --set web_password=changeme123'
    networks:
      mitm_internal:
        ipv4_address: 192.168.115.80
      mitm_external:
        ipv4_address: 192.168.116.80
    ports:
      - '38080:80/tcp'
      - '38443:443/tcp'
      - '38081:8081/tcp'

networks:
  mitm_internal:
    name: mitm_internal
    internal: true
    ipam:
      driver: default
      config:
        - subnet: 192.168.115.0/24
          gateway: 192.168.115.1
  mitm_external:
    name: mitm_external
    ipam:
      driver: default
      config:
        - subnet: 192.168.116.0/24
          gateway: 192.168.116.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adjustments to the n8n docker-compose.yml
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add the docker network to the n8n service/container
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  n8n:
    ...
    networks:
      - mitm_external
...
networks:
  mitm_external:
    name: mitm_external
    external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Add environment variables to tell Axios to use mitmproxy

&lt;ul&gt;
&lt;li&gt;Note: These are not n8n environment variables, but are &lt;a href="https://github.com/axios/axios/blob/v1.x/README.md?plain=1#L559" rel="noopener noreferrer"&gt;used directly by the [Axios] npm library/module&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Note: https requests could also be routed through the https listener on mitmproxy, but that potentially introduces certificate related issues.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  n8n:
    ...
    environment:
      ...
      - "HTTPS_PROXY=http://mitmproxy:80"
      - "HTTP_PROXY=http://mitmproxy:80"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using mitmproxy with n8n
&lt;/h2&gt;

&lt;p&gt;After starting the mitmproxy container and restarting the n8n container to pick up changes to networks and environment variables...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the n8n editor to a workflow which sends out http/s requests.&lt;/li&gt;
&lt;li&gt;Open the mitmproxy web UI in another browser or browser-tab.

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://localhost:38081" rel="noopener noreferrer"&gt;http://localhost:38081&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Log in with the &lt;code&gt;web_password&lt;/code&gt; specified in the mitmproxy docker-compose.yml ("changeme123" if you didn't modify it).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Execute the workflow (or http based workflow step), and the mitmproxy web UI should begin showing the requests as they are sent.&lt;/li&gt;

&lt;li&gt;&lt;strong&gt;Click on one of the request items to see details about the request, response, connection, etc.&lt;/strong&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Alternative to "Global" Proxy Config
&lt;/h4&gt;

&lt;p&gt;Instead of setting the &lt;code&gt;HTTPS_PROXY&lt;/code&gt; and/or &lt;code&gt;HTTP_PROXY&lt;/code&gt; environment variables, which sends &lt;strong&gt;all&lt;/strong&gt; Axios traffic through the proxy, some n8n nodes, like the &lt;code&gt;HTTP Request&lt;/code&gt; node, have a &lt;code&gt;Proxy&lt;/code&gt; option that can be explicitly routed to mitmproxy.  For nodes that support this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the &lt;code&gt;Proxy&lt;/code&gt; option in the node configuration&lt;/li&gt;
&lt;li&gt;Set the target proxy URL to &lt;code&gt;http://mitmproxy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mitmproxy.org" rel="noopener noreferrer"&gt;https://mitmproxy.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/mitmproxy/mitmproxy/" rel="noopener noreferrer"&gt;https://hub.docker.com/r/mitmproxy/mitmproxy/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Configuring OpenWRT DDNS for freedns.afraid.org</title>
      <dc:creator>R</dc:creator>
      <pubDate>Sat, 26 Oct 2024 17:57:19 +0000</pubDate>
      <link>https://dev.to/hubschrauber/configuring-openwrt-ddns-for-freednsafraidorg-4caa</link>
      <guid>https://dev.to/hubschrauber/configuring-openwrt-ddns-for-freednsafraidorg-4caa</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;freedns.afraid.org is a DNS provider that offers free registration of subdomains on a long list of "public" (shared) top-level domains. &lt;br&gt;
 This is a more friendly alternative for use with Dynamic DNS clients, than many similar services, because it does &lt;strong&gt;not&lt;/strong&gt; require a periodic "manual" intervention to renew a registered subdomain just to keep it  active.  Other such services seem to use the "manual renewal" as a tactic to "inconvenience" people into paying for their service.&lt;/p&gt;

&lt;p&gt;Setting up the DDNS client in OpenWRT to monitor for changes in a dynamic IP address, and update freedns.afraid.org, is a little confusing, because the DDNS scripts do &lt;strong&gt;NOT&lt;/strong&gt; use their "Direct URL" approach for IP updates.&lt;/p&gt;
&lt;h2&gt;
  
  
  Official Docs
&lt;/h2&gt;

&lt;p&gt;The freedns.afraid.org &lt;a href="https://freedns.afraid.org/scripts/freedns.clients.php" rel="noopener noreferrer"&gt;docs&lt;/a&gt; describe settings that &lt;strong&gt;might&lt;/strong&gt; work with some OpenWRT/DDNS configurations, but the DDNS client parameters didn't exactly work in my case.  The other option, which uses &lt;code&gt;cron&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt;, might work, but isn't the usual/proper way to have OpenWRT handle DNS updates, and anything non-standard ends up burning extra time to document it well, or extra time later trying to remember how it is working.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Direct URL" vs. DDNS Client
&lt;/h2&gt;
&lt;h3&gt;
  
  
  "Direct URL"
&lt;/h3&gt;

&lt;p&gt;The biggest source of confusion while setting this up came from what freedns.afraid.org calls the "Direct URL."  This URL has the path &lt;code&gt;/dynamic/update.php?{authtoken}&lt;/code&gt;.  It is intended to be called/fetched from anywhere within a NAT network, and the server extracts the "apparent" originating IP (outside all NAT layers) from the request, as the IP to associate with the subdomain name.  The token embeds authentication information, and the reference to the subdomain, so the request requires no other parameters.  This URL, and its "token" are &lt;strong&gt;NOT&lt;/strong&gt; used by the OpenWRT DDNS client.&lt;/p&gt;
&lt;h3&gt;
  
  
  DDNS Client
&lt;/h3&gt;

&lt;p&gt;freedns.afraid.org &lt;strong&gt;ALSO&lt;/strong&gt; supports typical ddns client URLs with the path &lt;code&gt;/nic/update&lt;/code&gt;, which accepts various URL parameters like &lt;code&gt;hostname&lt;/code&gt;, and &lt;code&gt;myip&lt;/code&gt;.  &lt;strong&gt;THIS&lt;/strong&gt; is what the OpenWRT DDNS client update script generates and uses.  Authentication for this update option at freedns.afraid.org is HTTP basic, and requires the username:password for the freedns.afraid.org account, &lt;strong&gt;NOT&lt;/strong&gt; the token, and &lt;strong&gt;NOT&lt;/strong&gt; the FQDN of the subdomain (as the line &lt;code&gt;option username 'yourhostname'&lt;/code&gt; in the docs incorrectly suggests).&lt;/p&gt;
&lt;h4&gt;
  
  
  DDNS IP resolution within NAT
&lt;/h4&gt;

&lt;p&gt;One important aspect of the OpenWRT setup is whether the OpenWRT router is behind one or more NAT (network address translation) layers.  If so, the default &lt;code&gt;wan&lt;/code&gt; option for &lt;code&gt;IP address source&lt;/code&gt; WON'T WORK, since it will discover an IP address that is still "hidden" within the private network.  The DDNS client needs to reach outside the NAT/private network to get the "public facing" IP.  See instructions below re: using the &lt;code&gt;URL&lt;/code&gt; option instead.&lt;/p&gt;
&lt;h2&gt;
  
  
  How To
&lt;/h2&gt;

&lt;p&gt;Now, with some of the confusing bits cleared up (let's hope), here are the steps to get things going.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up an account at freedns.afraid.org&lt;/li&gt;
&lt;li&gt;Decide on a suitable subdomain name, and add it to the account under one of the public domains controlled by freedns.afraid.org.

&lt;ul&gt;
&lt;li&gt;i.e. Click "Add" on &lt;a href="https://freedns.afraid.org/subdomain/" rel="noopener noreferrer"&gt;https://freedns.afraid.org/subdomain/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Note: There are other options (transfer domain, etc.) too, but establishing a "registered" FQDN is the goal for this step.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If not already installed, add the DDNS client/service to OpenWRT.

&lt;ul&gt;
&lt;li&gt;See: &lt;a href="https://openwrt.org/docs/guide-user/services/ddns/client" rel="noopener noreferrer"&gt;https://openwrt.org/docs/guide-user/services/ddns/client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Optional: Also install the LuCI components to manage the DDNS service.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add a DDNS "service" for freedns.afraid.org (using an example FQDN of &lt;code&gt;mychosenname.example.com&lt;/code&gt;) (names/values in parens are for direct &lt;code&gt;option&lt;/code&gt; entries in &lt;code&gt;/etc/config/ddns&lt;/code&gt; without LuCI UI)

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lookup Hostname&lt;/strong&gt; (&lt;code&gt;lookup_host&lt;/code&gt;): mychosenname.example.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDNS Service Provider&lt;/strong&gt; (&lt;code&gt;service_name&lt;/code&gt;): Choose "afraid.org-basicauth"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain&lt;/strong&gt; (&lt;code&gt;domain&lt;/code&gt;): mychosenname.example.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username&lt;/strong&gt; (&lt;code&gt;username&lt;/code&gt;): freedns.afraid.org account username&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt; (&lt;code&gt;password&lt;/code&gt;): freedns.afraid.org account password&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Switch the DDNS service's &lt;code&gt;IP address source&lt;/code&gt; from &lt;code&gt;Network&lt;/code&gt; to &lt;code&gt;URL&lt;/code&gt; IP lookup (Advanced tab in LuCI).  This uses the checkip.dyndns.com utility to reach all the way out from all internal network layers and echo back the "public" IP, instead of resolving the first-layer private address (i.e. when the "wan" side of an internal router is still within a NAT/private network).

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP address source&lt;/strong&gt; (&lt;code&gt;ip_source&lt;/code&gt;): Choose "URL" (&lt;code&gt;web&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL to detect&lt;/strong&gt; (&lt;code&gt;ip_url&lt;/code&gt;): &lt;code&gt;http://checkip.dyndns.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Private IP Error
&lt;/h3&gt;

&lt;p&gt;Either the DDNS scripts, or maybe the freedns.afraid.org update service rejected a private/"non-routable" (i.e. &lt;a href="https://www.rfc-editor.org/rfc/rfc1918" rel="noopener noreferrer"&gt;RFC-1918&lt;/a&gt;) IP address with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; WARN : Updating IP at DDNS provider failed...
...
ERROR : No or private or invalid IP '192.168.0.2' given! Please check your configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  LuCI vs. afraid.org-v2-token
&lt;/h3&gt;

&lt;p&gt;The DDNS client in OpenWRT has another &lt;code&gt;DDNS Service provider&lt;/code&gt; option named &lt;code&gt;afraid.org-v2-token&lt;/code&gt;.  This sounds like it would support the "Direct URL" approach, but...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Best guess for where to fill in the token is:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optional Parameter&lt;/strong&gt; (&lt;code&gt;param_opt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Or possibly &lt;strong&gt;Optional Encoded Parameter&lt;/strong&gt; (&lt;code&gt;param_opt_encoded&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;These were generated by LuCI, but not documented &lt;a href="https://openwrt.org/docs/guide-user/base-system/ddns" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LuCI still requires 'username' and 'password', which are not used (so, presumably, they could be filled in with anything like &lt;code&gt;notused&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The server response on an update indicates the (local/LAN) IP is still being included in the request, so the &lt;code&gt;IP address source&lt;/code&gt; option must still be changed from &lt;code&gt;Network&lt;/code&gt; to &lt;code&gt;URL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Whatever request URL this configuration is constructing and sending to freedns.afraid.org, it does NOT seem to match the "Direct URL" format, and does not work.&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Self Hosting n8n with Docker and Traefik/LetsEncrypt (for https)</title>
      <dc:creator>R</dc:creator>
      <pubDate>Sun, 13 Oct 2024 19:32:07 +0000</pubDate>
      <link>https://dev.to/hubschrauber/self-hosting-n8n-with-docker-and-traefikletsencrypt-for-https-4ae8</link>
      <guid>https://dev.to/hubschrauber/self-hosting-n8n-with-docker-and-traefikletsencrypt-for-https-4ae8</guid>
      <description>&lt;p&gt;Setting up n8n in Docker, without https, is relatively simple.  That's covered &lt;a href="https://docs.n8n.io/hosting/installation/docker/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.  However, some things (e.g. oauth2 connection to Google Drive) are difficult or impossible to do in n8n unless it is accessed using https (SSL).  There are many ways to do that, including configuring n8n itself with a dedicated server certificate, but running Traefik as a reverse proxy, in front of n8n, provides automated integration with LetsEncrypt to obtain and renew a "trusted by default" server certificate, and just let n8n run without https.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;n8n running in Docker, preferably started using docker-compose&lt;/li&gt;
&lt;li&gt;Access to start a Traefik container in Docker&lt;/li&gt;
&lt;li&gt;A registered (top level) domain name with access to DNS configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;Set up Traefik, in Docker, such that it handles requests for a subdomain, like myn8n.example.com and forwards (reverse-proxies) that traffic to n8n, also running in Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  High Level Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Register/activate API access to domain registrar

&lt;ul&gt;
&lt;li&gt;How to do this varies depending upon which registrar is used.&lt;/li&gt;
&lt;li&gt;The list of supported registrars, and links to specific instructions can be found &lt;a href="https://go-acme.github.io/lego/dns/#configuration-and-credentials" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Get Traefik running in a container

&lt;ul&gt;
&lt;li&gt;This is documented &lt;a href="https://doc.traefik.io/traefik/user-guides/docker-compose/acme-dns/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Part of this example setup configures Traefik to automatically fetch 
a server certificate, for https, from LetsEncrypt&lt;/li&gt;
&lt;li&gt;Note: The example docker compose shows env-variables for a DNS/registrar provider named &lt;strong&gt;ovh&lt;/strong&gt;.  This part will be different with each different registrar provider. The Traefik plugin specifics for each registrar (like which ENV vars are required) can be found along with the API-access instructions from the previous step.

&lt;ul&gt;
&lt;li&gt;Unless you are actually using &lt;strong&gt;ovh&lt;/strong&gt; as your registrar, change this: &lt;code&gt;- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=ovh"&lt;/code&gt; from &lt;code&gt;ovh&lt;/code&gt; to the one matching your actual registrar, e.g. &lt;code&gt;ionos&lt;/code&gt;, &lt;code&gt;godaddy&lt;/code&gt;, &lt;code&gt;namecheap&lt;/code&gt;, etc. etc.&lt;/li&gt;
&lt;li&gt;The environment variables will also be different.  For instance, if you are using &lt;a href="https://go-acme.github.io/lego/dns/godaddy/" rel="noopener noreferrer"&gt;GoDaddy&lt;/a&gt; you would include environment variables &lt;code&gt;GODADDY_API_KEY&lt;/code&gt; and &lt;code&gt;GODADDY_API_SECRET&lt;/code&gt; instead of all the ones starting with &lt;code&gt;OVH_&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configure DNS so that a domain/subdomain name maps to the Traefik server (from the web browser's perspective)

&lt;ul&gt;
&lt;li&gt;If this cannot be done in the public DNS, it will still work with host entries in the &lt;code&gt;/etc/hosts&lt;/code&gt; file on one machine, a hostname/DNS feature on a router, or an "internal-network" DNS like pihole.&lt;/li&gt;
&lt;li&gt;Note that it is probably &lt;strong&gt;not&lt;/strong&gt; a good idea to configure the public DNS at your registrar to resolve an internal-only (i.e. private/LAN) Traefik server IP address.  Also, securing Traefik for public access is outside the scope of this article.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add configuration (labels) to the n8n container to tell Traefik to handle the https access.

&lt;ul&gt;
&lt;li&gt;The example whoami service shown in the Traefik docs is ok for checking to be sure the reverse proxy is working, but it doesn't make the importance of the "labels" section exactly clear.&lt;/li&gt;
&lt;li&gt;In the n8n container, there would be corresponding labels like these:
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  labels:
      - "traefik.enable=true"
      - "traefik.http.routers.n8n.rule=Host(`n8n.example.com`)"
      - "traefik.http.routers.n8n.entrypoints=websecure"
      - "traefik.http.routers.n8n.tls.certresolver=myresolver"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Background Info, How Stuff Works, etc.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supplemental Note on Docker Networks
&lt;/h3&gt;

&lt;p&gt;When I set up n8n and Postgres in Docker, I put both of the containers in their own docker network for isolation from other stuff running in docker, so there was a network defined in n8n's docker-compose.yaml like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  networks:
    n8nstack:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And each container "joined" the network like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    services:
      n8n:
        # ...
        networks:
          - "n8nstack"
      postgres:
        # ...
        networks:
          - "n8nstack"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik was running on the default docker network, not the &lt;code&gt;n8nstack&lt;/code&gt; network, so the reverse-proxy wasn't working. To fix this, Traefik was similarly configured with its own (external) docker network, created by docker command like...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker network create traefikproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, in Traefik's docker-compose.yaml...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  networks:
    traefikproxy:
      external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with each container referencing the network like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  services:
    traefik:
      # ...
      networks:
        - "traefikproxy"
    whoami:
      # ...
      networks:
        - "traefikproxy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Somewhat obviously, the n8n docker-compose would need to be updated so the n8n service would &lt;strong&gt;ALSO&lt;/strong&gt; join the traefikproxy network with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  networks:
    n8nstack:
    traefikproxy:
      external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the n8n service (not others) &lt;strong&gt;ALSO&lt;/strong&gt; joins the traefik network with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    services:
      n8n:
        # ...
        networks:
          - "n8nstack"
          - "traefikproxy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that was not as obvious was that the Traefik-related labels in the n8n container/service would now need &lt;strong&gt;an additional label&lt;/strong&gt; to tell Traefik &lt;strong&gt;which docker network to use,&lt;/strong&gt; like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;labels:
  - "traefik.enable=true"
  - "traefik.docker.network=traefikproxy"
  # ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Note: I think this could also have been reversed such that the &lt;code&gt;traefik&lt;/code&gt; service joined the &lt;code&gt;n8nstack&lt;/code&gt; network, and any other stack's network too, but it seemed better to me (more encapsulated) to go the other direction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Use Case that Made this Necessary
&lt;/h3&gt;

&lt;p&gt;The reason I got motivated to set up n8n behind a reverse proxy, with https/SSL enabled, is that there seems to be no other way to get through the oauth2 sequence for Google API access, which is required for the Google Drive node.  Google wants a redirect URL on the oauth2 credentials that maps to a real, public domain name, and uses https.  There may be ways to configure n8n such that it generates an https redirect url (possibly using &lt;code&gt;WEBHOOK_URL&lt;/code&gt;) but without actually loading the n8n UI using the https endpoint, it seemed like some part of the whole oauth2 sequence would always skip the track and fail to work properly.  Setting up Traefik to deal with the SSL certificate hassles was ultimately the best option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Overview of Traefik
&lt;/h3&gt;

&lt;p&gt;Traefik is (primarily) a reverse proxy service that accepts and routes web traffic (from browsers and other http(s) clients) to one or more "back end services."  In this context, n8n &lt;strong&gt;is&lt;/strong&gt; a "back end service."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Note: If the notion of a reverse proxy isn't familiar, &lt;a href="https://duckduckgo.com/?q=reverse+proxy" rel="noopener noreferrer"&gt;reading up on that&lt;/a&gt; would probably help.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Traefik + LetsEncrypt - Certificate Automation
&lt;/h3&gt;

&lt;p&gt;This is a simplified overview of how this works...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Traefik is configured with an API key for the registrar that controls DNS for the top level domain and its subdomains.&lt;/li&gt;
&lt;li&gt;Traefik is configured to accept https/443 requests on the domain, one or more of its subdomains, or both.&lt;/li&gt;
&lt;li&gt;When required (i.e. when the SSL server certificate for a given host name is missing or expired), Traefik automatically sends a request to LetsEncrypt to fetch a server certificate.&lt;/li&gt;
&lt;li&gt;LetsEncrypt responds to Traefik with a "DNS Challenge", which is basically a request to create an arbitrary DNS record, temporarily, in order to prove "ownership" of the domain.&lt;/li&gt;
&lt;li&gt;Traefik uses the registrar API key (and a corresponding "provider" plugin) to make the DNS change, and then sends a message back to LetsEncrypt saying, "Check DNS again now."&lt;/li&gt;
&lt;li&gt;LetsEncrypt checks DNS and, assuming the "challenge" entry/record is found, returns the server certificate for the requested domain/subdomain.&lt;/li&gt;
&lt;li&gt;Traefik stores the certificate (for use on the proxy server) and cleans up the temporary DNS records (again using the registrar API functions).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Related n8n Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  N8N_EDITOR_BASE_URL
&lt;/h3&gt;

&lt;p&gt;When n8n is reached through a reverse-proxy like Traefik (or a kubernetes load balancer, or a rule on a dedicated nginx web server, or any number of other things like that), there is an environment-variable (or n8n config) setting named &lt;strong&gt;N8N_EDITOR_BASE_URL&lt;/strong&gt; to tell n8n, internally, what to use (in the UI and elsewhere) as the "external" (browser context) base URL.  It may be necessary to set this in the n8n config when using Traefik.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Currently, (around n8n v1.59.0), the credentials dialog for a Google API related credential, and, therefore, the redirect URL sent to Google for oauth2, appears to formulate the redirect URL based on &lt;strong&gt;either&lt;/strong&gt;:

&lt;ol&gt;
&lt;li&gt;how the UI itself is loaded into the browser, &lt;strong&gt;or&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N8N_EDITOR_BASE_URL&lt;/strong&gt;, if it is specified in the environment or config.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WEBHOOK_URL
&lt;/h3&gt;

&lt;p&gt;Another override that might be necessary when n8n is cloistered behind Traefik, is the WEBHOOK_URL config/environment-variable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is used for... big surprise... webhook related things like &lt;code&gt;$execution.resumeUrl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt; appear that WEBHOOK_URL has an influence over the oauth2 redirect URL, although I think that may have been suggested in a few places.  Perhaps WEBHOOK_URL also served the purpose of N8N_EDITOR_BASE_URL in a previous version of n8n.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;https://n8n.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.n8n.io/t/how-could-i-use-n8n-editor-base-url/14163/3" rel="noopener noreferrer"&gt;https://community.n8n.io/t/how-could-i-use-n8n-editor-base-url/14163/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/hosting/configuration/environment-variables/deployment/" rel="noopener noreferrer"&gt;https://docs.n8n.io/hosting/configuration/environment-variables/deployment/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/hosting/configuration/configuration-examples/webhook-url/" rel="noopener noreferrer"&gt;https://docs.n8n.io/hosting/configuration/configuration-examples/webhook-url/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Developing Custom Nodes for n8n with Docker</title>
      <dc:creator>R</dc:creator>
      <pubDate>Sat, 14 Sep 2024 01:28:59 +0000</pubDate>
      <link>https://dev.to/hubschrauber/developing-custom-nodes-for-n8n-with-docker-3poj</link>
      <guid>https://dev.to/hubschrauber/developing-custom-nodes-for-n8n-with-docker-3poj</guid>
      <description>&lt;p&gt;The n8n automation / orchestration / workflow tool already covers a massive number of services without adding anything to it, but there may come a time when writing your own "custom-node" makes sense.  The custom-node &lt;a href="https://docs.n8n.io/integrations/creating-nodes/build/programmatic-style-node/" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; hits the high points, but doesn't include (as of this writing) any specifics about how to develop a custom node and test it in a self-hosted instance of n8n running in a Docker container.  That's the subject of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;n8n running in Docker, preferably started using docker-compose&lt;/li&gt;
&lt;li&gt;An n8n custom-node (typescript/npm) project that is already working&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quick Clarification
&lt;/h3&gt;

&lt;p&gt;This is about developing custom-&lt;strong&gt;nodes&lt;/strong&gt; for n8n, which are like plugins for the product, not to be confused with Node.js, even though n8n runs in Node.js.  Assuming if you're still reading you probably know the difference, but if you thought this was about coding in Node.js, stop now.  This isn't the droid you're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;Map one or more custom-node projects &lt;code&gt;dist&lt;/code&gt; subdirectory &lt;strong&gt;directly&lt;/strong&gt; into an n8n Docker container such that n8n will find the "compiled" files and load the custom-node(s) at startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev Process Steps
&lt;/h2&gt;

&lt;p&gt;(i.e. "How This Works Once You Have It Set Up")&lt;/p&gt;

&lt;p&gt;For each "development cycle"...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make changes to the custom-node source.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rm -rf dist&lt;/code&gt; (when necessary, in the dev project directory, to remove old/renamed file versions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; - i.e. re-compile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker-compose restart n8n&lt;/code&gt; - i.e. trigger code reload in n8n&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add one or more docker volumes, mapped to the dev/source directories:

&lt;ul&gt;
&lt;li&gt;Edit the n8n &lt;strong&gt;docker-compose.yaml&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The important part here is that the volumes map into the container at a path under &lt;code&gt;/Users/node/.n8n/custom&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  n8n:
    ...
    volumes:
      - /Users/myuserid/devstuff/custom_node_dev_project_1/dist:/Users/node/.n8n/custom/node_modules/my-awesome-custom-node
      - /Users/myuserid/devstuff/custom_node_dev_project_abc/dist:/Users/node/.n8n/custom/node_modules/my-other-custom-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;That's it.  Go write code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Explanations (...if you care...)
&lt;/h2&gt;

&lt;p&gt;(Read the rest of this [later] if you want to know reasons.)&lt;/p&gt;

&lt;h3&gt;
  
  
  npm link
&lt;/h3&gt;

&lt;p&gt;The tutorial says to use &lt;code&gt;npm link&lt;/code&gt; to map a custom-node project into n8n, but that assumes n8n is running from source, not in Docker.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notes:

&lt;ul&gt;
&lt;li&gt;Getting set up to run n8n from source is more time-consuming and has its own challenges, vs. running n8n in docker.&lt;/li&gt;
&lt;li&gt;Docker volumes don't permit access to symlinked files, so &lt;code&gt;npm link&lt;/code&gt; won't work for this anyway.&lt;/li&gt;
&lt;li&gt;Trying to run &lt;code&gt;npm link&lt;/code&gt; from the n8n custom node starter project already causes an error because the project (in &lt;strong&gt;package.json&lt;/strong&gt;) is set up to require &lt;code&gt;pnpm&lt;/code&gt; in the preinstall script, which runs implicitly from &lt;code&gt;npm link&lt;/code&gt; and causes a conflict between &lt;code&gt;pnpm&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt;. :(

&lt;ul&gt;
&lt;li&gt;The error message is &lt;code&gt;npm ERR! command sh -c npx only-allow pnpm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;See: &lt;a href="https://github.com/n8n-io/n8n-nodes-starter/blob/master/package.json#L25" rel="noopener noreferrer"&gt;package.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Effective Directory Structure
&lt;/h3&gt;

&lt;p&gt;The directory structure mapped into the docker volumes mimics what &lt;code&gt;npm link my-awesome-custom-node&lt;/code&gt; and &lt;code&gt;npm link my-other-custom-node&lt;/code&gt; &lt;strong&gt;would&lt;/strong&gt; have created, if they were run within the n8n runtime's &lt;strong&gt;.n8n/custom&lt;/strong&gt; directory.  Within the running Docker container, it ends up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── .n8n
    ├── custom
        ├── node_modules
            ├── my-awesome-custom-node
            │   ├── dist
            │   ├── ...other stuff...
            ├── my-other-custom-node
                ├── dist
                ├── ...other stuff...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Normal Docker install for a Custom-Node
&lt;/h3&gt;

&lt;p&gt;Per the instructions for &lt;a href="https://docs.n8n.io/integrations/creating-nodes/deploy/install-private-nodes/#install-your-node-in-a-docker-n8n-instance" rel="noopener noreferrer"&gt;Install(ing) Private Nodes&lt;/a&gt;, you would just &lt;em&gt;"Copy the node and credential folders from within the dist folder into your container's ~/.n8n/custom/ directory."&lt;/em&gt;&lt;br&gt;&lt;br&gt;
There are few issues with that strategy though.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Without some additional directory structure, which isn't specified in the instructions, it would only allow one custom-node to be presented into the n8n Docker runtime.&lt;/li&gt;
&lt;li&gt;If the project is based on the custom node starter project, from the n8n github repo, you'd be copying the &lt;strong&gt;nodes&lt;/strong&gt; and &lt;strong&gt;credentials&lt;/strong&gt; directories/folders, not &lt;strong&gt;node&lt;/strong&gt; and &lt;strong&gt;credential&lt;/strong&gt; directories... but that could be just something that didn't get corrected/updated in the docs.&lt;/li&gt;
&lt;li&gt;More importantly... copying stuff into a container's filesystem is transient.  As soon as the container gets rebuilt/reset, all that stuff is gone.  Mapping a volume to a directory on the host filesystem survives container resets, &lt;strong&gt;and&lt;/strong&gt; doesn't require any overwriting.  After each

&lt;code&gt;npm run build&lt;/code&gt;

of the project, the container (i.e. n8n) just needs to be restarted to load the changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;https://n8n.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things that might lead here...
&lt;/h2&gt;

&lt;h3&gt;
  
  
  npx only-allow pnpm
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search finds stuff like this:  &lt;a href="https://stackoverflow.com/questions/63165630/trouble-with-npm-preinstall-script" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/63165630/trouble-with-npm-preinstall-script&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ERR_PNPM_LINK_BAD_PARAMS  You must provide a parameter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search finds stuff like this: &lt;a href="https://github.com/pnpm/pnpm/issues/4296" rel="noopener noreferrer"&gt;https://github.com/pnpm/pnpm/issues/4296&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  npm install n8n-nodes-my-custom-nodes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Search finds stuff like this: &lt;a href="https://community.n8n.io/t/npm-install-n8n-nodes-my-custom-nodes-throws-error/6528" rel="noopener noreferrer"&gt;https://community.n8n.io/t/npm-install-n8n-nodes-my-custom-nodes-throws-error/6528&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>node</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
