Skip to content

Docker Compose vs. Helm Charts

Docker Compose and Helm Charts both serve to define and manage multi-container applications, but they operate in different environments and have different scopes and capabilities.

Comparison

Here’s a breakdown of their differences, similarities, and use cases:

Docker Compose

  • What it is: A tool for defining and running multi-container Docker applications on a single Docker host. It uses a YAML file (docker-compose.yml) to configure the application’s services, networks, and volumes.
  • Target Environment: Primarily local development, testing, and simple single-host deployments.
  • Key Features:
    • Simple YAML Configuration: Easy to learn and write.
    • Service Definition: Defines containers, images, ports, volumes, networks, environment variables, dependencies, etc.
    • Orchestration (Basic): Manages the lifecycle of containers (start, stop, rebuild) on a single host.
    • Networking: Automatically creates a default network for services to communicate.
    • Volume Management: Simplifies persistent data management.
  • Pros:
    • Simplicity: Very easy to get started with for local development.
    • Fast Iteration: Quick to bring up and tear down environments.
    • Ideal for Local Development: Perfectly mimics a multi-service environment locally.
    • Good for CI/CD: Can be used in CI pipelines for building and testing applications.
  • Cons:
    • Single Host Limitation: Not designed for distributed, multi-node production clusters.
    • No Real Orchestration: Lacks features like auto-scaling, self-healing, rolling updates, load balancing across multiple hosts (these are Docker Swarm or Kubernetes features).
    • Limited Templating: Relies mostly on environment variable substitution.
  • When to use Docker Compose:
    • Local development environments.
    • Automated testing in CI pipelines.
    • Simple, single-host application deployments (e.g., a personal blog with a database).

Helm Charts

  • What it is: The package manager for Kubernetes. Helm uses “Charts,” which are collections of files that describe a related set of Kubernetes resources. A chart is essentially a template for deploying an application or a piece of an application to a Kubernetes cluster.
  • Target Environment: Kubernetes clusters (development, staging, production).
  • Key Features:
    • Kubernetes Native: Designed specifically to manage Kubernetes applications.
    • Templating Engine (Go Templates): Allows for highly configurable and reusable deployments. Values can be injected at deployment time via values.yaml files or command-line flags.
    • Release Management: Manages “releases” (instances of a chart deployed to a cluster), allowing for versioning, upgrades, and rollbacks.
    • Dependency Management: Charts can depend on other charts.
    • Shareable Packages: Charts can be packaged into .tgz files and shared via Chart Repositories (like Artifact Hub or private ones).
    • Lifecycle Hooks: Allows for custom actions during different phases of a release lifecycle (e.g., pre-install, post-upgrade).
  • Pros:
    • Standardized Kubernetes Deployments: Provides a consistent way to package and deploy applications on Kubernetes.
    • Reusability and Configurability: Templating makes charts highly reusable across different environments (dev, staging, prod) with different configurations.
    • Version Control & Rollbacks: Manages application versions and facilitates easy upgrades and rollbacks.
    • Complex Application Management: Simplifies the deployment of complex applications with many interdependent Kubernetes resources.
    • Community Support: Large number of pre-built charts available for common software.
  • Cons:
    • Steeper Learning Curve: Understanding Helm concepts and Go templating can take time.
    • Kubernetes Complexity: Inherits the complexity of Kubernetes itself.
    • Overkill for Simple Use Cases: Can be too much for very simple applications or local development if not targeting Kubernetes.
    • Templating Can Get Complex: For very intricate charts, the Go templating can become hard to manage and debug.
  • When to use Helm Charts:
    • Deploying applications to any Kubernetes cluster.
    • Managing the lifecycle (install, upgrade, rollback) of applications on Kubernetes.
    • Creating reusable and configurable deployment packages for Kubernetes.
    • Sharing Kubernetes application configurations.

Key Differences Summarized

