Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ Use this structure to orientate yourself.
6. Raw Resources to pass through your own manifests like GatewayAPI, ServiceMonitor etc.
7. Redis related settings + Kubernetes specific settings

## Deployment Types

By default, n8n is deployed as a Kubernetes Deployment. However, in some scenarios, you may want to use a StatefulSet instead:

### StatefulSet vs Deployment

When using multiple replicas with a standard Deployment, all pods share the same persistent volume, which can lead to data corruption as multiple replicas try to access the same files simultaneously.

Using a StatefulSet (by setting `main.useStatefulSet: true`) ensures each replica gets its own persistent volume for the `/home/node/.n8n` directory. This is useful when:

- You need horizontal scaling with multiple replicas
- Each replica needs its own persistent state
- You want to preserve workflow data for each replica independently

For proper StatefulSet usage with multiple replicas, it's recommended to:
- Enable Redis/Valkey to handle session state
- Use external databases rather than SQLite
- Set a shared encryption key for all replicas

> [!IMPORTANT]
> StatefulSets manage their own PersistentVolumeClaims through `volumeClaimTemplates`. Using `persistence.existingClaim` with `useStatefulSet: true` will result in an error. When using StatefulSets, configure the persistence settings using `persistence.enabled: true`, `persistence.storageClass`, and other parameters.

### Service Configuration for StatefulSets

When using StatefulSets (`useStatefulSet: true`), the chart creates two services:
- A headless Service (`{release-name}-n8n-headless`) for StatefulSet pod management
- A regular Service (`{release-name}-n8n-svc`) for external access to the application

See the example at `examples/values_stateful.yaml` for a sample configuration.

## Configurating N8n via Values and Environment Variables

These n8n configuration should be added to `main.config:` or `main.secret:` in the `values.yaml` file.
Expand Down Expand Up @@ -173,6 +203,10 @@ main:
#
# N8n Kubernetes specific settings
#
# When true, deploy as a StatefulSet instead of a Deployment
# This ensures each replica has its own persistent volume for /home/node/.n8n
useStatefulSet: false

persistence:
# If true, use a Persistent Volume Claim, If false, use emptyDir
enabled: false
Expand Down
6 changes: 3 additions & 3 deletions charts/n8n/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v2
name: n8n
version: 1.0.14
version: 1.0.15
appVersion: 1.109.1
type: application
description: "Helm Chart for deploying n8n on Kubernetes, a fair-code workflow automation platform with native AI capabilities for technical teams. Easily automate tasks across different services."
Expand Down Expand Up @@ -34,5 +34,5 @@ annotations:
artifacthub.io/prerelease: "false"
# supported kinds are added, changed, deprecated, removed, fixed and security.
artifacthub.io/changes: |
- kind: changed
description: "Update n8n app version to 1.109.1"
- kind: added
description: "Support for StatefulSet deployment with individual persistent volumes per replica"
11 changes: 10 additions & 1 deletion charts/n8n/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- else if and .Values.main.persistence.enabled .Values.main.persistence.existingClaim -}}
persistentVolumeClaim:
claimName: {{ .Values.main.persistence.existingClaim }}
{{- else if and .Values.main.persistence.enabled (eq .Values.main.persistence.type "dynamic") -}}
{{- else if and .Values.main.persistence.enabled (eq .Values.main.persistence.type "dynamic") (not .Values.main.useStatefulSet) -}}
persistentVolumeClaim:
claimName: {{ include "n8n.fullname" . }}
{{- end }}
Expand Down Expand Up @@ -102,4 +102,13 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- end -}}

{{/* Validate StatefulSet and PVC configuration compatibility */}}
{{- define "n8n.validateStatefulSet" -}}
{{- if and .Values.main.useStatefulSet .Values.main.persistence.enabled .Values.main.persistence.existingClaim -}}
{{- fail "StatefulSets cannot use existingClaim. When useStatefulSet=true, remove persistence.existingClaim as StatefulSets manage their own PVCs through volumeClaimTemplates" -}}
{{- end -}}
{{- if and .Values.main.useStatefulSet (not .Values.main.persistence.enabled) -}}
{{- fail "StatefulSets require persistence to be enabled. When useStatefulSet=true, set persistence.enabled=true" -}}
{{- end -}}
{{- end -}}

