Installing Custom Nodes from a Private NPM Registry in n8n Using pnpm Inside a Custom Docker Image

Installing Custom Nodes from a Private NPM Registry in n8n Using pnpm Inside a Custom Docker Image

Introduction

Hey everyone,

At our company, we use n8n with a paid license and actively contribute to its development through PRs on the GitHub repo. However, we also have proprietary nodes that we publish on a private NPM registry and need to integrate them properly into our self-hosted n8n environment.

Since we’re deploying n8n using Docker Compose, we decided that the best approach would be to create a custom Docker image to version and manage these nodes efficiently.

What We Tried

After researching best practices in the n8n community, we found some relevant discussions and documentation:

Following these recommendations, we created a custom Dockerfile like this:

FROM docker.n8n.io/n8nio/n8n:latest@sha256:2c41c31d5becfd3466e2f15ae34ecc75024b8aac061abb44623c52e7a0fe3afb  

COPY .npmrc ./  

USER root  

RUN pnpm install @mycompany/n8n-nodes-custom  

USER node  

After building and running the container, we verified that a package.json file was generated inside the /home/node directory, confirming that the package installation was successful.

However, despite this, n8n does not recognize the custom node, and it does not appear in the UI.

We also tried adding the following environment variable to docker-compose.yml to ensure external dependencies are allowed:

environment:
  - NODE_FUNCTION_ALLOW_EXTERNAL=*

The Problem

The issue is that n8n doesn’t seem to detect our custom node, even though it is installed inside the container. We suspect this is due to the way n8n loads nodes internally.

We cannot use the .n8n/custom directory (which is the recommended approach for development) because this folder is mounted as a volume in our Docker Compose setup to persist stateful data.

What’s the Correct Way to Install Custom Nodes in Docker?

Given our setup, what is the proper way to install and register private custom nodes inside a Docker container so that n8n can detect them correctly?

Should we use a global flag when using the installation step in the Dockerfile ?

Or should we use a specific path that you recomment ?

Any insights or best practices would be greatly appreciated!


Debug Info

Core

  • n8n Version: 1.81.4
  • Platform: Docker (self-hosted)
  • Node.js Version: 20.18.3
  • Database: PostgreSQL
  • Execution Mode: Scaling
  • Concurrency: -1
  • License: Community
  • Consumer ID: Unknown

Storage

  • Success: All
  • Error: All
  • Progress: False
  • Manual: True
  • Binary Mode: Memory

Pruning

  • Enabled: True
  • Max Age: 336 hours
  • Max Count: 10,000 executions

Hi @Gentleman9914,

Integrating proprietary nodes into your self-hosted n8n environment using Docker Compose is a strategic approach to maintain version control and streamline deployment. Building upon your current setup, here’s a refined method to ensure n8n recognizes and utilizes your private nodes effectively.

1. Create a Custom Docker Image with Your Private Nodes

To embed your proprietary nodes within the n8n Docker container, you’ll need to create a custom Dockerfile that installs your private npm packages. Here’s how you can achieve this:

Dockerfile:

# Use the official n8n image as the base
FROM n8nio/n8n:1.81.4

# Switch to root user to install global packages
USER root

# Copy the npm configuration file to authenticate with your private registry
COPY .npmrc /home/node/.npmrc

# Install your private n8n nodes globally
RUN npm install -g @mycompany/n8n-nodes-custom

# Adjust permissions if necessary
RUN chown -R node:node /home/node/.n8n

# Switch back to the node user
USER node

Key Points:

  • Authentication: The .npmrc file should contain the necessary authentication tokens or credentials to access your private npm registry. Ensure this file is correctly configured and securely managed.

  • Global Installation: Installing the package globally (-g flag) makes it accessible system-wide, which is suitable for n8n to detect and load the nodes.

  • Permissions: Adjusting ownership ensures that the node user, under which n8n operates, has the appropriate permissions to access the installed packages.

2. Modify Your Docker Compose Configuration

Update your docker-compose.yml to build the custom image you’ve defined:

version: '3.8'

services:
  n8n:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5678:5678"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - DB_POSTGRESDB_DATABASE=n8n
      - N8N_PERSONALIZATION_ENABLED=false
      - NODE_FUNCTION_ALLOW_EXTERNAL=*
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - postgres

  postgres:
    image: postgres:16.3
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  n8n_data:
  postgres_data:

Key Points:

  • Build Context: The build section specifies the path to the directory containing your Dockerfile.

  • Environment Variables: The NODE_FUNCTION_ALLOW_EXTERNAL=* environment variable allows n8n to use external modules within function nodes. Adjust other environment variables as per your database and authentication configurations.

  • Volumes: Mounting volumes ensures data persistence across container restarts.

3. Build and Deploy

Navigate to the directory containing your docker-compose.yml and execute:

docker-compose up --build

This command builds the custom Docker image and starts the n8n and PostgreSQL services.

4. Verify the Integration

After deployment, access your n8n instance through the web interface. Your custom nodes should now be available in the node selection menu. If they do not appear:

  • Check Logs: Review the container logs for any errors related to the installation or loading of custom nodes.

  • Validate Installation: Access the running container and verify that the custom nodes are present in the global node_modules directory.

By following this approach, you ensure that your proprietary nodes are seamlessly integrated into your n8n workflows, leveraging Docker’s capabilities for consistent and reproducible deployments.

1 Like

Hey there thanks for your input.

However, please note that this approach does not work anymore now that n8n is using pnpm.

