Skip to main content

Docker Compose to Helm Charts Migration

This guide covers migrating Docker Compose applications to Helm charts for Kubernetes deployment, including conversion tools, advanced charting techniques, and simplified deployment strategies.

Introduction and Prerequisites

What is Helm?

Helm is the package manager for Kubernetes that simplifies deploying and managing applications. It uses charts (packages) to define, install, and upgrade Kubernetes applications.

Prerequisites

  • Kubernetes cluster access
  • Helm CLI installed
  • Docker Compose application
  • Basic understanding of Kubernetes concepts

Installing Helm

# Install Helm on Linux/macOS
curl https://get.helm.sh/helm-v3.12.0-linux-amd64.tar.gz | tar xz
sudo mv linux-amd64/helm /usr/local/bin/

# Install on Windows
choco install kubernetes-helm

# Verify installation
helm version

Conversion Tools: Kompose and Katenary

Using Kompose for Helm Conversion

Install Kompose:

# Download Kompose
curl -L https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-amd64 -o kompose
chmod +x kompose
sudo mv ./kompose /usr/local/bin/kompose

Convert to Helm Chart:

# Basic conversion
kompose convert --chart

# Convert with custom chart name
kompose convert --chart --chart-name myapp

# Convert with specific namespace
kompose convert --chart --namespace production

Generated Chart Structure:

myapp/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ └── NOTES.txt
└── .helmignore

Using Katenary for Advanced Conversion

Install Katenary:

# Install via npm
npm install -g katenary

# Or use Docker
docker run --rm -v $(pwd):/workspace katenary/katenary

Convert with Katenary:

# Convert to Helm chart
katenary convert docker-compose.yml --format helm

# Convert with custom values
katenary convert docker-compose.yml --format helm --values custom-values.yaml

Helm Repositories and Advanced Charting

Creating a Custom Helm Chart

Initialize Chart:

# Create new chart
helm create myapp

# Chart structure
myapp/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── charts/ # Dependencies
├── templates/ # Kubernetes manifests
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── ingress.yaml
│ ├── pvc.yaml
│ ├── hpa.yaml
│ ├── serviceaccount.yaml
│ ├── _helpers.tpl
│ ├── NOTES.txt
│ └── tests/
└── .helmignore

Chart.yaml Configuration

apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- webapp
- api
home: https://github.com/myorg/myapp
sources:
- https://github.com/myorg/myapp
maintainers:
- name: Your Name
email: your.email@example.com
dependencies:
- name: postgresql
version: 12.x.x
repository: https://charts.bitnami.com/bitnami
- name: redis
version: 17.x.x
repository: https://charts.bitnami.com/bitnami

Values.yaml Structure

# Global values
global:
environment: production
domain: example.com

# Application configuration
app:
name: myapp
image:
repository: myapp
tag: latest
pullPolicy: IfNotPresent
replicas: 3
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
ports:
- name: http
containerPort: 8080
servicePort: 80

# Database configuration
database:
enabled: true
type: postgresql
host: postgresql
port: 5432
name: myapp
user: myapp
password: ""
existingSecret: ""

# Redis configuration
redis:
enabled: true
host: redis
port: 6379
password: ""

# Ingress configuration
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls: []

# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 8080

# Persistence configuration
persistence:
enabled: true
storageClass: ""
accessMode: ReadWriteOnce
size: 10Gi

# Autoscaling
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80

Template Examples

