n8n on Docker: Self-Hosted Workflow Automation for Your Homelab

n8n on Docker: Self-Hosted Workflow Automation for Your Homelab

What Is n8n and Why Should Homelab Builders Care?

If you’ve spent any time in the self-hosting community, you’ve probably run into n8n — the open-source, node-based workflow automation platform that’s been steadily eating into Zapier’s lunch. Unlike its SaaS competitors, n8n is fully self-hostable, runs beautifully in Docker, and doesn’t charge you per-task after you hit some arbitrary tier limit. For homelab builders who already have a Docker host running services like Uptime Kuma, Grafana, and Home Assistant, n8n is a natural next step.

Think of n8n as the glue between everything in your stack. It can watch a webhook, pull data from an API, transform it, then push a notification to your phone, write a row to a spreadsheet, trigger a script on another machine, or restart a Docker container — all without writing a single line of code (though you can write code when you need to). This guide walks you through deploying n8n on Docker with persistent storage, securing it behind a reverse proxy, and building three practical homelab workflows from scratch.

If you’re new to Docker or need a refresher on the fundamentals before diving in, check out our complete beginner’s guide to Docker first. And if you’re already running a monitoring stack, our guide on Uptime Kuma for homelab monitoring pairs perfectly with what we’re building here — the two tools integrate directly.


Prerequisites

Before you start, make sure you have:

  • A Linux host (Debian/Ubuntu recommended) with Docker and Docker Compose v2 installed
  • At least 2GB of RAM dedicated to n8n (4GB recommended for heavier multi-workflow setups)
  • A domain or subdomain you control, with DNS pointing to your host (for HTTPS via a reverse proxy)
  • Ports 80 and 443 accessible, or a reverse proxy already in place (Caddy, Nginx Proxy Manager, or Traefik)

I’m running this on a Proxmox VM with Ubuntu 24.04, but any Docker-capable host works — a Raspberry Pi 4 handles n8n comfortably for light-to-moderate workloads. On a Pi, stick with SQLite initially and only migrate to Postgres if you start hitting performance limits with 10+ active workflows.


Deploying n8n with Docker Compose

n8n’s official Docker image is n8nio/n8n. We’ll use Docker Compose to keep everything reproducible. Create a project directory and a docker-compose.yml:

mkdir -p ~/docker/n8n && cd ~/docker/n8n
# docker-compose.yml
version: "3.8"

services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - N8N_HOST=n8n.yourdomain.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://n8n.yourdomain.com/
      - GENERIC_TIMEZONE=America/Chicago
      - N8N_ENCRYPTION_KEY=your_32_char_random_encryption_key
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=n8n-postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8nuser
      - DB_POSTGRESDB_PASSWORD=n8n_db_password
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - n8n-postgres
    networks:
      - n8n-net

  n8n-postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    environment:
      - POSTGRES_USER=n8nuser
      - POSTGRES_PASSWORD=n8n_db_password
      - POSTGRES_DB=n8n
    volumes:
      - n8n_postgres_data:/var/lib/postgresql/data
    networks:
      - n8n-net

volumes:
  n8n_data:
  n8n_postgres_data:

networks:
  n8n-net:
    driver: bridge

A few things to customize before running:

  • Replace n8n.yourdomain.com with your actual subdomain in three places
  • Generate a random 32-character string for N8N_ENCRYPTION_KEY: openssl rand -hex 16 — store this somewhere safe
  • Set your timezone using a valid tz database value
  • Change the Postgres credentials from the defaults above

By default, n8n ships with SQLite, which is fine for experimentation. For a homelab with multiple concurrent workflows and execution history you actually want to query, PostgreSQL is more reliable and performs significantly better. That’s why we’re bundling a Postgres 16 container in this Compose stack.

Bring it up:

docker compose up -d
docker compose logs -f n8n

You should see n8n start up, connect to Postgres, and report it’s listening on port 5678. On first access, n8n 1.x prompts you to create an owner account (email + password) — this is your web UI login and replaces the older basic auth environment variables used in pre-1.0 versions. Follow the on-screen setup and store those credentials in your password manager.


Putting n8n Behind a Reverse Proxy

Exposing n8n on a bare port without HTTPS is fine for LAN-only access, but if you want external services to fire webhooks at your instance, you need HTTPS. I use Caddy because its automatic Let’s Encrypt integration requires zero certificate management.

Add this to your Caddyfile:

n8n.yourdomain.com {
    reverse_proxy n8n:5678
}

If Caddy is running in its own Docker network, connect both containers to a shared network or use host resolution. That’s all Caddy needs — it handles cert issuance and renewal automatically. Traefik and Nginx Proxy Manager work equally well if that’s already what you’re running.