7.514 npm error code 127                                                                                                                     
7.514 npm error path /usr/local/lib/node_modules/@mycompany/n8n-nodes-custom
7.514 npm error command failed                                                                                                               
7.514 npm error command sh -c npx only-allow pnpm                                                                                            
7.514 npm error npm warn exec The following package was not found and will be installed: [email protected]
7.514 npm error sh: only-allow: not found
7.515 npm error A complete log of this run can be found in: /root/.npm/_logs/2025-03-24T12_38_35_922Z-debug-0.log

So I tried my luck with the following :

FROM docker.n8n.io/n8nio/n8n:latest@sha256:2c41c31d5becfd3466e2f15ae34ecc75024b8aac061abb44623c52e7a0fe3afb

COPY .npmrc ./

ENV PNPM_HOME="home/node/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

RUN pnpm install -g @mycompany/n8n-nodes-custom

But still can’t find the package in my n8n installation. So I think installing globally is not what is required, but we should install it somewhere specific.

1 Like

I always worked with yarn/npm and did the job.
I will need to upgrade myself before :wink:

I managed to do something like that :

FROM docker.n8n.io/n8nio/n8n:latest@sha256:2c41c31d5becfd3466e2f15ae34ecc75024b8aac061abb44623c52e7a0fe3afb

COPY .npmrc /home/node/.npmrc

ENV N8N_CUSTOM_EXTENSIONS=/home/node/custom-nodes

RUN mkdir -p /home/node/custom-nodes \
    && cd /home/node/custom-nodes \
    && pnpm install @mycompany/n8n-nodes-custom

Alas, the n8n-nodes-starter that I am using is then creating issues. Indeed, when building it creates these at the top of the NodeName.node.js :

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomNode= void 0;
const change_case_1 = require("change-case");
const n8n_workflow_1 = require("n8n-workflow");
const descriptions_1 = require("./descriptions");
const GenericFunctions_1 = require("./GenericFunctions");

This works fine on a locall install, but using the Dockerfile above it results in this error :

2025-03-24T15:03:32.014Z | error | /home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js:4
2025-03-24T15:03:32.014937593Z const change_case_1 = require("change-case");
2025-03-24T15:03:32.014941110Z                       ^
2025-03-24T15:03:32.014943490Z 
2025-03-24T15:03:32.014945705Z Error [ERR_REQUIRE_ESM]: require() of ES Module /home/node/custom-nodes/node_modules/.pnpm/[email protected]/node_modules/change-case/dist/index.js from /home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js not supported.
2025-03-24T15:03:32.014948261Z Instead change the require of index.js in /home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js to a dynamic import() which is available in all CommonJS modules.
2025-03-24T15:03:32.014950142Z     at Object.<anonymous> (/home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js:4:23) {"file":"start.js","function":"catch"}
2025-03-24T15:03:32.015665029Z 2025-03-24T15:03:32.015Z | error | Exiting due to an error. {"file":"error-reporter.js","function":"defaultReport"}
2025-03-24T15:03:32.016424590Z 2025-03-24T15:03:32.015Z | error | require() of ES Module /home/node/custom-nodes/node_modules/.pnpm/[email protected]/node_modules/change-case/dist/index.js from /home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js not supported.
2025-03-24T15:03:32.016531773Z Instead change the require of index.js in /home/node/custom-nodes/node_modules/.pnpm/[email protected][email protected]/node_modules/n8n-nodes-custom/dist/nodes/CustomNode/CustomNode.node.js to a dynamic import() which is available in all CommonJS modules. {"file":"error-reporter.js","function":"defaultReport"}

1 Like

This is a classic ESM vs CommonJS module compatibility issue, especially common with recent libraries like change-case, which are now pure ES Modules. Since n8n still loads custom nodes in a CommonJS context, direct require('change-case') will fail.

:warning: What’s happening?

In your compiled CustomNode.node.js, this line fails:

const change_case_1 = require("change-case");

Because change-case is now a pure ESM package, it cannot be loaded using require().


:white_check_mark: Solution Paths

:white_check_mark: Option 1: Use change-case alternatives that support CommonJS

There are still some packages that provide CommonJS-compatible versions. For example:

  • Use individual case functions from CommonJS-compatible packages like:
    pnpm add change-case-all
    
    Then in your source:
    import { camelCase } from 'change-case-all';
    
    change-case-all works with both ESM and CommonJS.

Or:

  • Use lodash for string case transformations (_.camelCase, _.kebabCase etc.)
    pnpm add lodash
    
    And in the source:
    import _ from 'lodash';
    

This is the cleanest solution if you’re fine with swapping libraries.


:warning: Option 2: Use dynamic import() (hacky in this context)

Technically, you could do this inside a CommonJS module:

const changeCase = await import('change-case');

But this won’t work in top-level code and will break your TypeScript-to-CommonJS build unless you restructure it heavily (e.g., async factory functions). Not recommended in n8n custom nodes.


:warning: Option 3: Bundle your node with a transpiler like esbuild or vite and shim the ESM import

You can use esbuild to prebundle ESM packages into CommonJS-compatible format during build time, but this adds complexity to your n8n-nodes-custom build pipeline.


:package: Recommended Fix

Since you’re using the n8n-nodes-starter, just:

  1. Replace change-case with change-case-all:

    pnpm remove change-case
    pnpm add change-case-all
    
  2. Update import in your node:

    import { camelCase } from 'change-case-all';
    
  3. Rebuild your node package:

    pnpm build
    
  4. Rebuild your Docker image and it should work.


Let me know if you’d like help adapting the build pipeline to use esbuild or other tools to keep change-case — but for most cases, swapping the lib is the easiest win.

1 Like

Hey here Miquel, thank you very much, it was indeed the issue.

I managed to make it work by downgrading to 4.x version of change-case, but I will investigate the need to change to a CommonJS compatible dependency in order to be able to upgrade safely.

Thanks again for your time !

1 Like

You are welcome!

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.