Feature Docker Compose Helm Charts
Target System Docker Engine (typically single host) Kubernetes Cluster (multi-node)
Purpose Define & run multi-container apps locally/simple Package manager for Kubernetes applications
Orchestration Basic (container lifecycle on one host) Leverages full Kubernetes orchestration capabilities
Templating Minimal (environment variables) Powerful (Go templating)
Packaging docker-compose.yml Charts (.tgz archives)
Repositories Docker Hub (for images) Chart Repositories (e.g., Artifact Hub)
Lifecycle Mgmt. up, down, build install, upgrade, rollback, delete
Complexity Low Medium to High
Use Case Focus Local Development, CI, simple single-host Production-grade Kubernetes deployments

Can they be used together?

Yes, indirectly or as part of a workflow:

  1. Development Phase: You might use Docker Compose for local development because of its simplicity and speed.
  2. Transition to Kubernetes: Once you’re ready to deploy to Kubernetes, you’d create Helm charts.
    • Tools like Kompose can help convert docker-compose.yml files into Kubernetes manifests, which can then be a starting point for creating a Helm chart. However, the conversion is often not perfect and requires manual refinement to leverage Kubernetes-specific features.
  3. CI/CD Pipeline:
    • A CI/CD pipeline might use Docker Compose to build and test an application.
    • Then, it would use Helm to package and deploy the tested application to a Kubernetes staging or production environment.

Analogy

  • Docker Compose is like a detailed recipe for cooking a multi-course meal in your own kitchen (single host). It tells you the ingredients (images), how to prepare them (volumes, ports), and in what order (dependencies).
  • Helm Charts are like a blueprint and operations manual for setting up and running a franchise restaurant (Kubernetes application). The blueprint is templated (so you can open restaurants in different locations with slight variations) and includes instructions for opening day, upgrades, and even shutting down a location if needed.

⇒ choose Docker Compose for its simplicity in local development and single-host scenarios. Choose Helm Charts when you need robust, configurable, and lifecycle-managed deployments on Kubernetes.

Concrete Examples

Let’s illustrate with a simple project: A web application (e.g., Python Flask) that uses a Redis database to store a counter.

The Simple Project:

  • app (Web Application):
    • A Flask app (app.py) that increments a counter in Redis on each visit and displays it.
    • Dockerfile to containerize the Flask app.
    • requirements.txt for Python dependencies (Flask, Redis).
  • redis (Database):
    • A standard Redis image.

Scenario 1: Docker Compose

Directory Structure:

simple-project/
├── app/
│   ├── app.py
│   ├── Dockerfile
│   └── requirements.txt
└── docker-compose.yml

app/requirements.txt:

Flask
redis

app/Dockerfile:

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
# Environment variable for Redis host, will be set by Docker Compose
# ENV REDIS_HOST=redis
CMD ["flask", "run"]

app/app.py:

from flask import Flask
import redis
import os

app = Flask(__name__)
# Get Redis host from environment variable, default to 'redis' if not set
redis_host = os.environ.get('REDIS_HOST', 'redis')
r = redis.Redis(host=redis_host, port=6379, db=0, decode_responses=True)

@app.route('/')
def hello():
    count = r.incr('hits')
    return f'Hello from Web App! I have been seen {count} times.\n'

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

docker-compose.yml:

version: '3.8'

services:
  web:
    build: ./app         # Build the image from the Dockerfile in ./app
    ports:
      - "5000:5000"     # Map host port 5000 to container port 5000
    volumes:
      - ./app:/app      # Mount local ./app directory into container for live code changes
    environment:
      - FLASK_DEBUG=1
      - REDIS_HOST=redis # Tells the app where to find Redis (service name)
    depends_on:
      - redis           # Ensures Redis starts before the web app

  redis:
    image: "redis:alpine" # Use a standard Redis image from Docker Hub
    ports:
      - "6379:6379"     # Expose Redis port (optional, for direct access/debugging)
    # volumes: # Optional: persist Redis data
    #   - redis_data:/data

# volumes: # Optional: define named volume for Redis
#   redis_data:

To Run Docker Compose:

  1. Navigate to the simple-project/ directory.
  2. Run docker-compose up --build
  3. Access the web app at http://localhost:5000.

Explanation of Docker Compose:

  • Defines two services: web and redis.
  • web service is built from the local app/Dockerfile.
  • ports map host ports to container ports.
  • volumes mount the local app directory for development, so changes are reflected live.
  • environment sets REDIS_HOST to redis, which is the service name Docker Compose uses for DNS resolution within its internal network.
  • depends_on ensures redis starts before web.
  • redis service uses a public image.

