
You run n8n on your VPS and want your Discord bot there too, so a slash command can start a workflow without a second machine. That works if you plan for what the two share: RAM, disk, the IP address Discord sees and every reboot. Here is the setup on Ubuntu 24.04 or Debian 12, with n8n in Docker and a discord.js bot under PM2 or in a container, talking over 127.0.0.1.
Quick answer: Publish n8n's port on 127.0.0.1:5678 only and give its container a mem_limit, restart: unless-stopped and the local log driver. Run the bot as its own Linux user under PM2 with max_memory_restart, or in its own Compose project on a network shared with n8n; a gateway bot opens no port. It calls a published workflow at http://127.0.0.1:5678/webhook/<path> with Header Auth and a timeout, after deferring the reply, since Discord wants a first response within 3 seconds. Back up n8n's volume and the bot's folder separately, and split them when the measured RAM no longer fits or other people need access to the server.
Step 1: Budget the RAM From What Each Side Documents
Few figures are published, so start with them and end with a measurement:
| Part | What its docs say |
|---|---|
| Ubuntu 24.04 | 1 GB minimum (cloud images), 3 GB or more suggested |
| n8n | No minimum: it "doesn't restrict the amount of data each node can fetch and process" |
| n8n's Compose stack with the n8n Assistant sandbox | At least 4 GB of RAM and 2 vCPUs |
| discord.js bot | No figure; its guide shows how to limit caches "to reduce memory usage" |
| A bot in 2,500 or more servers | Sharding is required: more gateway connections |
n8n lists what drives its memory: JSON and binary data per execution, the number of nodes, Code nodes, manual runs from the editor and workflows running at the same time.
Measure both on a busy day:
sudo docker stats --no-stream
pm2 monit
The first prints each container's MEM USAGE / LIMIT once; the second is PM2's live dashboard, run as the bot's user. Add the peaks of n8n, PostgreSQL if you use it, the bot and the system, and keep the total under about three quarters of the plan's RAM. Our RAM guide explains that margin.
Step 2: Keep n8n on 127.0.0.1 and Cap Its Memory
Without limits, a container "can use as much of a given resource as the host's kernel scheduler allows", and when the kernel runs out of memory, Docker warns, "Any process is subject to killing". A workflow that loads a large file can end your bot that way. Add these lines to the n8n service in your compose file, in place of its current ports entry:
services:
n8n:
# keep your image, env_file, environment and volumes
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
mem_limit: 2g
logging:
driver: local
cd ~/n8n # the folder with your compose file
sudo docker compose up -d
mem_limitis a hard ceiling;2gis an example, so set it above n8n's measured peak with room left for the bot. At the limit, the kernel kills processes in that container, not elsewhere.restart: unless-stoppedrestarts n8n whatever its exit code, also after a reboot, unless you stopped it yourself. Compose's default isno.127.0.0.1:5678:5678publishes the port on the loopback address only; without an address, Docker publishes it everywhere, past UFW, as our Docker and UFW guide shows.docker compose uprecreates the changed container, "preserving mounted volumes".
The ports at a glance:
| Program | Listens on | Who connects |
|---|---|---|
| n8n | 127.0.0.1:5678 | The bot, a reverse proxy, your SSH tunnel |
| Reverse proxy (optional) | 80 and 443 | Outside services that call n8n webhooks |
| Discord bot | Nothing | It opens a WebSocket to Discord's gateway itself |
If only your bot calls n8n, n8n needs no domain: open the editor through an SSH tunnel. Webhooks from outside services need the public setup in our n8n reverse proxy guide.
Step 3: Give the Bot Its Own User and Secrets
Run the bot as an account that can't read n8n's files and isn't in the docker group, which Docker's docs say "grants root-level privileges to the user":
sudo useradd --create-home --shell /bin/bash discordbot
sudo chmod 750 /home/discordbot
From your admin account, install Node.js and PM2 as in step 1 of our Next.js guide: discord.js 14.28.0 needs Node.js 18 or newer, and the --env-file flag below needs 20.6.0. Then switch with sudo -iu discordbot, put the bot in ~/bot and its secrets in ~/bot/.env:
DISCORD_TOKEN=your-bot-token
N8N_URL=http://127.0.0.1:5678
N8N_BOT_KEY=output-of-openssl-rand-hex-32
openssl rand -hex 32 prints 32 random bytes as hex. Run chmod 600 ~/bot/.env, then create ~/ecosystem.config.cjs:
module.exports = {
apps: [{
name: 'discord-bot',
cwd: '/home/discordbot/bot',
script: 'index.js',
node_args: '--env-file=/home/discordbot/bot/.env',
max_memory_restart: '400M',
time: true,
}],
};
pm2 start ~/ecosystem.config.cjs
pm2 save
pm2 startup
PM2 restarts the bot past max_memory_restart (an example; set it above the bot's peak), and time dates every log line. pm2 startup prints a sudo env PATH=... command; run it from your admin account for a pm2-discordbot service that restores the bot at boot. Our multiple Node.js apps guide adds a systemd memory ceiling per user.
The bot's .env holds the Discord token and the header value; n8n keeps the same value as a Header Auth credential, encrypted with the key in its config file. Neither needs the other's main secret: n8n can do without the bot token (step 4), and the bot never needs your n8n login. If the header value leaks, change it in both places.
Step 4: Let the Bot Call n8n Over Localhost
Start the workflow with a Webhook node:
- HTTP Method: POST. Path:
discord-report. - Authentication: Header Auth, with a credential whose Name is
X-Bot-Keyand whose Value is the bot'sN8N_BOT_KEY. - Respond: When Last Node Finishes, Response Data: First Entry JSON. End the workflow with a node that outputs a
replyfield. - Publish the workflow.
/webhook/URLs work only once it's published;/webhook-test/URLs only while you listen for a test event.
Test it on the VPS before touching the bot:
curl -i -H "X-Bot-Key: paste-the-value" \
--json '{"user": "test", "text": "hello"}' \
http://127.0.0.1:5678/webhook/discord-report
--json (curl 7.82.0 or newer) sends a JSON POST. Expect a 200 with your reply. A missing or wrong header gets 403 Authorization data is wrong!, and a 404 means the path, method or publishing is off, as our reverse proxy guide explains.
Discord expects an initial response "within 3 seconds of receiving the event", so defer first: that gives you 15 minutes to edit the reply. In your handler for a /report command with a string option text:
await interaction.deferReply();
try {
const res = await fetch(`${process.env.N8N_URL}/webhook/discord-report`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Bot-Key': process.env.N8N_BOT_KEY,
},
body: JSON.stringify({
user: interaction.user.id,
text: interaction.options.getString('text'),
}),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error(`n8n answered ${res.status}`);
const data = await res.json();
await interaction.editReply(data.reply ?? 'Done.');
} catch (err) {
console.error('n8n call failed:', err);
await interaction.editReply('The workflow did not answer. Try again later.');
}
AbortSignal.timeout() ends a stuck request with a TimeoutError after 30 seconds. Use 127.0.0.1, where the port is published, not localhost.
Jobs That Take Longer
For workflows that run for minutes, set Respond to Immediately. The bot still defers, sends the interaction's application ID and token, and leaves the reply to n8n:
body: JSON.stringify({
application: interaction.applicationId,
token: interaction.token,
text: interaction.options.getString('text'),
}),
Drop the bot's success editReply() and keep the one in catch: the immediate answer has no reply, so the bot would show "Done." before the job runs.
At the end of the workflow, an HTTP Request node sends a PATCH with a JSON body such as {"content": "Report ready"} to:
https://discord.com/api/v10/webhooks/{{ $('Webhook').first().json.body.application }}/{{ $('Webhook').first().json.body.token }}/messages/@original
Discord's endpoint for editing the original response "Functions the same as Edit Webhook Message", and webhooks "do not require a bot user or authentication to use", so n8n never holds the bot token. Keep v10 in the URL: without a version, Discord routes to its default, the deprecated v6. The token expires after 15 minutes.
For messages that aren't replies, such as a daily summary, give n8n's Discord node a Webhook credential for one channel. Both programs call Discord from the same IP address, and Discord temporarily restricts addresses that make 10,000 invalid requests (401, 403 or 429) in 10 minutes or keep calling a webhook that returned 404. A workflow stuck retrying a deleted webhook can get your bot temporarily blocked too.
If the Bot Runs in Docker
Inside a container, 127.0.0.1 is the container itself, so connect the two Compose projects through a shared network:
sudo docker network create n8n-bot
In n8n's compose file, add the network and keep default, so n8n still reaches its database:
services:
n8n:
# keep everything from step 2
networks:
- default
- n8n-bot
networks:
n8n-bot:
external: true
name: n8n-bot
The bot's compose file joins only that network and reaches n8n by its service name, with N8N_URL=http://n8n:5678 in its .env:
services:
bot:
build: .
restart: unless-stopped
env_file: .env
mem_limit: 512m
logging:
driver: local
networks:
- n8n-bot
networks:
n8n-bot:
external: true
name: n8n-bot
The bot reaches n8n but not n8n's PostgreSQL. Our Discord bot Docker guide covers the Dockerfile.
Step 5: Logs and the Shared Disk
Each side logs in its own place:
cd ~/n8n && sudo docker compose logs --since 30m -t n8n
pm2 logs discord-bot --lines 100 # as discordbot
With -t and PM2's time: true, both carry timestamps, so you can match a failed command to a workflow run. n8n logs to the console at level info by default, which Docker stores.
A full disk stops both programs. Docker's default json-file driver doesn't rotate, which "can lead to disk space exhaustion"; the local driver from step 2 keeps 100 MB per container (five 20 MB files, compressed). For the bot, run pm2 install pm2-logrotate as discordbot. n8n's execution history grows too: pruning is on by default and deletes executions older than 336 hours (14 days) or beyond 10,000, but on SQLite the freed space is reused, not returned to the disk, unless you set DB_SQLITE_VACUUM_ON_STARTUP.
Step 6: Back Up Both, Restore Either
| n8n | Bot | |
|---|---|---|
| Data | Its volume at /home/node/.n8n (n8n-data in n8n's Compose file, n8n_data in its Docker guide): the config file with the encryption key and, on SQLite, the database | Its data folder or database file |
| Database | A PostgreSQL dump, if you use it | A dump, if it uses a database server |
| Settings | The compose file and .env | .env and ecosystem.config.cjs |
| Code | The image version you run | Its Git repository |
On SQLite, stop n8n before copying the folder: "Copying the database file while n8n is writing to it can produce an inconsistent backup". Without the encryption key, restored credentials can't be decrypted. Kept apart, either side restores without rolling back the other. Archive the volume as in our Docker volumes guide, send both off the server with restic, and see our n8n restore guide for a move. On Space-Node, backups aren't included in the plan price, so run your own or arrange them through a support ticket.
When to Split Them
Give one of the two its own VPS when:
- The measured peaks no longer fit under about three quarters of the RAM. If both grow together, a bigger plan is simpler: a Space-Node VPS for n8n and AI agents has dedicated RAM, and you upgrade from the client area.
- n8n runs its Assistant sandbox. n8n's current Compose file includes it: a privileged Docker-in-Docker runner that n8n's checklist says to "Treat it as equivalent to root on the host", which puts the bot's token within its reach.
- Someone else needs access to n8n's server. Docker or sudo rights there amount to root, so they could read the bot's token.
- The bot passes 2,500 servers and must shard, splitting its events over several gateway connections.
Splitting later is a small change: point N8N_URL at https://n8n.example.com behind a reverse proxy and keep the header auth.
If It Still Fails
| What you see | Cause and fix |
|---|---|
TypeError: fetch failed, cause ECONNREFUSED | Nothing listens on 127.0.0.1:5678. Check sudo docker compose ps; curl -i http://127.0.0.1:5678/healthz/readiness returns 200 once n8n's database is ready. |
ENOTFOUND from a bot container | The bot isn't on the n8n-bot network, or n8n's service has another name. |
403 Authorization data is wrong! | The header name or value differs between the bot's .env and the credential. |
TimeoutError in the bot's log | The workflow outlasts the timeout: respond immediately and edit the reply from n8n. |
| Discord marks the command as failed | No deferReply() within 3 seconds, often because the bot waited for n8n first. |
Execution stopped at this node (n8n may have run out of memory while executing it) | n8n hit its memory: raise mem_limit within your budget, or process the data in smaller batches, as n8n suggests. |
FAQ
Does the Discord bot need an open port? No. By default a bot receives interactions over its Gateway connection, a WebSocket it opens itself. Only a bot that receives them over HTTP instead needs a public Interactions Endpoint URL.
Can n8n use my bot's token? Yes, through the Discord node's Bot credential, which you need for actions such as role changes. But Discord counts rate limits per bot token ("All bots can make up to 50 requests per second"), so n8n would spend your bot's budget, and a leak from n8n leaks the bot. For posting, a channel webhook needs no bot user; for replies, use the interaction token as in step 4.
Will updating n8n restart my bot?
No. Updating with docker compose pull, docker compose down and docker compose up -d in n8n's folder touches only that project. A reboot stops both; Docker then restarts n8n and pm2-discordbot starts the bot, possibly first, so keep the error handling from step 4.
Should n8n use SQLite or PostgreSQL here? n8n calls SQLite "fine for trying things out" and recommends PostgreSQL for "a production instance that must handle more than a handful of users or workflows running around the clock". PostgreSQL adds its own memory to the budget; our SQLite vs PostgreSQL guide compares them.
Sources
All opened on 31 October 2026:
- n8n docs: Fix memory issues, Install using Docker Compose, Install with Docker, Webhook node, Webhook credentials, HTTP Request node, Reference previous nodes, Discord node, Discord credentials, Logs variables, Manage execution data, Back up and restore, Monitor n8n
- n8n source at 2.45.7: get-n8n-compose.yml, Webhook node, Webhook auth checks, Webhook auth errors
- Discord docs: Gateway, Interactions overview, Receiving and responding, Rate limits, Webhook resource, API reference
- discord.js 14.28.0: README, BaseInteraction; guide: Response methods, Parsing options, Cache customization
- Node.js: fetch and AbortSignal.timeout,
--env-file, common system errors; undici fetch source; MDN: AbortSignal.timeout() - PM2: Ecosystem file, Log management, Startup script, Quick start, config file types in 7.1.1
- Docker: Resource constraints, Compose services reference, Compose byte values, Networking in Compose, Port publishing, Packet filtering and firewalls, Configure logging drivers, local driver, json-file driver, Dual logging, Start containers automatically, Linux post-installation steps, docker compose up, docker compose logs, docker stats
- Ubuntu Server system requirements
- Man pages and tools: useradd(8), chmod(1), sudo(8), openssl-rand(1), curl