In this post, I’ll share my journey exploring n8n —a flexible, open-source workflow automation tool with built-in AI integrations. I’ll walk through how I set it up locally on my Windows machine and later deployed it to my homelab environment.

Prerequiste: Cloning and Running n8n Locally

The Self-hosted AI Starter Kit is an open-source Docker Compose template designed to quickly spin up a comprehensive local AI and low-code automation environment.

Clone the Repository

git clone https://github.com/n8n-io/self-hosted-ai-starter-kit.git
cd self-hosted-ai-starter-kit.git

Configure Local Settings

Start by copying the sample environment file:

cp .env.example .env

Since I’m working on a Windows setup, and the Docker images are primarily Linux-based, I ran everything through WSL (Windows Subsystem for Linux). I also modified the following environment variables to suit my setup:

N8N_RUNNERS_ENABLED=true
N8N_LISTEN_ADDRESS=0.0.0.0
n8n-docker-compose-up

Start the Services

Spin up the necessary containers using:

docker compose --profile gpu-nvidia up

Once everything is up and running, visit http://localhost:5678 to access the n8n dashboard.

n8n-local-setup n8n-local-setup-success

Demo Workflow

To get a quick feel of how things work, click on the Demo workflow from the Overview tab, or directly navigate to:

http://localhost:5678/workflow/srOnR8PAY3u4RSwb

Make sure to configure the required fallback model. In my case, I used the OpenRouter Chat Model.

n8n-demo-workflow

Learn by Examples

One of the best ways to explore n8n is by diving into real workflows. The official Workflow Automation Templates are a great starting point.

First Example: API Fundamentals

Let’s start with the tutorial: Learn API Fundamentals with an Interactive Hands-On Tutorial

n8n-first-example
  1. Open a Node

Select a node, press Enter or double-click to open its configuration panel.

n8n-our-data-source-open
  1. Access a Simple Value

Use the expression:

{{ $('Source Data').item.json.name }}
n8n-access-simple-value
  1. Using n8n Selectors

Handy helpers like .first(), .last(), .all():

n8n-the-n8n-selectors
  1. Accessing Array Elements

Example:

{{ $('Source Data').last().json.skills[1] }}
n8n-working-with-arrays
  1. Working with Nested Data

Example:

{{ $('Source Data').last().json.contact.email }}
n8n-accessing-nested-data
  1. Accessing Data in Object Arrays

Example:

{{ $('Source Data').last().json.projects[0].status }}
n8n-access-data-in-nested-object-arrays
  1. Using JavaScript Functions

Example:

{{ $('Source Data').last().json.name.toUpperCase() }}
n8n-js-functions
  1. Inspecting Objects

Useful when exploring dynamic JSON:

Object.keys($('Source Data').item.json)
n8n-inspecting-objects
  1. Stringify Object Data

Example:

JSON.stringify($('Source Data').item.json)
n8n-stringify-object
  1. Working with Multiple Items

Combine $items with arrow functions for batch processing:

n8n-working-with-multiple-items

Optional – Running n8n in My Homelab

For those who prefer deploying n8n on a Kubernetes cluster, I’ve included a streamlined setup using Kompose to convert the Docker Compose setup into Kubernetes manifests. Below is my working configuration deployed in my homelab.

n8n-kompose-convert

PostgreSQL Deployment

Use the following commands to deploy the PostgreSQL database:

kubectl apply -f postgres-storage-persistentvolumeclaim.yaml
kubectl apply -f postgres-deployment.yaml
kubectl apply -f postgres-service.yaml
Postgres K8s files
  1. Persistent Volume Claim
# postgres-storage-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    io.kompose.service: postgres-storage
  name: postgres-storage
  namespace: n8n
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
  1. Deployment
# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    io.kompose.service: postgres
  name: postgres
  namespace: n8n
spec:
  replicas: 1
  selector:
    matchLabels:
      io.kompose.service: postgres
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        io.kompose.service: postgres
    spec:
      containers:
        - env:
            - name: POSTGRES_DB
              value: n8n
            - name: POSTGRES_PASSWORD
              value: postgres
            - name: POSTGRES_USER
              value: postgres
          image: postgres:16-alpine
          livenessProbe:
            exec:
              command:
                - pg_isready -h localhost -U postgres -d n8n
            failureThreshold: 10
            periodSeconds: 5
            timeoutSeconds: 5
          name: postgres
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgres-storage
      hostname: postgres
      restartPolicy: Always
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-storage
  1. Service
# postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    io.kompose.service: postgres
  name: postgres
  namespace: n8n
spec:
  ports:
    - name: "5432"
      port: 5432
      targetPort: 5432
  selector:
    io.kompose.service: postgres
  type: ClusterIP

n8n Deployment

Use the following commands to deploy n8n:

kubectl apply -f n8n-demo-persistentVolumeClaim.yaml
kubectl apply -f n8n-storage-persistentVolumeClaim.yaml
kubectl apply -f n8n-claim2-persistentVolumeClaim.yaml
kubectl apply -f n8n-job.yaml
kubectl apply -f n8n-configmap.yaml
kubectl apply -f n8n-deployment.yaml
kubectl apply -f n8n-service.yaml
n8n K8s files
  1. Persistent Volume Claims