2 changes: 2 additions & 0 deletions charts/n8n/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- if not .Values.main.useStatefulSet }}
apiVersion: apps/v1
kind: Deployment
metadata:
Expand Down Expand Up @@ -122,3 +123,4 @@ spec:
{{- if .Values.main.extraVolumes }}
{{- toYaml .Values.main.extraVolumes | nindent 8 }}
{{- end }}
{{- end }}
2 changes: 1 addition & 1 deletion charts/n8n/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{- if and .Values.main.persistence.enabled (not .Values.main.persistence.existingClaim) }}
{{- if and .Values.main.persistence.enabled (not .Values.main.persistence.existingClaim) (not .Values.main.useStatefulSet) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
Expand Down
25 changes: 25 additions & 0 deletions charts/n8n/templates/service-headless.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{{- if .Values.main.useStatefulSet }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "n8n.fullname" . }}-headless
namespace: {{ .Release.Namespace }}
labels:
{{- include "n8n.labels" . | nindent 4 }}
app.kubernetes.io/component: headless
{{- with .Values.main.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: ClusterIP
clusterIP: None
ports:
- port: {{ get (default (dict) .Values.main.config.n8n) "port" | default 5678 }}
targetPort: http
protocol: TCP
name: http
selector:
{{ include "n8n.selectorLabels" . | nindent 4 }}
app.kubernetes.io/type: master
{{- end }}
4 changes: 2 additions & 2 deletions charts/n8n/templates/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "n8n.fullname" . }}
name: {{ include "n8n.fullname" . }}{{ if .Values.main.useStatefulSet }}-svc{{ end }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "n8n.labels" . | nindent 4 }}
Expand All @@ -11,7 +11,7 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ default "ClusterIP_" .Values.main.service.type }}
type: {{ default "ClusterIP" .Values.main.service.type }}
ports:
- port: {{ default 80 .Values.main.service.port }}
targetPort: http
Expand Down
139 changes: 139 additions & 0 deletions charts/n8n/templates/statefulset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
{{- if .Values.main.useStatefulSet }}
{{- include "n8n.validateStatefulSet" . -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "n8n.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "n8n.labels" . | nindent 4 }}
{{- if .Values.main.deploymentLabels }}
{{- toYaml .Values.main.deploymentLabels | nindent 4 }}
{{- end }}
{{- if .Values.main.deploymentAnnotations }}
annotations:
{{- toYaml .Values.main.deploymentAnnotations | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.main.autoscaling.enabled }}
replicas: {{ .Values.main.replicaCount }}
{{- end }}
serviceName: {{ include "n8n.fullname" . }}-headless
selector:
matchLabels:
{{- include "n8n.selectorLabels" . | nindent 6 }}
app.kubernetes.io/type: master
template:
metadata:
annotations:
checksum/config: {{ print .Values.main | sha256sum }}
{{- with .Values.main.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "n8n.selectorLabels" . | nindent 8 }}
app.kubernetes.io/type: master
{{- if .Values.main.podLabels }}
{{ toYaml .Values.main.podLabels | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "n8n.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.main.podSecurityContext | nindent 8 }}
{{- if .Values.main.initContainers }}
initContainers:
{{ tpl (toYaml .Values.main.initContainers) . | nindent 10 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.main.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
{{- toYaml .Values.main.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
{{- if .Values.main.config }}
- configMapRef:
name: {{ include "n8n.fullname" . }}-app-config
{{- end }}
{{- if .Values.main.secret }}
- secretRef:
name: {{ include "n8n.fullname" . }}-app-secret
{{- end }}
{{- if .Values.main.extraEnv }}
env:
{{- range $key, $value := .Values.main.extraEnv }}
- name: {{ $key }}
{{- toYaml $value | nindent 14 }}
{{- end }}
{{- end }}
lifecycle:
{{- toYaml .Values.main.lifecycle | nindent 12 }}
ports:
- name: http
containerPort: {{ get (default (dict) .Values.main.config.n8n) "port" | default 5678 }}
protocol: TCP
{{- with .Values.main.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.main.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.main.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /home/node/.n8n
{{- if .Values.main.extraVolumeMounts }}
{{- toYaml .Values.main.extraVolumeMounts | nindent 12 }}
{{- end }}
{{- with .Values.hostAliases }}
hostAliases:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.main.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.main.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.main.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumeClaimTemplates:
- metadata:
name: data
{{- with .Values.main.persistence.annotations }}
annotations:
{{- range $key, $value := . }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
spec:
accessModes:
{{- range .Values.main.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.main.persistence.size }}
{{- if .Values.main.persistence.storageClass }}
{{- if eq "-" .Values.main.persistence.storageClass }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.main.persistence.storageClass }}
{{- end }}
{{- end }}
{{- end }}
3 changes: 3 additions & 0 deletions charts/n8n/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ ingress:
# the main (n8n) application related configuration + Kubernetes specific settings
# The config: {} dictionary is converted to environmental variables in the ConfigMap.
main:
# When true, deploy as a StatefulSet instead of a Deployment
# This ensures each replica has its own persistent volume
useStatefulSet: false
# See https://docs.n8n.io/hosting/configuration/environment-variables/ for all values.
config: {}
# n8n:
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ for to help you get started with a setup for your particular use case.
* values_local.yaml - n8n on local kind/k3s cluster for testing on localhost
* aws - n8n on AWS with EKS, ingress-nginx
* simple-prod - simple production setup with AWS
* values_stateful.yaml - n8n deployed as a StatefulSet with individual persistent volumes per replica

## Render Examples

Expand Down
37 changes: 37 additions & 0 deletions examples/values_stateful.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# stateful deployment for multiple replicas with persistent state per replica
main:
# Use StatefulSet to give each replica its own persistent volume
# This will create both a headless service for StatefulSet management
# and a regular service for external access
useStatefulSet: true
# Number of replicas for horizontal scaling
replicaCount: 3
# Enable persistence for each replica
# Note: When using StatefulSet, do NOT specify 'existingClaim' as StatefulSets
# manage their own PersistentVolumeClaims through volumeClaimTemplates
persistence:
enabled: true
# For StatefulSets, we use dynamic provisioning through volumeClaimTemplates
type: dynamic
size: 5Gi
accessModes:
- ReadWriteOnce
config:
n8n:
hide_usage_page: true
# For real high availability deployments:
encryption_key: "<shared-encryption-key>"
resources:
limits:
memory: 2048Mi
requests:
memory: 512Mi

# Redis is recommended for multi-replica deployments
valkey:
enabled: true
architecture: standalone
primary:
persistence:
enabled: true
size: 2Gi