Scenario 2: Helm Chart (for Kubernetes Deployment)

Let’s create a Helm chart named my-simple-app.

Directory Structure (Helm Chart):

my-simple-app/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── _helpers.tpl
│   ├── web-deployment.yaml
│   ├── web-service.yaml
│   ├── redis-deployment.yaml  # Simplified for this example
│   └── redis-service.yaml     # Simplified for this example
└── .helmignore

(Note: For a production Redis, you’d typically use a dependency chart like Bitnami’s Redis chart. Here, we’ll create simplified Redis resources for direct comparison.)

Chart.yaml:

apiVersion: v2
name: my-simple-app
description: A Helm chart for a simple web app with Redis
type: application
version: 0.1.0
appVersion: "1.0.0" # Version of the application itself

values.yaml (Default Configuration):

web:
  replicaCount: 1
  image:
    repository: your-dockerhub-username/simple-flask-app # <-- PUSH YOUR APP IMAGE HERE
    tag: "latest"
    pullPolicy: IfNotPresent
  service:
    type: LoadBalancer # Or NodePort/ClusterIP
    port: 80
  containerPort: 5000
  # resources: {} # Optional: CPU/Memory limits

redis:
  image:
    repository: redis
    tag: "alpine"
    pullPolicy: IfNotPresent
  port: 6379
  # persistence: # For simplicity, not enabling persistence here
  #   enabled: false
  # resources: {} # Optional: CPU/Memory limits