Multiple PVCs are used to separate storage for runtime data, demo workflows, and shared content.

# n8n-demo-persistentVolumeClaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    io.kompose.service: demo-data-pvc
  name: demo-data-pvc
  namespace: n8n
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi    
# n8n-storage-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    io.kompose.service: n8n-storage
  name: n8n-storage
  namespace: n8n
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
# n8n-claim2-persistentVolumeClaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    io.kompose.service: n8n-claim2
  name: n8n-claim2
  namespace: n8n
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
  1. Import Job

This job initializes the demo credentials and workflows:

# n8n-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  labels:
    io.kompose.service: n8n-import
  name: n8n-import
  namespace: n8n
spec:
  template:
    metadata:
      labels:
        io.kompose.service: n8n-import
    spec:
      containers:
        - args:
            - -c
            - n8n import:credentials --separate --input=/demo-data/credentials && n8n import:workflow --separate --input=/demo-data/workflows
          command:
            - /bin/sh
          env:
            - name: DB_POSTGRESDB_HOST
              value: postgres
            - name: DB_POSTGRESDB_PASSWORD
              value: postgres
            - name: DB_POSTGRESDB_USER
              value: postgres
            - name: DB_TYPE
              value: postgresdb
            - name: N8N_DIAGNOSTICS_ENABLED
              value: "false"
            - name: N8N_PERSONALIZATION_ENABLED
              value: "false"
            - name: OLLAMA_HOST
              value: ollama:11434
          envFrom:
            - configMapRef:
                name: env
          image: n8nio/n8n:latest
          name: n8n-import
          volumeMounts:
            - mountPath: /demo-data
              name: demo-data
      hostname: n8n-import
      restartPolicy: Never
      volumes:
        - name: demo-data
          persistentVolumeClaim:
            claimName: demo-data-pvc
  1. ConfigMap

Centralized environment variables:

# n8n-configmap.yaml
apiVersion: v1
data:
  N8N_DEFAULT_BINARY_DATA_MODE: filesystem
  N8N_ENCRYPTION_KEY: super-secret-key
  N8N_LISTEN_ADDRESS: 0.0.0.0
  N8N_RUNNERS_ENABLED: "true"
  N8N_USER_MANAGEMENT_JWT_SECRET: even-more-secret
  N8N_SECURE_COOKIE: "false"
  POSTGRES_DB: n8n
  POSTGRES_PASSWORD: postgres
  POSTGRES_USER: postgres
kind: ConfigMap
metadata:
  labels:
    io.kompose.service: n8n-env
  name: env
  namespace: n8n
  1. Deployment
# n8n-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    io.kompose.service: n8n
  name: n8n
  namespace: n8n
spec:
  replicas: 1
  selector:
    matchLabels:
      io.kompose.service: n8n
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        io.kompose.service: n8n 
    spec:
      containers:
        - env:
            - name: DB_POSTGRESDB_HOST
              value: postgres
            - name: DB_POSTGRESDB_PASSWORD
              value: postgres
            - name: DB_POSTGRESDB_USER
              value: postgres
            - name: DB_TYPE
              value: postgresdb
            - name: N8N_DIAGNOSTICS_ENABLED
              value: "false"
            - name: N8N_PERSONALIZATION_ENABLED
              value: "false"
            - name: OLLAMA_HOST
              value: ollama:11434
          envFrom:
            - configMapRef:
                name: env
          image: n8nio/n8n:latest
          name: n8n
          ports:
            - containerPort: 5678
              protocol: TCP
          volumeMounts:
            - mountPath: /home/node/.n8n
              name: n8n-storage
            - mountPath: /data/shared
              name: n8n-claim2
            - mountPath: /demo-data
              name: demo-data
      hostname: n8n
      restartPolicy: Always
      volumes:
        - name: n8n-storage
          persistentVolumeClaim:
            claimName: n8n-storage
        - name: n8n-claim2
          persistentVolumeClaim:
            claimName: n8n-claim2
        - name: demo-data
          persistentVolumeClaim:
            claimName: demo-data-pvc
  1. Service
# n8n-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    io.kompose.service: n8n
  name: n8n
spec:
  ports:
    - name: "5678"
      port: 5678
      targetPort: 5678
  selector:
    io.kompose.service: n8n
  type: LoadBalancer

Homelab n8n In Action

Once the Kubernetes services are up and running, check the external IP address or NodePort exposed by your homelab cluster. In my case, I accessed n8n via:

http://192.168.68.222:5678/
n8n-homelab-services

With n8n now running in my homelab, I imported the same demo workflow used earlier. Everything worked seamlessly—just like the local setup, but now running in a self-hosted, scalable Kubernetes environment.

n8n-homelab-learn-n8n-expressions

Conclusion

Exploring n8n has been a rewarding experience—from running it locally with Docker to deploying it on a Kubernetes homelab. With its low-code interface, flexible integrations, and powerful scripting capabilities, n8n makes workflow automation approachable and scalable. Whether you’re just starting out or integrating AI into your pipelines, there’s a lot to build and automate.

Happy nodemating!