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:
- Development Phase: You might use Docker Compose for local development because of its simplicity and speed.
- 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.
- Tools like Kompose can help convert
- 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).
- A Flask app (
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:
- Navigate to the
simple-project/
directory. - Run
docker-compose up --build
- Access the web app at
http://localhost:5000
.
Explanation of Docker Compose:
- Defines two
services
:web
andredis
. web
service is built from the localapp/Dockerfile
.ports
map host ports to container ports.volumes
mount the localapp
directory for development, so changes are reflected live.environment
setsREDIS_HOST
toredis
, which is the service name Docker Compose uses for DNS resolution within its internal network.depends_on
ensuresredis
starts beforeweb
.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:
- 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.
- Go to
- Install the Helm chart:
- Navigate to the
my-simple-app/
chart directory. helm install my-release .
(orhelm install my-release . -n my-namespace --create-namespace
)
- Navigate to the
- Check status:
helm ls
kubectl get pods
kubectl get svc
(to find the external IP if using LoadBalancer)
- 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 viaClusterIP
for Redis, or externally viaLoadBalancer
orNodePort
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