Preview Environments with Helm: Deploy Multi-Service Apps Per PR with Bunnyshell
Why Preview Environments with Helm?
Helm is the de facto package manager for Kubernetes. If your team already uses Helm charts to deploy to staging and production, you don't want to maintain a separate deployment mechanism for preview environments. You want the same charts, same values structure, same release process — just automated per pull request.
But Helm alone doesn't solve the orchestration problem. You still need something to:
- Trigger a Helm install when a PR is opened
- Inject dynamic values (hostnames, image tags, connection strings) per environment
- Manage the full lifecycle — install, upgrade on push, uninstall on merge
- Coordinate multiple releases (your app chart + database chart + cache chart) as a single environment
That's where Bunnyshell comes in. Bunnyshell's kind: Helm component type wraps Helm operations into a managed lifecycle. You define your charts, values overrides, and dependencies in bunnyshell.yaml, and Bunnyshell handles the rest — including building Docker images, injecting dynamic URLs, and cleaning up on PR close.
Bunnyshell supports Helm v3 natively. You define Helm components with deploy, destroy, start, and stop scripts that run helm install, helm uninstall, kubectl scale, etc. The runner image provides both helm and kubectl.
With Bunnyshell + Helm, you get:
- Chart reuse — Same Helm charts for dev, staging, production, and preview environments
- Values-driven configuration — Override only what differs per environment (hostnames, replicas, image tags)
- Dependency coordination — Database charts install before app charts, automatically
- Automatic cleanup —
helm uninstallruns when the PR is merged or closed - Image building — Bunnyshell builds Docker images from your Dockerfile and passes the image reference to Helm values
How Bunnyshell Uses Helm Charts
When you define a kind: Helm component in bunnyshell.yaml, Bunnyshell:
- Spins up a runner pod with the specified
runnerImage(typicallydtzar/helm-kubectl) - Clones your Git repository (if the chart is in Git)
- Executes the
deployscript — yourhelm upgrade --installcommands with values - Captures exported variables — endpoints, hostnames, or any output you define
- On environment delete, executes the
destroyscript —helm uninstall
The runner has access to helm, kubectl, and the Kubernetes namespace for the environment. Bunnyshell provides a post-renderer that adds tracking labels to all Kubernetes resources so it can show logs, resource status, and manage lifecycle.
Component Lifecycle
1Environment Deploy
2 └── Helm Component
3 ├── Clone git repo (if chart is in git)
4 ├── Generate values file from bunnyshell.yaml interpolation
5 ├── helm upgrade --install --namespace {{ env.k8s.namespace }}
6 │ --post-renderer /bns/helpers/helm/bns_post_renderer
7 └── Export variables (endpoints, etc.)
8
9Environment Delete
10 └── Helm Component
11 └── helm uninstall <release-name> --namespace {{ env.k8s.namespace }}Always include --post-renderer /bns/helpers/helm/bns_post_renderer in your helm upgrade --install commands. Without it, Bunnyshell can't track the deployed resources — you won't see logs, pod status, or resource details in the UI.
Prerequisites
Before setting up Helm-based preview environments:
- A Bunnyshell account with a connected Kubernetes cluster — sign up free
- Helm 3 knowledge (chart structure, values, releases)
- Helm charts — either from a public repository (Bitnami, etc.) or custom charts in your Git repo
- A Dockerfile for your application (Bunnyshell builds the image and passes it to Helm)
Helm Runner Image
Bunnyshell Helm components use a Docker image as the runner environment. The standard choice is:
runnerImage: 'dtzar/helm-kubectl:3.14'This image includes both helm (v3.14) and kubectl, pre-configured to talk to the environment's Kubernetes namespace. You can use any image that has helm and kubectl installed.
Approach A: Public Chart Repositories
The fastest way to add databases, caches, and other infrastructure to your preview environments is using public Helm charts. Bitnami maintains production-quality charts for PostgreSQL, MySQL, Redis, MongoDB, RabbitMQ, and dozens more.
Example: PostgreSQL via Bitnami Chart
1kind: Environment
2name: helm-preview
3type: primary
4
5environmentVariables:
6 DB_PASSWORD: SECRET["preview-db-password"]
7 DB_DATABASE: appdb
8 DB_USERNAME: appuser
9
10components:
11 # ── PostgreSQL via Bitnami Helm Chart ──
12 - kind: Helm
13 name: postgresql
14 runnerImage: 'dtzar/helm-kubectl:3.14'
15 deploy:
16 - |
17 cat << EOF > pg_values.yaml
18 global:
19 storageClass: bns-network-sc
20 auth:
21 postgresPassword: {{ env.vars.DB_PASSWORD }}
22 database: {{ env.vars.DB_DATABASE }}
23 username: {{ env.vars.DB_USERNAME }}
24 password: {{ env.vars.DB_PASSWORD }}
25 primary:
26 persistence:
27 size: 1Gi
28 resources:
29 requests:
30 memory: 128Mi
31 cpu: 100m
32 limits:
33 memory: 256Mi
34 cpu: 250m
35 EOF
36 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
37 - 'helm repo update'
38 - 'helm upgrade --install postgresql bitnami/postgresql
39 --version 14.0.5
40 --namespace {{ env.k8s.namespace }}
41 --post-renderer /bns/helpers/helm/bns_post_renderer
42 -f pg_values.yaml
43 --wait --timeout 5m'
44 - |
45 PG_HOST="postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
46 PG_PORT="5432"
47 destroy:
48 - 'helm uninstall postgresql --namespace {{ env.k8s.namespace }}'
49 start:
50 - 'kubectl scale statefulset postgresql --replicas=1
51 --namespace {{ env.k8s.namespace }}'
52 stop:
53 - 'kubectl scale statefulset postgresql --replicas=0
54 --namespace {{ env.k8s.namespace }}'
55 exportVariables:
56 - PG_HOST
57 - PG_PORT
58
59 # ── Redis via Bitnami Helm Chart ──
60 - kind: Helm
61 name: redis
62 runnerImage: 'dtzar/helm-kubectl:3.14'
63 deploy:
64 - |
65 cat << EOF > redis_values.yaml
66 global:
67 storageClass: bns-network-sc
68 architecture: standalone
69 auth:
70 enabled: false
71 master:
72 persistence:
73 size: 512Mi
74 resources:
75 requests:
76 memory: 64Mi
77 cpu: 50m
78 limits:
79 memory: 128Mi
80 cpu: 100m
81 EOF
82 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
83 - 'helm upgrade --install redis bitnami/redis
84 --version 18.6.1
85 --namespace {{ env.k8s.namespace }}
86 --post-renderer /bns/helpers/helm/bns_post_renderer
87 -f redis_values.yaml
88 --wait --timeout 5m'
89 - |
90 REDIS_HOST="redis-master.{{ env.k8s.namespace }}.svc.cluster.local"
91 REDIS_PORT="6379"
92 destroy:
93 - 'helm uninstall redis --namespace {{ env.k8s.namespace }}'
94 start:
95 - 'kubectl scale statefulset redis-master --replicas=1
96 --namespace {{ env.k8s.namespace }}'
97 stop:
98 - 'kubectl scale statefulset redis-master --replicas=0
99 --namespace {{ env.k8s.namespace }}'
100 exportVariables:
101 - REDIS_HOST
102 - REDIS_PORTAlways pin chart versions (e.g., --version 14.0.5) in your Bunnyshell configuration. Unpinned versions will install the latest chart on each deploy, which can introduce breaking changes in preview environments.
Key Details for Public Charts
- Storage class: Use
bns-network-sc(Bunnyshell's default) or your cluster's storage class for persistent volumes - Wait flag:
--wait --timeout 5mensures the Helm component reports success only after all pods are ready - Service DNS: The in-cluster DNS name follows the pattern
<release-name>.<namespace>.svc.cluster.local - Resource limits: Set modest limits for preview environments — you don't need production-level resources for PR reviews
Approach B: Custom Helm Charts from Git
For your application code, you'll typically have custom Helm charts stored in the same Git repository. Bunnyshell clones the repo and runs helm install against the local chart path.
Chart Structure
1your-repo/
2├── src/ # Application source code
3├── Dockerfile
4└── helm/
5 └── app/
6 ├── Chart.yaml
7 ├── values.yaml
8 └── templates/
9 ├── _helpers.tpl
10 ├── deployment.yaml
11 ├── service.yaml
12 ├── ingress.yaml
13 └── hpa.yamlChart.yaml
1apiVersion: v2
2name: my-app
3description: A Helm chart for the application
4type: application
5version: 0.1.0
6appVersion: "1.0.0"values.yaml
1replicaCount: 1
2
3image:
4 repository: ""
5 tag: "latest"
6 pullPolicy: IfNotPresent
7
8service:
9 type: ClusterIP
10 port: 3000
11
12ingress:
13 enabled: true
14 className: bns-nginx
15 host: ""
16 tls: true
17
18env:
19 APP_ENV: production
20 DATABASE_URL: ""
21 REDIS_URL: ""
22 S3_BUCKET: ""
23
24resources:
25 requests:
26 memory: 128Mi
27 cpu: 100m
28 limits:
29 memory: 512Mi
30 cpu: 500m
31
32autoscaling:
33 enabled: false
34 minReplicas: 1
35 maxReplicas: 3
36 targetCPUUtilization: 80Deployment Template
1# helm/app/templates/deployment.yaml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: {{ include "my-app.fullname" . }}
6 labels:
7 {{- include "my-app.labels" . | nindent 4 }}
8spec:
9 replicas: {{ .Values.replicaCount }}
10 selector:
11 matchLabels:
12 {{- include "my-app.selectorLabels" . | nindent 6 }}
13 template:
14 metadata:
15 labels:
16 {{- include "my-app.selectorLabels" . | nindent 8 }}
17 spec:
18 containers:
19 - name: {{ .Chart.Name }}
20 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
21 imagePullPolicy: {{ .Values.image.pullPolicy }}
22 ports:
23 - name: http
24 containerPort: {{ .Values.service.port }}
25 env:
26 {{- range $key, $value := .Values.env }}
27 - name: {{ $key }}
28 value: {{ $value | quote }}
29 {{- end }}
30 resources:
31 {{- toYaml .Values.resources | nindent 12 }}
32 livenessProbe:
33 httpGet:
34 path: /health
35 port: http
36 initialDelaySeconds: 15
37 periodSeconds: 10
38 readinessProbe:
39 httpGet:
40 path: /health
41 port: http
42 initialDelaySeconds: 5
43 periodSeconds: 5Bunnyshell Configuration with Custom Chart
1components:
2 # ── Build Docker Image ──
3 - kind: DockerImage
4 name: app-image
5 context: /
6 dockerfile: Dockerfile
7 gitRepo: 'https://github.com/your-org/your-repo.git'
8 gitBranch: main
9 gitApplicationPath: /
10
11 # ── Deploy App via Custom Helm Chart ──
12 - kind: Helm
13 name: app
14 runnerImage: 'dtzar/helm-kubectl:3.14'
15 deploy:
16 - |
17 cat << EOF > app_values.yaml
18 replicaCount: 1
19 image:
20 repository: {{ components.app-image.image }}
21 tag: latest
22 pullPolicy: Always
23 service:
24 type: ClusterIP
25 port: 3000
26 ingress:
27 enabled: true
28 className: bns-nginx
29 host: app-{{ env.base_domain }}
30 tls: true
31 env:
32 APP_ENV: production
33 APP_URL: "https://app-{{ env.base_domain }}"
34 DATABASE_URL: "postgres://{{ env.vars.DB_USERNAME }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgresql.exported.PG_HOST }}:{{ components.postgresql.exported.PG_PORT }}/{{ env.vars.DB_DATABASE }}"
35 REDIS_URL: "redis://{{ components.redis.exported.REDIS_HOST }}:{{ components.redis.exported.REDIS_PORT }}"
36 resources:
37 requests:
38 memory: 128Mi
39 cpu: 100m
40 limits:
41 memory: 512Mi
42 cpu: 500m
43 EOF
44 - 'helm upgrade --install app-{{ env.unique }}
45 ./helm/app
46 --namespace {{ env.k8s.namespace }}
47 --post-renderer /bns/helpers/helm/bns_post_renderer
48 -f app_values.yaml
49 --wait --timeout 5m'
50 destroy:
51 - 'helm uninstall app-{{ env.unique }}
52 --namespace {{ env.k8s.namespace }}'
53 start:
54 - 'helm upgrade app-{{ env.unique }}
55 ./helm/app
56 --namespace {{ env.k8s.namespace }}
57 --post-renderer /bns/helpers/helm/bns_post_renderer
58 --reuse-values --set replicaCount=1'
59 stop:
60 - 'helm upgrade app-{{ env.unique }}
61 ./helm/app
62 --namespace {{ env.k8s.namespace }}
63 --post-renderer /bns/helpers/helm/bns_post_renderer
64 --reuse-values --set replicaCount=0'
65 gitRepo: 'https://github.com/your-org/your-repo.git'
66 gitBranch: main
67 gitApplicationPath: /helm/app
68 dependsOn:
69 - app-image
70 - postgresql
71 - redis
72 hosts:
73 - hostname: 'app-{{ env.base_domain }}'
74 path: /
75 servicePort: 3000The {{ components.app-image.image }} interpolation resolves to the full Docker image reference (registry/repo:tag) that Bunnyshell built from your Dockerfile. This is how built images flow into Helm values without any manual image tagging.
Release Naming
Notice the release name uses {{ env.unique }}:
'helm upgrade --install app-{{ env.unique }} ./helm/app ...'This ensures each preview environment gets a unique Helm release name. Without it, two preview environments would try to manage the same release, causing conflicts. The pattern app-{{ env.unique }} produces names like app-env-abc123, app-env-def456, etc.
Approach C: Umbrella Charts for Multi-Service Apps
For applications with multiple microservices, an umbrella chart bundles everything into a single Helm release. This simplifies the Bunnyshell configuration — one Helm component instead of many.
Umbrella Chart Structure
1helm/
2└── umbrella/
3 ├── Chart.yaml
4 ├── values.yaml
5 └── charts/
6 ├── api/
7 │ ├── Chart.yaml
8 │ ├── values.yaml
9 │ └── templates/
10 │ ├── deployment.yaml
11 │ ├── service.yaml
12 │ └── ingress.yaml
13 ├── frontend/
14 │ ├── Chart.yaml
15 │ ├── values.yaml
16 │ └── templates/
17 │ ├── deployment.yaml
18 │ ├── service.yaml
19 │ └── ingress.yaml
20 └── worker/
21 ├── Chart.yaml
22 ├── values.yaml
23 └── templates/
24 └── deployment.yamlUmbrella Chart.yaml
1apiVersion: v2
2name: my-platform
3description: Umbrella chart for the full platform
4type: application
5version: 0.1.0
6
7dependencies:
8 - name: api
9 version: "0.1.0"
10 condition: api.enabled
11 - name: frontend
12 version: "0.1.0"
13 condition: frontend.enabled
14 - name: worker
15 version: "0.1.0"
16 condition: worker.enabledUmbrella values.yaml
1# Global values shared across all subcharts
2global:
3 domain: ""
4 env: production
5
6api:
7 enabled: true
8 replicaCount: 1
9 image:
10 repository: ""
11 tag: latest
12 service:
13 port: 3000
14 ingress:
15 enabled: true
16 host: ""
17 env:
18 DATABASE_URL: ""
19 REDIS_URL: ""
20
21frontend:
22 enabled: true
23 replicaCount: 1
24 image:
25 repository: ""
26 tag: latest
27 service:
28 port: 8080
29 ingress:
30 enabled: true
31 host: ""
32 env:
33 API_URL: ""
34
35worker:
36 enabled: true
37 replicaCount: 1
38 image:
39 repository: ""
40 tag: latest
41 env:
42 DATABASE_URL: ""
43 REDIS_URL: ""
44 QUEUE_NAME: "default"Bunnyshell Configuration with Umbrella Chart
1kind: Environment
2name: helm-umbrella-preview
3type: primary
4
5environmentVariables:
6 DB_PASSWORD: SECRET["preview-db-password"]
7 DB_DATABASE: appdb
8 DB_USERNAME: appuser
9
10components:
11 # ── Build Images ──
12 - kind: DockerImage
13 name: api-image
14 context: /services/api
15 dockerfile: services/api/Dockerfile
16 gitRepo: 'https://github.com/your-org/your-repo.git'
17 gitBranch: main
18 gitApplicationPath: /services/api
19
20 - kind: DockerImage
21 name: frontend-image
22 context: /services/frontend
23 dockerfile: services/frontend/Dockerfile
24 gitRepo: 'https://github.com/your-org/your-repo.git'
25 gitBranch: main
26 gitApplicationPath: /services/frontend
27
28 - kind: DockerImage
29 name: worker-image
30 context: /services/worker
31 dockerfile: services/worker/Dockerfile
32 gitRepo: 'https://github.com/your-org/your-repo.git'
33 gitBranch: main
34 gitApplicationPath: /services/worker
35
36 # ── PostgreSQL (Bitnami) ──
37 - kind: Helm
38 name: postgresql
39 runnerImage: 'dtzar/helm-kubectl:3.14'
40 deploy:
41 - |
42 cat << EOF > pg_values.yaml
43 global:
44 storageClass: bns-network-sc
45 auth:
46 postgresPassword: {{ env.vars.DB_PASSWORD }}
47 database: {{ env.vars.DB_DATABASE }}
48 username: {{ env.vars.DB_USERNAME }}
49 password: {{ env.vars.DB_PASSWORD }}
50 primary:
51 persistence:
52 size: 1Gi
53 EOF
54 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
55 - 'helm upgrade --install postgresql bitnami/postgresql
56 --version 14.0.5
57 --namespace {{ env.k8s.namespace }}
58 --post-renderer /bns/helpers/helm/bns_post_renderer
59 -f pg_values.yaml
60 --wait --timeout 5m'
61 - |
62 PG_HOST="postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
63 destroy:
64 - 'helm uninstall postgresql --namespace {{ env.k8s.namespace }}'
65 start:
66 - 'kubectl scale statefulset postgresql --replicas=1
67 --namespace {{ env.k8s.namespace }}'
68 stop:
69 - 'kubectl scale statefulset postgresql --replicas=0
70 --namespace {{ env.k8s.namespace }}'
71 exportVariables:
72 - PG_HOST
73
74 # ── Redis (Bitnami) ──
75 - kind: Helm
76 name: redis
77 runnerImage: 'dtzar/helm-kubectl:3.14'
78 deploy:
79 - |
80 cat << EOF > redis_values.yaml
81 global:
82 storageClass: bns-network-sc
83 architecture: standalone
84 auth:
85 enabled: false
86 master:
87 persistence:
88 size: 512Mi
89 EOF
90 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
91 - 'helm upgrade --install redis bitnami/redis
92 --version 18.6.1
93 --namespace {{ env.k8s.namespace }}
94 --post-renderer /bns/helpers/helm/bns_post_renderer
95 -f redis_values.yaml
96 --wait --timeout 5m'
97 - |
98 REDIS_HOST="redis-master.{{ env.k8s.namespace }}.svc.cluster.local"
99 destroy:
100 - 'helm uninstall redis --namespace {{ env.k8s.namespace }}'
101 start:
102 - 'kubectl scale statefulset redis-master --replicas=1
103 --namespace {{ env.k8s.namespace }}'
104 stop:
105 - 'kubectl scale statefulset redis-master --replicas=0
106 --namespace {{ env.k8s.namespace }}'
107 exportVariables:
108 - REDIS_HOST
109
110 # ── Platform Umbrella Chart ──
111 - kind: Helm
112 name: platform
113 runnerImage: 'dtzar/helm-kubectl:3.14'
114 deploy:
115 - |
116 cat << EOF > platform_values.yaml
117 global:
118 domain: {{ env.base_domain }}
119 env: preview
120
121 api:
122 enabled: true
123 replicaCount: 1
124 image:
125 repository: {{ components.api-image.image }}
126 tag: latest
127 pullPolicy: Always
128 service:
129 port: 3000
130 ingress:
131 enabled: true
132 host: api-{{ env.base_domain }}
133 env:
134 DATABASE_URL: "postgres://{{ env.vars.DB_USERNAME }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgresql.exported.PG_HOST }}:5432/{{ env.vars.DB_DATABASE }}"
135 REDIS_URL: "redis://{{ components.redis.exported.REDIS_HOST }}:6379"
136
137 frontend:
138 enabled: true
139 replicaCount: 1
140 image:
141 repository: {{ components.frontend-image.image }}
142 tag: latest
143 pullPolicy: Always
144 service:
145 port: 8080
146 ingress:
147 enabled: true
148 host: app-{{ env.base_domain }}
149 env:
150 API_URL: "https://api-{{ env.base_domain }}"
151
152 worker:
153 enabled: true
154 replicaCount: 1
155 image:
156 repository: {{ components.worker-image.image }}
157 tag: latest
158 pullPolicy: Always
159 env:
160 DATABASE_URL: "postgres://{{ env.vars.DB_USERNAME }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgresql.exported.PG_HOST }}:5432/{{ env.vars.DB_DATABASE }}"
161 REDIS_URL: "redis://{{ components.redis.exported.REDIS_HOST }}:6379"
162 QUEUE_NAME: "default"
163 EOF
164 - 'helm dependency update ./helm/umbrella'
165 - 'helm upgrade --install platform-{{ env.unique }}
166 ./helm/umbrella
167 --namespace {{ env.k8s.namespace }}
168 --post-renderer /bns/helpers/helm/bns_post_renderer
169 -f platform_values.yaml
170 --wait --timeout 10m'
171 destroy:
172 - 'helm uninstall platform-{{ env.unique }}
173 --namespace {{ env.k8s.namespace }}'
174 start:
175 - 'helm upgrade platform-{{ env.unique }}
176 ./helm/umbrella
177 --namespace {{ env.k8s.namespace }}
178 --post-renderer /bns/helpers/helm/bns_post_renderer
179 --reuse-values
180 --set api.replicaCount=1
181 --set frontend.replicaCount=1
182 --set worker.replicaCount=1'
183 stop:
184 - 'helm upgrade platform-{{ env.unique }}
185 ./helm/umbrella
186 --namespace {{ env.k8s.namespace }}
187 --post-renderer /bns/helpers/helm/bns_post_renderer
188 --reuse-values
189 --set api.replicaCount=0
190 --set frontend.replicaCount=0
191 --set worker.replicaCount=0'
192 gitRepo: 'https://github.com/your-org/your-repo.git'
193 gitBranch: main
194 gitApplicationPath: /helm/umbrella
195 dependsOn:
196 - api-image
197 - frontend-image
198 - worker-image
199 - postgresql
200 - redis
201 hosts:
202 - hostname: 'api-{{ env.base_domain }}'
203 path: /
204 servicePort: 3000
205 - hostname: 'app-{{ env.base_domain }}'
206 path: /
207 servicePort: 8080Umbrella charts are ideal when your services are tightly coupled and always deployed together. If services are independently deployable (different release cadences), use separate Helm components instead — Bunnyshell will coordinate them through the dependency graph.
Values Overrides in bunnyshell.yaml
The pattern for injecting Bunnyshell-specific values into Helm charts is consistent across all approaches:
1. Generate a Values File with Interpolation
1deploy:
2 - |
3 cat << EOF > values.yaml
4 image:
5 repository: {{ components.app-image.image }}
6 ingress:
7 host: app-{{ env.base_domain }}
8 env:
9 DATABASE_URL: "postgres://{{ env.vars.DB_USERNAME }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgresql.exported.PG_HOST }}:5432/{{ env.vars.DB_DATABASE }}"
10 EOF
11 - 'helm upgrade --install ... -f values.yaml'2. Use --set for Simple Overrides
1deploy:
2 - 'helm upgrade --install myapp ./helm/app
3 --set image.repository={{ components.app-image.image }}
4 --set ingress.host=app-{{ env.base_domain }}'3. Combine Multiple Values Files
1deploy:
2 - 'helm upgrade --install myapp ./helm/app
3 -f ./helm/app/values.yaml
4 -f ./helm/app/values-preview.yaml
5 -f generated_values.yaml'Helm merges values files left to right — later files override earlier ones. A common pattern is:
values.yaml— defaults for all environmentsvalues-preview.yaml— preview-specific overrides (lower replicas, debug enabled)generated_values.yaml— Bunnyshell-generated dynamic values (images, URLs)
When using --set with values that contain special characters (dots, commas, slashes), use --set-string instead. Helm's --set parser interprets dots as nested keys, which can produce unexpected results with URLs or connection strings.
Chart Dependencies and Subcharts
Helm's dependency management lets you declare infrastructure requirements in Chart.yaml:
1# helm/app/Chart.yaml
2apiVersion: v2
3name: my-app
4version: 0.1.0
5
6dependencies:
7 - name: postgresql
8 version: "14.0.5"
9 repository: "https://charts.bitnami.com/bitnami"
10 condition: postgresql.enabled
11 - name: redis
12 version: "18.6.1"
13 repository: "https://charts.bitnami.com/bitnami"
14 condition: redis.enabledWhen to Use Chart Dependencies vs. Separate Bunnyshell Components
| Use chart dependencies when... | Use separate Bunnyshell components when... |
|---|---|
| Database is always co-deployed with the app | Database is shared across multiple apps |
You want a single helm install for everything | You need independent lifecycle control |
| Chart versions are locked and rarely change | Different teams manage different services |
| Simple single-service applications | Complex multi-service architectures |
For preview environments, separate Bunnyshell components are usually better because:
- You get individual deploy/destroy status per component
- Failures are isolated — a database issue doesn't block app image build
- Exported variables create clear data flow between components
- Start/stop can be granular (stop the app but keep the database)
If you do use chart dependencies, make sure to run helm dependency update before install:
1deploy:
2 - 'helm dependency update ./helm/app'
3 - 'helm upgrade --install ...'Helm Hooks and Post-Install Jobs
Helm hooks let you run tasks at specific points in the release lifecycle — database migrations, cache warming, seed data loading.
Migration Job Hook
1# helm/app/templates/migration-job.yaml
2apiVersion: batch/v1
3kind: Job
4metadata:
5 name: {{ include "my-app.fullname" . }}-migrate
6 annotations:
7 "helm.sh/hook": post-install,post-upgrade
8 "helm.sh/hook-weight": "0"
9 "helm.sh/hook-delete-policy": hook-succeeded
10spec:
11 backoffLimit: 3
12 template:
13 spec:
14 restartPolicy: Never
15 containers:
16 - name: migrate
17 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
18 command: ["./migrate.sh"]
19 env:
20 {{- range $key, $value := .Values.env }}
21 - name: {{ $key }}
22 value: {{ $value | quote }}
23 {{- end }}This Job runs after helm install (post-install) and after helm upgrade (post-upgrade). It uses the same Docker image and environment variables as the main application, ensuring migrations run against the correct database.
Seed Data Hook (Preview Only)
For preview environments, you might want to seed demo data:
1# helm/app/templates/seed-job.yaml
2{{- if eq .Values.global.env "preview" }}
3apiVersion: batch/v1
4kind: Job
5metadata:
6 name: {{ include "my-app.fullname" . }}-seed
7 annotations:
8 "helm.sh/hook": post-install
9 "helm.sh/hook-weight": "1"
10 "helm.sh/hook-delete-policy": hook-succeeded
11spec:
12 backoffLimit: 1
13 template:
14 spec:
15 restartPolicy: Never
16 containers:
17 - name: seed
18 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
19 command: ["./seed.sh"]
20 env:
21 {{- range $key, $value := .Values.env }}
22 - name: {{ $key }}
23 value: {{ $value | quote }}
24 {{- end }}
25{{- end }}The {{- if eq .Values.global.env "preview" }} conditional ensures this Job only runs in preview environments. Set global.env: preview in your Bunnyshell values override to activate it.
Helm hooks with hook-delete-policy: hook-succeeded automatically clean up completed Jobs, keeping your namespace tidy. Use hook-succeeded,hook-failed to clean up regardless of outcome (useful for preview environments where you don't need to debug failed seed jobs).
Alternative: Run Migrations from Deploy Script
If you prefer running migrations outside of Helm hooks, use the Bunnyshell deploy script:
1deploy:
2 - 'helm upgrade --install ... --wait --timeout 5m'
3 - 'kubectl exec -n {{ env.k8s.namespace }}
4 deploy/app-{{ env.unique }}-my-app
5 -- ./migrate.sh'This runs after the Helm release is healthy, giving you more control and better error visibility in the Bunnyshell logs.
Enabling Preview Environments
Once your primary environment is deployed and running with Helm components:
- Ensure your environment status shows Running or Stopped
- Go to Settings in the environment view
- Toggle "Create ephemeral environments on pull request" to ON
- Toggle "Destroy environment after merge or close pull request" to ON
- Select the Kubernetes cluster for ephemeral environments
The PR workflow:
- Developer opens a PR
- Bunnyshell receives the webhook, clones the environment config
DockerImagecomponents build new images from the PR branchHelmcomponents deploy charts with the new images and unique release names- Bunnyshell posts a comment on the PR with live URLs
- Developer pushes more commits — Bunnyshell redeploys with updated code
- PR is merged — Bunnyshell runs
helm uninstallfor all components
No GitHub Actions. No GitLab CI. Bunnyshell manages the webhooks and the full lifecycle.
How Image Tags Work in Preview Environments
When a preview environment is created from a PR:
DockerImagecomponents build from the PR's branch (not main)- The built image gets a unique tag tied to the environment
{{ components.app-image.image }}resolves to the newly built image- Helm values automatically reference the correct image for the PR
This means every PR gets images built from its own code, injected into Helm charts that deploy to an isolated namespace. Full isolation, zero configuration per PR.
Debugging Helm Releases in Preview Envs
When things go wrong in a preview environment, here's how to diagnose:
Check Release Status
1# List all releases in the environment namespace
2bns exec HELM_COMPONENT_ID -- helm list --namespace NAMESPACE
3
4# Get detailed release info
5bns exec HELM_COMPONENT_ID -- helm status RELEASE_NAME --namespace NAMESPACE
6
7# See what values were applied
8bns exec HELM_COMPONENT_ID -- helm get values RELEASE_NAME --namespace NAMESPACE
9
10# View the rendered manifests
11bns exec HELM_COMPONENT_ID -- helm get manifest RELEASE_NAME --namespace NAMESPACECheck Kubernetes Resources
1# Pods and their status
2bns exec HELM_COMPONENT_ID -- kubectl get pods --namespace NAMESPACE
3
4# Describe a failing pod
5bns exec HELM_COMPONENT_ID -- kubectl describe pod POD_NAME --namespace NAMESPACE
6
7# Pod logs
8bns exec HELM_COMPONENT_ID -- kubectl logs POD_NAME --namespace NAMESPACE
9
10# Events (sorted by time)
11bns exec HELM_COMPONENT_ID -- kubectl get events --sort-by='.lastTimestamp' --namespace NAMESPACEDry Run Before Deploy
To test your Helm values without actually deploying, add a dry-run step:
1deploy:
2 - 'helm upgrade --install app-{{ env.unique }}
3 ./helm/app
4 --namespace {{ env.k8s.namespace }}
5 -f app_values.yaml
6 --dry-run --debug'
7 - 'helm upgrade --install app-{{ env.unique }}
8 ./helm/app
9 --namespace {{ env.k8s.namespace }}
10 --post-renderer /bns/helpers/helm/bns_post_renderer
11 -f app_values.yaml
12 --wait --timeout 5m'The first command renders templates and validates without applying. The second does the actual install. If the dry run fails, the deploy stops before creating any resources.
The --dry-run command should NOT include the --post-renderer flag. The post-renderer modifies resources for Bunnyshell tracking and should only run during actual deployment.
Troubleshooting
| Issue | Solution |
|---|---|
| "release not found" on upgrade | The release name changed between deploys. Ensure you use {{ env.unique }} consistently in release names across deploy, destroy, start, and stop scripts. |
| Pods stuck in ImagePullBackOff | The Docker image wasn't built or the registry is unreachable. Check that the DockerImage component completed successfully and {{ components.app-image.image }} resolves to a valid reference. |
| "timed out waiting for the condition" | Pods aren't becoming ready within the --timeout period. Check readiness probes, resource limits, and startup time. Increase timeout if the app needs more time. |
| Helm values not applied | Values files are merged left to right. Check the order of -f flags. Later files override earlier ones. Use helm get values RELEASE to verify what was actually applied. |
| "cannot re-use a name that is still in use" | A previous deploy left a failed release. Run helm uninstall RELEASE --namespace NS manually, then redeploy. |
| Ingress not working / 404 | Check that ingress.className matches your cluster's ingress controller (usually bns-nginx for Bunnyshell). Verify the host value matches the hostname in hosts on the Bunnyshell component. |
| PVC stuck in Pending | Storage class doesn't exist or has no available provisioner. Use bns-network-sc for Bunnyshell clusters or check kubectl get sc. |
| Hook job fails | Check job logs: kubectl logs job/RELEASE-migrate -n NAMESPACE. Common issues: migration syntax errors, database not reachable (dependency not ready), or incorrect DATABASE_URL. |
| Resources not tracked in Bunnyshell UI | Missing --post-renderer /bns/helpers/helm/bns_post_renderer. Add it to all helm upgrade --install commands. |
| "Error: UPGRADE FAILED: another operation is in progress" | A previous Helm operation didn't finish cleanly. Run helm rollback RELEASE 0 --namespace NS to reset, then retry. |
What's Next?
- Add Prometheus monitoring — Deploy
kube-prometheus-stackas a Helm component for per-environment metrics - Use OCI registries for charts — Push your custom charts to an OCI-compatible registry (ECR, GCR, ACR) for versioned chart management
- Implement chart testing — Add
helm lintandhelm templateto your CI pipeline to catch errors before preview deployment - Add Ingress annotations — Configure rate limiting, CORS, or authentication per preview environment using Helm values
- Combine with Terraform — Use Terraform components for managed infrastructure (RDS, ElastiCache) alongside Helm for application deployment
Related Resources
- Bunnyshell Helm Component Docs
- Helm Official Documentation
- Bitnami Helm Charts
- Bunnyshell CLI Reference
- Ephemeral Environments — Learn more about the concept
- Preview Environments with Terraform — Using Terraform for managed cloud infrastructure
- All Guides — More technical guides
Ship faster starting today.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.