templates/_helpers.tpl (Common Labels & Names):

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

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

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

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "my-simple-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Create a default fully qualified app name for a component.
*/}}
{{- define "my-simple-app.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Create a default fully qualified component name.
*/}}
{{- define "my-simple-app.component.fullname" -}}
{{- $componentName := index . 1 -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s-%s" .Release.Name $name $componentName | trunc 63 | trimSuffix "-" -}}
{{- end -}}

templates/web-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-simple-app.component.fullname" (list . "web") }}
  labels:
    {{- include "my-simple-app.labels" . | nindent 4 }}
    app.kubernetes.io/component: web
spec:
  replicas: {{ .Values.web.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-simple-app.selectorLabels" . | nindent 6 }}
      app.kubernetes.io/component: web
  template:
    metadata:
      labels:
        {{- include "my-simple-app.selectorLabels" . | nindent 8 }}
        app.kubernetes.io/component: web
    spec:
      containers:
        - name: {{ .Chart.Name }}-web
          image: "{{ .Values.web.image.repository }}:{{ .Values.web.image.tag }}"
          imagePullPolicy: {{ .Values.web.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.web.containerPort }}
              protocol: TCP
          env:
            - name: REDIS_HOST
              # Kubernetes service DNS: <service-name>.<namespace>.svc.cluster.local
              # Helm default service name is based on release name + chart name + component
              value: {{ include "my-simple-app.component.fullname" (list . "redis") }}
            - name: FLASK_DEBUG
              value: "1"
          # livenessProbe: ...
          # readinessProbe: ...
          # resources:
          #   {{- toYaml .Values.web.resources | nindent 12 }}

templates/web-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-simple-app.component.fullname" (list . "web") }}
  labels:
    {{- include "my-simple-app.labels" . | nindent 4 }}
    app.kubernetes.io/component: web
spec:
  type: {{ .Values.web.service.type }}
  ports:
    - port: {{ .Values.web.service.port }}
      targetPort: http # Refers to the named port in the Deployment
      protocol: TCP
      name: http
  selector:
    {{- include "my-simple-app.selectorLabels" . | nindent 4 }}
    app.kubernetes.io/component: web

templates/redis-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-simple-app.component.fullname" (list . "redis") }}
  labels:
    {{- include "my-simple-app.labels" . | nindent 4 }}
    app.kubernetes.io/component: redis
spec:
  replicas: 1 # For simplicity, Redis is single replica here
  selector:
    matchLabels:
      {{- include "my-simple-app.selectorLabels" . | nindent 6 }}
      app.kubernetes.io/component: redis
  template:
    metadata:
      labels:
        {{- include "my-simple-app.selectorLabels" . | nindent 8 }}
        app.kubernetes.io/component: redis
    spec:
      containers:
        - name: {{ .Chart.Name }}-redis
          image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
          imagePullPolicy: {{ .Values.redis.image.pullPolicy }}
          ports:
            - name: redis
              containerPort: {{ .Values.redis.port }}
              protocol: TCP
          # For a real Redis, you'd configure persistence, health checks, etc.
          # resources:
          #   {{- toYaml .Values.redis.resources | nindent 12 }}

templates/redis-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-simple-app.component.fullname" (list . "redis") }}
  labels:
    {{- include "my-simple-app.labels" . | nindent 4 }}
    app.kubernetes.io/component: redis
spec:
  type: ClusterIP # Redis typically only needs to be accessible within the cluster
  ports:
    - port: {{ .Values.redis.port }}
      targetPort: redis # Refers to the named port in the Deployment
      protocol: TCP
      name: redis
  selector:
    {{- include "my-simple-app.selectorLabels" . | nindent 4 }}
    app.kubernetes.io/component: redis

To Use the Helm Chart:

  1. Build and Push your app image:
    • Go to simple-project/app/.
    • docker build -t your-dockerhub-username/simple-flask-app:latest .
    • docker push your-dockerhub-username/simple-flask-app:latest
    • Update values.yaml with your image name.
  2. Install the Helm chart:
    • Navigate to the my-simple-app/ chart directory.
    • helm install my-release . (or helm install my-release . -n my-namespace --create-namespace)
  3. Check status:
    • helm ls
    • kubectl get pods
    • kubectl get svc (to find the external IP if using LoadBalancer)
  4. Access the web app via the Service’s external IP/port.

Explanation of Helm Chart:

  • Chart.yaml: Metadata about the chart.
  • values.yaml: Default configuration values. These can be overridden during installation (helm install ... --set web.replicaCount=3).
  • templates/: Contains Kubernetes manifest templates.
    • _helpers.tpl: Defines reusable template snippets for names, labels, etc. This promotes consistency.
    • Deployments (web-deployment.yaml, redis-deployment.yaml): Define the desired state for your application pods (how many replicas, which image to use, environment variables).
    • Services (web-service.yaml, redis-service.yaml): Define how to access your applications (e.g., internally via ClusterIP for Redis, or externally via LoadBalancer or NodePort for the web app).
  • Templating: Uses Go templating (e.g., {{ .Values.web.replicaCount }}, {{ include "my-simple-app.labels" . }}) to make the manifests configurable and reusable.
  • Service Discovery: The web app’s REDIS_HOST environment variable is set to the Kubernetes service name for Redis (e.g., my-release-my-simple-app-redis). Kubernetes provides DNS for this.
  • Release Management: Helm manages “releases” (instances of your chart), allowing upgrades, rollbacks, etc.

Key Differences Highlighted by Example:

Feature Docker Compose Helm Chart
Target Single Docker host (local dev, simple deployments) Kubernetes cluster (scalable, production-grade)
App Definition docker-compose.yml Multiple YAML files in templates/, structured as a “Chart”
Configuration Primarily environment variables, command-line args values.yaml (highly configurable), command-line overrides (--set)
Service Discovery Docker Compose network, service names (e.g., redis) Kubernetes DNS, service names (e.g., my-release-my-simple-app-redis)
Networking Simple port mapping Kubernetes Services (ClusterIP, NodePort, LoadBalancer), Ingress
Scaling docker-compose scale web=3 (on one host) Kubernetes Deployment replicas (across cluster nodes)
Updates/Rollbacks Re-run docker-compose up helm upgrade, helm rollback
Complexity Simpler More complex, but more powerful for distributed systems
Packaging docker-compose.yml file Packaged chart (.tgz) that can be stored in a repository

This example shows how Docker Compose is great for getting a multi-container app running quickly locally, while Helm provides the structure and tooling needed to manage that same application (once containerized) in a more robust and scalable Kubernetes environment.

Page last modified: 2025-05-21 11:17:19