Once HTTPS is live, verify your WEBHOOK_URL is set to the HTTPS URL and restart n8n:

docker compose restart n8n

This is critical — n8n generates webhook URLs based on the WEBHOOK_URL value, and if it’s set to an HTTP URL or wrong domain, every external trigger you create will have the wrong address baked in.


Navigating the n8n Interface

When you first open n8n, you’ll land on the workflow canvas — a blank grid where you drag and connect nodes. The core concepts:

  • Nodes are the building blocks. Each node performs one discrete action: trigger on a schedule, call an HTTP endpoint, filter data, transform a value, send a message, and so on.
  • Connections (the lines between nodes) pass data from one node to the next as JSON arrays. Every node receives the full output of its predecessor.
  • Triggers are special input nodes that start a workflow — cron schedules, incoming webhooks, new email in an inbox, file changes, etc.
  • Credentials are stored once, encrypted using your N8N_ENCRYPTION_KEY, and reusable across any number of workflows.

The node panel gives you access to 400+ built-in integrations — from Slack, Discord, and GitHub to Postgres, SSH, and generic HTTP Request. The HTTP Request node alone is worth the price of admission: it can call any REST API, set headers and auth, parse the response, and pass it downstream. For anything without a dedicated node, HTTP Request is your go-to.

One UI feature worth knowing early: the Expression editor. Every field in every node can use n8n’s expression syntax ({{ }}) to reference data from previous nodes. You’ll use this constantly — to pull a container name from SSH output, format a timestamp, or build a dynamic URL from upstream data.


Community Nodes: Extending n8n Beyond the Defaults

n8n has a community node registry (similar to npm packages) where developers publish additional integrations. To install a community node, go to Settings → Community Nodes → Install and enter the package name. Useful ones for homelab users include:

  • n8n-nodes-ntfy — dedicated ntfy.sh node with cleaner configuration than using HTTP Request manually
  • n8n-nodes-gotify — if you self-host Gotify for push notifications
  • n8n-nodes-proxmox — control VMs and containers on a Proxmox host directly from workflows
  • n8n-nodes-homeassistant — trigger Home Assistant services, read entity states, react to events

Community nodes run in the same process as n8n, so only install packages you trust. Stick to nodes with active maintenance and reasonable download counts.


Workflow 1: Docker Container Health Monitor with Alerts

This first workflow polls your Docker host every 5 minutes, checks for exited containers, and fires a notification if anything is down. It’s a lightweight complement to service-level monitoring tools — where Uptime Kuma tells you a web endpoint is unreachable, this workflow tells you which container crashed.

Nodes in this workflow:

  1. Schedule Trigger — every 5 minutes
  2. SSH node — runs a command on your Docker host
  3. IF node — branches on whether the output is non-empty
  4. HTTP Request node — POSTs to ntfy.sh with the container list in the body

For the SSH node, create an SSH credential with your Docker host’s IP, username, and SSH private key. The command to run:

docker ps --filter "status=exited" --format "{{.Names}}" | tr '\n' ',' | sed 's/,$//'

This returns a comma-separated list of exited containers, or an empty string if everything is running. The IF node checks whether {{ $json.stdout }} is not empty. If it branches true, the HTTP Request node sends a POST to https://ntfy.sh/your-alert-topic with:

  • Body: ⚠️ Containers down: {{ $json.stdout }}
  • Header Title: Docker Alert
  • Header Priority: high

A 4-node workflow that replaces a bash cron script and gives you a visual audit trail of every alert fired, with execution logs you can review later.


Workflow 2: RSS-to-Discord Homelab News Digest

This workflow checks an RSS feed once daily and posts a digest of new items to a Discord channel. It’s useful for tracking Cisco security advisories, Docker release notes, or any technical feed without needing a dedicated RSS reader open in a browser tab.

