Deploying Node.js Apps to Kubernetes with Helm Charts
Introduction
Helm simplifies Kubernetes deployments by providing templating and package management. This guide creates Helm charts for Node.js applications with proper configuration management.
Prerequisites
- Kubernetes cluster
- Helm v3 installed
- kubectl configured
Step 1: Install Helm
# macOS
brew install helm
# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Verify installation
helm version
Step 2: Create Helm Chart
helm create nodejs-app
cd nodejs-app
This creates the basic structure:
nodejs-app/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── ...
Step 3: Configure Chart.yaml
Edit Chart.yaml
:
apiVersion: v2
name: nodejs-app
description: A Helm chart for Node.js application
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
- name: Your Team
email: team@example.com
keywords:
- nodejs
- api
- web
Step 4: Define Values
Edit values.yaml
:
replicaCount: 3
image:
repository: your-registry/nodejs-app
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: nodejs-app-tls
hosts:
- api.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
env:
NODE_ENV: production
PORT: 3000
secrets:
database:
host: postgres.example.com
username: myuser
password: mypassword
database: mydb
jwt:
secret: jwt-secret-key
redis:
url: redis://redis.example.com:6379
configMap:
data:
app.json: |
{
"name": "nodejs-app",
"version": "1.0.0",
"logging": {
"level": "info"
}
}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
Step 5: Create Deployment Template
Edit templates/deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "nodejs-app.fullname" . }}
labels:
{{- include "nodejs-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "nodejs-app.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "nodejs-app.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "nodejs-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.env.PORT }}
protocol: TCP
env:
- name: NODE_ENV
value: {{ .Values.env.NODE_ENV }}
- name: PORT
value: "{{ .Values.env.PORT }}"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "nodejs-app.fullname" . }}-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "nodejs-app.fullname" . }}-secrets
key: jwt-secret
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: {{ include "nodejs-app.fullname" . }}-secrets
key: redis-url
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: tmp
mountPath: /tmp
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: config
configMap:
name: {{ include "nodejs-app.fullname" . }}-config
- name: tmp
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Step 6: Create ConfigMap and Secrets Templates
Create templates/configmap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "nodejs-app.fullname" . }}-config
labels:
{{- include "nodejs-app.labels" . | nindent 4 }}
data:
{{- toYaml .Values.configMap.data | nindent 2 }}
Create templates/secret.yaml
:
apiVersion: v1
kind: Secret
metadata:
name: {{ include "nodejs-app.fullname" . }}-secrets
labels:
{{- include "nodejs-app.labels" . | nindent 4 }}
type: Opaque
data:
database-url: {{ printf "postgresql://%s:%s@%s/%s" .Values.secrets.database.username .Values.secrets.database.password .Values.secrets.database.host .Values.secrets.database.database | b64enc }}
jwt-secret: {{ .Values.secrets.jwt.secret | b64enc }}
redis-url: {{ .Values.secrets.redis.url | b64enc }}
Step 7: Create HPA Template
Create templates/hpa.yaml
:
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "nodejs-app.fullname" . }}
labels:
{{- include "nodejs-app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "nodejs-app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
Step 8: Environment-Specific Values
Create values-production.yaml
:
replicaCount: 5
image:
tag: "v1.2.3"
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
ingress:
hosts:
- host: api.production.example.com
paths:
- path: /
pathType: Prefix
env:
NODE_ENV: production
Create values-staging.yaml
:
replicaCount: 2
image:
tag: "latest"
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
ingress:
hosts:
- host: api.staging.example.com
paths:
- path: /
pathType: Prefix
Step 9: Deployment Commands
# Install to staging
helm install nodejs-app . -f values-staging.yaml -n staging --create-namespace
# Install to production
helm install nodejs-app . -f values-production.yaml -n production --create-namespace
# Upgrade deployment
helm upgrade nodejs-app . -f values-production.yaml -n production
# Rollback to previous version
helm rollback nodejs-app 1 -n production
# Check status
helm status nodejs-app -n production
# Uninstall
helm uninstall nodejs-app -n production
Step 10: Testing and Validation
Create test scripts:
#!/bin/bash
set -e
NAMESPACE=${1:-default}
RELEASE=${2:-nodejs-app}
echo "Testing deployment $RELEASE in namespace $NAMESPACE"
# Wait for pods to be ready
kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=$RELEASE -n $NAMESPACE --timeout=300s
# Test service connectivity
kubectl run test-pod --rm -i --tty --image=curlimages/curl -- sh -c "curl -f http://$RELEASE.$NAMESPACE.svc.cluster.local/health"
echo "Deployment test completed successfully"
Summary
Helm charts provide templating, versioning, and environment management for Kubernetes deployments. Use ConfigMaps and Secrets for configuration, implement health checks, and leverage HPA for automatic scaling of Node.js applications.