The definitive guide to custom NPM modules for self-hosted instances

I just spent the last 3 hours trying to get custom npm modules working.

Literally 14 community.n8n.io tabs open but it still took so long.

I feel like I’ve been through a car wash in a mini cooper convertible with the roof down.

But fear not young lass, for in this post I’m going to show you how to get your shit running without going crazy.

And in case you’re wondering, I’m running the vanilla Docker container on a vanilla Digital Ocean droplet with the vanilla database and the most recent version of n8n.

Here’s the proof of work.

Now, let’s get into it.

First of all, to my understanding there are only 2 basic parts to this setup:

The first is your .env file which is needed for the configuration of your n8n instance.

Mine is almost 100% default:

# Replace <directory-path> with the path where you created folders earlier
# I haven't touched this
DATA_FOLDER=/home/adomakins/n8n-docker-caddy

# The top level domain to serve from, this should be the same as the subdomain you created above
# Of course I've touched this
DOMAIN_NAME=yourmom.com

NODE_FUNCTION_ALLOW_EXTERNAL=*
# For some reason I had export in front of this earlier, but now I don't. Must not be important.

# The subdomain to serve from
SUBDOMAIN=n8n

# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# above example would result in: <https://n8n.example.com>

# Optional timezone to set which gets used by Cron-Node by default
# If not set New York time will be used
# I changed this, I believe it should make everything UTC
GENERIC_TIMEZONE=Etc/UTC

# The email address to use for the SSL certificate creation
# Redacted (you should probably change this)
[email protected]

The next part is your docker-compose.yml file.

This can be a bit of a confusing bastard if you’re too stubborn like me to study Docker for 10 minutes.

But, it should be fine because you can just copy mine:

version: "3.7" # No idea what this means

services: # Same here, just left it alone
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy_data:/data
      - ${DATA_FOLDER}/caddy_config:/config
      - ${DATA_FOLDER}/caddy_config/Caddyfile:/etc/caddy/Caddyfile

  n8n:
    container_name: n8n # nice to give it a name... I'm not 100% sure if this does anything though
    image: n8nio/n8n:latest # pulling the newest version of n8n everytime I do a rebuild! (default)
    restart: always
    ports:
      - 5678:5678
    environment:
      - NODE_FUNCTION_ALLOW_EXTERNAL=${NODE_FUNCTION_ALLOW_EXTERNAL} # VERY FUCKING IMPORTANT!!!!!
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
    volumes:
      - n8n_data:/home/node/.n8n
      - ${DATA_FOLDER}/local_files:/files

volumes:
  caddy_data:
    external: true
  n8n_data:
    external: true

Hopefully you saw that note above about this line:

      - NODE_FUNCTION_ALLOW_EXTERNAL=${NODE_FUNCTION_ALLOW_EXTERNAL} # VERY FUCKING IMPORTANT!!!!!

Not having this had me closer to suicide than I’m comfortable with admitting.

I think I was struggling for about 3 hours because I was missing this. The other stuff isn’t rocket science. But telling your docker container to bring in the right nodes is probably a little bit important. (To clarify: It’s very fucking important)

But now, it’s time for the real pièce de résistance.

Because up to this point we haven’t actually imported any modules.

So here we go.

In your project directory (for me it’s /home/[username]/n8n-docker-caddy (the same directory as .env and docker-compose.yml)) you want to create a new file called run.sh (actually, call it whatever you want but just something with a .sh at the end)

If you’re a real one then you’re going to be using vi run.sh at this point.

Then I want you to put this script in there:

#!/bin/bash

# Stop current containers
docker-compose down

# Start containers
docker-compose up -d

# Install custom modules
echo "Installing external modules"
docker-compose exec -u root n8n npm install -g ytdl-core
# Add any other modules you need

echo "Installation done"
echo "N8N ready for use"

In this example I’m installing ytdl-core, but just swap that out with whatever you’re installing.

Now again for you real ones, it’s time to hit ESC :wq (that means go back to the terminal for you soy devs).

So once you’re back in the terminal, just hit it with a chmod +x run.sh and you’re just about good to go.

Now, it’s important to make sure that you’re logged in as the root user. I think.

I’m actually not 100% sure, and I can’t be bothered to test it out as a normal user, so maybe give it a whirl if you can’t be bothered to switch to root, idk.

Anyways.

At this point all you should have to do is hit ./run.sh which will run your new executable, and Voilà.

Your old container will stop, your new container will start, and your package(s) will be installed.

Then just refresh your n8n page, take one last nasty breath of this no-packages-having air, and hit “Test step” to enter a world of bliss.

Just make sure you’re doing it like const ytdl = require('ytdl-core'); and not trying to use import. I don’t think that works. At least ChatGPT told me that shouldn’t work. But ChatGPT is also kind of a saboteur sometimes so maybe give it a whirl if you like.

And if you’re still here and it didn’t work, well that sucks. But a permissions error might be solved with a good ol’ sudo or by switching to the root user.

One more note:

Despite the jokes, I highly recommend using Zed or VS Code to SSH into your server so that you can see all of your files easily without typing cd or vi 1,000,000 times.

As a bonus, if you’re using an AI API with Zed.dev or Continue.dev, you can chat with Claude to fix your issues using context from the codebase.

Claude is the one that helped me fix my issue. Not ChatGPT. ChatGPT had me chasing my own tail for about 3 hours.

Also, @yousername has a thread on here that really helped, actually giving me the contents of run.sh in the first place.

1 Like

Hey @adomakins,

Welcome to the community :tada:

I am a big fan of this but can you maybe clean it up a little bit to keep it friendly :slight_smile:

I would also like to add an other option is to use a Dockerfile with

FROM n8nio/n8n:latest
USER root
RUN npm install -g ytdl-core
USER node

You can then update your Compose file replace image with build like below.

  n8n:
    container_name: n8n
    build: .
    restart: always
    ports:
      - 5678:5678
    environment:
      - NODE_FUNCTION_ALLOW_EXTERNAL=${NODE_FUNCTION_ALLOW_EXTERNAL}
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
    volumes:
      - n8n_data:/home/node/.n8n
      - ${DATA_FOLDER}/local_files:/files

So now if you use the docker compose file commands it will automatically build your image with the latest version of n8n and your module.

2 Likes

Okay gotcha, yeah that looks a little bit more straight forward!

Can you suggest where to run

FROM n8nio/n8n:latest
USER root
RUN npm install -g ytdl-core
USER node

I installed using: Docker Compose | n8n Docs

THANK YOU! This post helped me so much when ChatGPT had me chasing my tail for hours, too. Thank you!

I wish topics were not automatically closed after some time of inactivity!

We find a lot of people sharing the same problems we face, but we never get to learn how they solved it because the topic was closed too early.

And we never know how outdated is the info we are checking in this forum because nobody can post a reply pointing to a newer/better solution.