Nodes:

  1. Schedule Trigger — daily at 8:00 AM
  2. RSS Feed Read node — set to your chosen feed URL (e.g., https://tools.cisco.com/security/center/psirtrss20/CiscoSecurityAdvisory.xml)
  3. Filter node — keep only items where {{ new Date($json.pubDate) > new Date(Date.now() - 86400000) }} (published in the last 24 hours)
  4. Code node — formats items into a single Discord-friendly message
  5. Discord node — posts the formatted message via a channel webhook

The Filter node uses n8n’s expression engine to compare the pubDate field of each RSS item against 24 hours ago in milliseconds. Any item older than that gets dropped before reaching the formatter, so you never get duplicate alerts from items you saw yesterday.

The Code node (n8n’s JavaScript sandbox) formats the output:

const items = $input.all();
if (items.length === 0) {
  return [{ json: { message: "No new items in the last 24h." } }];
}
const lines = items.map(i => `• **${i.json.title}**\n  ${i.json.link}`);
return [{ json: { message: lines.join('\n\n').slice(0, 2000) } }];

Discord messages cap at 2000 characters, hence the slice. The Discord node receives the message field and posts it to the webhook URL you configure under Server Settings → Integrations → Webhooks.


Workflow 3: Automated Backup Verification

This one is a bit more advanced. After your nightly backup script runs, it writes a small JSON status file. n8n reads that file each morning, parses the result, and either sends a quiet confirmation or fires a high-priority alert.

Nodes:

  1. Schedule Trigger — 7:00 AM (after your backup window closes)
  2. SSH nodecat /var/log/homelab-backup-status.json
  3. Code nodereturn [{ json: JSON.parse($input.first().json.stdout) }]
  4. Switch node — routes on {{ $json.status }} being success or anything else
  5. HTTP Request node (success branch) — low-priority ntfy confirmation
  6. HTTP Request node (failure branch) — high-priority ntfy alert with error details

Your backup script writes the status file in this format:

{
  "status": "success",
  "timestamp": "2026-06-21T05:42:00",
  "duration_minutes": 18,
  "size_gb": 142.3,
  "destination": "nas01:/backups/homelab"
}

The success branch formats a message like: ✅ Backup OK — 142.3 GB in 18 min → nas01. The failure branch sends the full error string at high priority and marks the n8n execution as failed, so it shows up red in your execution history.


Securing n8n: What You Shouldn’t Skip

Set the encryption key once and back it up immediately. The N8N_ENCRYPTION_KEY encrypts every credential stored in n8n. If you lose it or change it without migrating, every API key and SSH credential in your instance becomes permanently unreadable. Generate it with openssl rand -hex 16, store the output in your password manager, and never touch it again.

Keep n8n off the public internet unless webhooks require it. If workflows only trigger internally — from Home Assistant, a local cron, or another LAN service — don’t expose port 5678 at all. Use your Tailscale mesh VPN to access the n8n UI remotely without punching holes in your firewall. Only expose the HTTPS endpoint publicly when you genuinely need external services to POST to your webhooks.

Back up the n8n data volume on a schedule. Your workflows, execution history, and credentials live in the n8n_data Docker volume. Export workflow JSON from the n8n UI periodically (Settings → Workflows → Export All), and include the Postgres data directory in your existing backup process. If you’ve already automated your homelab with Ansible, adding a docker exec play to dump the Postgres database nightly takes about 10 lines.

Rotate the Postgres password. The n8n_db_password placeholder in this guide is only for illustration. Set a real, unique password before going live — the Postgres container is networked internally, but defense in depth applies.


Tips for Building Reliable Workflows

Set up an Error Workflow before you have 20 active workflows. In n8n Settings, you can designate a dedicated error-handling workflow that fires whenever any other workflow fails. It receives the failed workflow name and error message as input, so you can log it to a file or send an alert. Debugging silent cron failures without this is painful.

Never hardcode credentials in Code nodes or HTTP URLs. Create a named credential in the Credentials panel and reference it from the node. This keeps secrets out of your workflow exports and makes rotation trivial — update the credential once, every workflow that uses it picks up the change immediately.

Use “Execute Step” aggressively during development. n8n lets you run individual nodes in isolation or step through a workflow manually. Run this before activating any scheduled or webhook-triggered workflow — especially ones that write data, send messages, or call paid APIs.

Use the Wait node between loop iterations. If you’re iterating over a list of items and calling an external API for each one, add a Wait node between calls to stay under rate limits. Set it to 1-2 seconds and it handles pagination and burst limiting cleanly.

Export your workflows to Git. n8n has no built-in version control. Export workflow JSON and commit it to a private repo. If you accidentally delete a complex 20-node workflow, you’ll be very glad you did this.


Wrapping Up

n8n earns its place in the homelab stack because it genuinely replaces a folder of cron scripts and bash one-liners with something visual, maintainable, and extensible without requiring a dedicated server or SaaS subscription. The 400+ built-in nodes cover most of what you’ll need, community nodes fill the gaps, and the Code node handles anything in between.

The Docker Compose setup in this guide is production-ready for homelab use: Postgres backend for reliable execution history, proper timezone configuration, encrypted credentials, and a clear path to HTTPS. Start with the container health monitor — it’s 4 nodes, takes 10 minutes to build, and is immediately useful.

For the next step, wire n8n into your existing Uptime Kuma monitoring stack via its webhook integration. When Uptime Kuma fires a down event, n8n receives it, attempts an SSH restart of the failed service, and reports back what it did — all automatically. That’s the self-healing homelab loop that makes the whole stack feel genuinely intelligent.

Enjoying this post?

Get more guides like this delivered straight to your inbox. No spam, just tech and trails.