Automating My Homelab with n8n

Automating My Homelab with n8n

For years the glue holding my homelab together was a folder of shell scripts and cron jobs. They worked until they didn't, and when one failed silently at 3am I usually found out days later. n8n replaced almost all of it. It's a self-hosted workflow automation tool where each automation is a graph of nodes you can see, run step by step, and inspect — and because I host it myself, my data never leaves the lab.

In this post I'll show how I run n8n in Docker and walk through a small but genuinely useful workflow: a webhook that receives an event, shapes the data in a Code node, and fires a notification.

Running n8n in Docker

n8n ships an official image, so it slots right into the Compose-based setup I use for everything else. Here's a minimal, current configuration:

1services:
2  n8n:
3    image: n8nio/n8n:latest
4    restart: unless-stopped
5    ports:
6      - '5678:5678'
7    environment:
8      - N8N_HOST=n8n.example.com
9      - N8N_PORT=5678
10      - N8N_PROTOCOL=https
11      - WEBHOOK_URL=https://n8n.example.com/
12      - GENERIC_TIMEZONE=America/Chicago
13      - TZ=America/Chicago
14    volumes:
15      - n8n_data:/home/node/.n8n
16
17volumes:
18  n8n_data:
19

Two settings matter more than they look.

1WEBHOOK_URL
tells n8n the public URL to advertise for incoming webhooks — get this wrong and the URLs it generates won't be reachable from outside. And
1GENERIC_TIMEZONE
makes the Schedule node fire when you expect it to, instead of in UTC. The
1n8n_data
volume persists your credentials and workflows, so a container rebuild doesn't wipe your work.

On first launch n8n walks you through creating an owner account via its built-in user management — no extra auth wiring needed.

A real workflow: webhook → transform → notify

My most-used pattern is dead simple: something happens, n8n catches it on a webhook, reshapes the payload, and pushes a notification. Here's the shape of it.

1. The Webhook node. Add a Webhook node, set the method to

1POST
, and give it a path like
1deploy-finished
. n8n hands you a test URL and a production URL. Anything that can make an HTTP request can now trigger the workflow:

1curl -X POST https://n8n.example.com/webhook/deploy-finished \
2  -H 'Content-Type: application/json' \
3  -d '{"service":"blog","status":"success","duration":42}'
4

Inside n8n, the incoming request shows up as a single item whose

1json
holds the parsed
1body
, plus
1headers
and
1query
.

2. The Code node. This is where n8n earns its keep. The Code node runs JavaScript over the items flowing through it. In "Run Once for All Items" mode you read the input with

1$input.all()
and return an array of items, each wrapped in a
1json
key:

1// Code node — shape the incoming webhook into a clean message
2const items = $input.all();
3
4return items.map((item) => {
5  const { service, status, duration } = item.json.body;
6  const emoji = status === 'success' ? '✅' : '❌';
7
8  return {
9    json: {
10      message: `${emoji} Deploy of ${service} ${status} in ${duration}s`,
11    },
12  };
13});
14

That contract —

1$input.all()
in, an array of
1{ json: ... }
out — is the whole mental model for the Code node. Once it clicks, you can do almost any transformation without leaving the editor.

3. The notification. Wire the Code node into whatever you use to be reached. I send a request to a self-hosted ntfy instance with an HTTP Request node, posting

1{{ $json.message }}
as the body. When the deploy webhook fires, the phone in my pocket buzzes with the formatted line.

Why this beats cron scripts

The thing I didn't expect was how much the visibility would matter. Every execution is recorded: I can open a run, click any node, and see exactly what data went in and came out. When something breaks, I'm not guessing — I'm looking at the failed item. n8n can also retry failed executions and email me when a workflow errors, which is the silent-failure problem solved outright.

It's also composable in a way scripts never were. The same

1deploy-finished
webhook now branches: it notifies me and writes a row to Postgres for a little deploy-history dashboard. Adding that branch was dragging out one more node, not rewriting a script.

Where I'd start

If you want to try this, host n8n with the Compose snippet above, then build the smallest workflow that removes a real annoyance — a webhook that pings you when a backup finishes, say. Get one end-to-end automation working and inspectable, and you'll start seeing candidates for it everywhere. My cron folder has been shrinking ever since.