Skip to content
Go back

Deploying Node.js Apps to Kubernetes with Helm Charts

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

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.


Share this post on:

Previous Post
Implementing Observability with OpenTelemetry in Node.js
Next Post
Optimizing Docker Build Performance and Image Size