Deployment Template:

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.app.replicas }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
ports:
{{- range .Values.app.ports }}
- name: {{ .name }}
containerPort: {{ .containerPort }}
protocol: TCP
{{- end }}
env:
{{- if .Values.database.enabled }}
- name: DATABASE_URL
value: "{{ .Values.database.type }}://{{ .Values.database.user }}:{{ .Values.database.password }}@{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}"
{{- end }}
{{- if .Values.redis.enabled }}
- name: REDIS_URL
value: "redis://{{ .Values.redis.host }}:{{ .Values.redis.port }}"
{{- end }}
{{- range $key, $value := .Values.app.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
resources:
{{- toYaml .Values.app.resources | nindent 12 }}
{{- if .Values.livenessProbe }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.readinessProbe }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
{{- end }}

Service Template:

apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
{{- range .Values.app.ports }}
- port: {{ .servicePort }}
targetPort: {{ .containerPort }}
protocol: TCP
name: {{ .name }}
{{- end }}
selector:
{{- include "myapp.selectorLabels" . | nindent 4 }}

Ingress Template:

{{- if .Values.ingress.enabled -}}
{{- $fullName := include "myapp.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if .pathType }}
pathType: {{ .pathType }}
{{- end }}
backend:
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

Helper Templates

_helpers.tpl:

{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Score and Simplified Deployment

Helm Chart Scoring

Chart Quality Metrics:

# Install chart-testing
pip install yamale yamllint pytest

# Run chart tests
ct lint --charts charts/myapp

# Validate chart
helm lint charts/myapp

# Test chart installation
helm test myapp

Chart Best Practices:

  • Use semantic versioning
  • Include comprehensive documentation
  • Provide sensible defaults
  • Support multiple environments
  • Include health checks
  • Implement proper resource limits

Simplified Deployment Process

1. Package Chart:

# Package chart
helm package myapp/

# Install from package
helm install myapp myapp-0.1.0.tgz

2. Deploy with Values:

# Deploy with custom values
helm install myapp ./myapp \
--values values-production.yaml \
--set app.replicas=5 \
--set database.password=secret123

# Deploy with specific namespace
helm install myapp ./myapp \
--namespace production \
--create-namespace

3. Upgrade Deployment:

# Upgrade existing deployment
helm upgrade myapp ./myapp \
--values values-production.yaml

# Rollback if needed
helm rollback myapp 1

4. Uninstall:

# Remove deployment
helm uninstall myapp

# Remove with namespace
helm uninstall myapp --namespace production
kubectl delete namespace production

CI/CD Integration

GitHub Actions Example:

name: Deploy to Kubernetes

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.12.0

- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig

- name: Deploy to Kubernetes
run: |
helm upgrade --install myapp ./charts/myapp \
--namespace production \
--create-namespace \
--values values-production.yaml \
--set app.image.tag=${{ github.sha }}

GitLab CI Example:

stages:
- deploy

deploy:
stage: deploy
image: alpine/helm:latest
script:
- helm repo add bitnami https://charts.bitnami.com/bitnami
- helm dependency update ./charts/myapp
- helm upgrade --install myapp ./charts/myapp \
--namespace production \
--create-namespace \
--values values-production.yaml
only:
- main

Advanced Deployment Strategies

Blue-Green Deployment:

# values-blue.yaml
app:
image:
tag: v1.0
replicas: 3

# values-green.yaml
app:
image:
tag: v2.0
replicas: 3
# Deploy blue version
helm install myapp-blue ./myapp --values values-blue.yaml

# Deploy green version
helm install myapp-green ./myapp --values values-green.yaml

# Switch traffic
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"green"}}}'

Canary Deployment:

# values-canary.yaml
app:
image:
tag: v2.0
replicas: 1

autoscaling:
enabled: true
minReplicas: 1
maxReplicas: 5
# Deploy canary
helm install myapp-canary ./myapp --values values-canary.yaml

# Monitor canary performance
kubectl get hpa myapp-canary

# Promote canary
helm upgrade myapp ./myapp --values values-canary.yaml

Summary

Migrating from Docker Compose to Helm charts provides:

  1. Standardized packaging for Kubernetes applications
  2. Version management and rollback capabilities
  3. Environment-specific configurations through values
  4. Dependency management for complex applications
  5. CI/CD integration for automated deployments

The key is to start with automated conversion tools, then customize the generated charts for your specific needs and deployment strategies.