Preview Environments for Angular: Automated Per-PR Deployments with Bunnyshell
Why Preview Environments for Angular?
Angular apps are deceptively complex to deploy. A single-page application looks simple — build it, serve the files — but then you hit SPA routing issues behind a reverse proxy, environment-specific API URLs baked into the production build, and Angular 17+ SSR that needs a Node server instead of static files. On a shared staging server, these problems compound.
One developer is testing the new standalone component architecture. Another pushed Angular Universal SSR changes that switch the entire deployment model. Someone updated environment.prod.ts with an API URL that only works for their feature branch. The QA team can't tell which PR broke the staging deploy because three branches landed in the last hour.
Preview environments solve this. Every pull request gets its own isolated deployment — whether that's a static SPA served by Nginx or an SSR app running on Node — in Kubernetes with production-like configuration. Reviewers click a link and see the actual running app, not just the diff.
With Bunnyshell, you get:
- Automatic deployment — A new environment spins up for every PR
- Production parity — Same Docker images, same build configuration, same API backends
- Isolation — Each PR environment is fully independent, no shared staging conflicts
- Automatic cleanup — Environments are destroyed when the PR is merged or closed
Choose Your Approach
Bunnyshell supports three ways to set up preview environments for Angular. Pick the one that fits your workflow:
| Approach | Best for | Complexity | CI/CD maintenance |
|---|---|---|---|
| Approach A: Bunnyshell UI | Teams that want the fastest setup with zero pipeline maintenance | Easiest | None — Bunnyshell manages webhooks automatically |
| Approach B: Docker Compose Import | Teams already using docker-compose.yml for local development | Easy | None — import converts to Bunnyshell config automatically |
| Approach C: Helm Charts | Teams with existing Helm infrastructure or complex K8s needs | Advanced | Optional — can use CLI or Bunnyshell UI |
All three approaches end the same way: a toggle in Bunnyshell Settings that enables automatic preview environments for every PR. No GitHub Actions, no GitLab CI pipelines to maintain — Bunnyshell adds webhooks to your Git provider and listens for PR events.
Prerequisites: Prepare Your Angular App
Regardless of which approach you choose, your Angular app needs a proper Docker setup. Angular has two deployment modes, and we'll cover both.
Understanding Angular's Deployment Modes
Angular apps can be deployed in two ways:
- Static SPA mode:
ng build --configuration productionoutputs static HTML/CSS/JS todist/your-app/browser/. Served by Nginx. No Node runtime needed in production. This is the default for most Angular apps. - SSR mode (Angular 17+ with
@angular/ssr): Runs a Node.js server that renders pages on the server. Outputs todist/your-app/server/server.mjs. Needed for SEO-critical pages, faster first contentful paint, and server-side data fetching.
Never use ng serve in production. It's a development server with no security hardening, no caching, and no compression. For production: use Nginx for static SPA, or node dist/your-app/server/server.mjs for SSR.
Option 1: Dockerfile for Static SPA (Nginx)
The most common deployment — build the Angular app and serve it with Nginx:
1# ── Stage 1: Build ──
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5
6COPY package.json package-lock.json ./
7RUN npm ci
8
9COPY . .
10
11RUN npx ng build --configuration production
12
13# ── Stage 2: Serve ──
14FROM nginx:1.25-alpine
15
16COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
17COPY /app/dist/*/browser /usr/share/nginx/html
18
19EXPOSE 8080
20
21CMD ["nginx", "-g", "daemon off;"]Create docker/nginx/default.conf:
1server {
2 listen 8080;
3 server_name _;
4 root /usr/share/nginx/html;
5 index index.html;
6
7 # SPA routing — all routes fall back to index.html
8 location / {
9 try_files $uri $uri/ /index.html;
10 }
11
12 # Cache static assets aggressively
13 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
14 expires 1y;
15 add_header Cache-Control "public, immutable";
16 }
17
18 # Security headers
19 add_header X-Frame-Options "SAMEORIGIN" always;
20 add_header X-Content-Type-Options "nosniff" always;
21
22 # Gzip compression
23 gzip on;
24 gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
25 gzip_min_length 1000;
26}The try_files $uri $uri/ /index.html; directive is critical for Angular SPA routing. Without it, refreshing any route other than / returns a 404 because Nginx looks for a file matching the URL path. This directive falls back to index.html and lets Angular's router handle the URL client-side.
Option 2: Dockerfile for SSR Mode (Node)
For Angular 17+ apps with @angular/ssr:
ng add @angular/ssrThen the Dockerfile:
1# ── Stage 1: Build ──
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5
6COPY package.json package-lock.json ./
7RUN npm ci
8
9COPY . .
10
11RUN npx ng build --configuration production
12
13# ── Stage 2: Run SSR Server ──
14FROM node:20-alpine
15
16WORKDIR /app
17
18COPY /app/dist ./dist
19COPY /app/node_modules ./node_modules
20COPY /app/package.json ./
21
22ENV PORT=4000
23
24EXPOSE 4000
25
26CMD ["node", "dist/*/server/server.mjs"]The SSR server entry point path depends on your Angular project name. If your project is called my-app, the path is dist/my-app/server/server.mjs. The wildcard dist/*/server/server.mjs works in Dockerfile CMD but may not work in all shells. Verify the exact path in your dist/ output after building.
Environment Configuration for Preview Environments
Angular's traditional environment.ts / environment.prod.ts approach bakes API URLs into the build at compile time. This is a problem for preview environments because each environment has a unique URL that isn't known until deployment.
Solution: Runtime configuration via an /app-config endpoint or window.__env.
Approach 1: Config endpoint served by Nginx
Create docker/nginx/app-config.json.template:
1{
2 "apiUrl": "${API_URL}",
3 "authUrl": "${AUTH_URL}",
4 "environment": "preview"
5}Update docker/nginx/default.conf to serve it:
1server {
2 listen 8080;
3 server_name _;
4 root /usr/share/nginx/html;
5 index index.html;
6
7 # Runtime config endpoint — values injected at container startup
8 location /app-config {
9 alias /usr/share/nginx/html/app-config.json;
10 add_header Cache-Control "no-cache";
11 default_type application/json;
12 }
13
14 location / {
15 try_files $uri $uri/ /index.html;
16 }
17
18 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
19 expires 1y;
20 add_header Cache-Control "public, immutable";
21 }
22
23 gzip on;
24 gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
25 gzip_min_length 1000;
26}Update the Dockerfile to use envsubst at startup:
1FROM nginx:1.25-alpine
2
3COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
4COPY docker/nginx/app-config.json.template /etc/nginx/templates/app-config.json.template
5COPY /app/dist/*/browser /usr/share/nginx/html
6
7EXPOSE 8080
8
9CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/templates/app-config.json.template > /usr/share/nginx/html/app-config.json && nginx -g 'daemon off;'"]Then in your Angular app, create a config service:
1// src/app/config.service.ts
2import { Injectable } from '@angular/core';
3import { HttpClient } from '@angular/common/http';
4import { firstValueFrom } from 'rxjs';
5
6export interface AppConfig {
7 apiUrl: string;
8 authUrl: string;
9 environment: string;
10}
11
12@Injectable({ providedIn: 'root' })
13export class ConfigService {
14 private config!: AppConfig;
15
16 constructor(private http: HttpClient) {}
17
18 async load(): Promise<void> {
19 this.config = await firstValueFrom(
20 this.http.get<AppConfig>('/app-config')
21 );
22 }
23
24 get apiUrl(): string {
25 return this.config.apiUrl;
26 }
27
28 get authUrl(): string {
29 return this.config.authUrl;
30 }
31}Initialize it in app.config.ts:
1// src/app/app.config.ts
2import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
3import { provideHttpClient } from '@angular/common/http';
4import { ConfigService } from './config.service';
5
6function initializeApp(configService: ConfigService) {
7 return () => configService.load();
8}
9
10export const appConfig: ApplicationConfig = {
11 providers: [
12 provideHttpClient(),
13 {
14 provide: APP_INITIALIZER,
15 useFactory: initializeApp,
16 deps: [ConfigService],
17 multi: true,
18 },
19 ],
20};Approach 2: window.__env injection
A simpler alternative — inject configuration via a script tag in index.html:
Create docker/env.js.template:
1window.__env = {
2 apiUrl: "${API_URL}",
3 authUrl: "${AUTH_URL}",
4 environment: "preview"
5};Add to your Dockerfile CMD:
CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/templates/env.js.template > /usr/share/nginx/html/env.js && nginx -g 'daemon off;'"]Add the script to src/index.html (before the Angular bundle):
<script src="/env.js"></script>Then access it anywhere:
const apiUrl = (window as any).__env?.apiUrl || 'http://localhost:3000';The window.__env approach is simpler but less type-safe. The /app-config endpoint approach integrates better with Angular's dependency injection and APP_INITIALIZER pattern. Choose based on your team's preference — both work equally well in preview environments.
Angular Deployment Checklist
- Decided on deployment mode: static SPA (Nginx) or SSR (Node)
-
ng build --configuration productionused — neverng serve - Nginx config includes
try_files $uri $uri/ /index.html;for SPA routing - Runtime configuration via
/app-configendpoint orwindow.__env— not compile-timeenvironment.ts -
envsubstor equivalent used to inject env vars at container startup - Multi-stage Docker build to keep image size small
- Gzip compression enabled in Nginx for JS/CSS/JSON
- Static assets cached with immutable headers
Approach A: Bunnyshell UI — Zero CI/CD Maintenance
This is the easiest approach. You connect your repo, paste a YAML config, deploy, and flip a toggle. No CI/CD pipelines to write or maintain — Bunnyshell automatically adds webhooks to your Git provider and creates/destroys preview environments when PRs are opened/closed.
Step 1: Create a Project and Environment
- Log into Bunnyshell
- Click Create project and name it (e.g., "Angular App")
- Inside the project, click Create environment and name it (e.g., "angular-main")
Step 2: Define the Environment Configuration (SPA Mode)
Click Configuration in your environment view and paste this bunnyshell.yaml for an Angular SPA with an API backend:
1kind: Environment
2name: angular-spa-preview
3type: primary
4
5environmentVariables:
6 DB_PASSWORD: SECRET["your-db-password"]
7 JWT_SECRET: SECRET["your-jwt-secret"]
8
9components:
10 # ── Angular Frontend (Nginx) ──
11 - kind: Application
12 name: angular-app
13 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
14 gitBranch: main
15 gitApplicationPath: /
16 dockerCompose:
17 build:
18 context: .
19 dockerfile: Dockerfile
20 environment:
21 API_URL: 'https://api-{{ env.base_domain }}'
22 AUTH_URL: 'https://api-{{ env.base_domain }}/auth'
23 ports:
24 - '8080:8080'
25 dependsOn:
26 - api-backend
27 hosts:
28 - hostname: 'app-{{ env.base_domain }}'
29 path: /
30 servicePort: 8080
31
32 # ── API Backend (Node/Express example) ──
33 - kind: Application
34 name: api-backend
35 gitRepo: 'https://github.com/your-org/your-api-repo.git'
36 gitBranch: main
37 gitApplicationPath: /
38 dockerCompose:
39 build:
40 context: .
41 dockerfile: Dockerfile
42 environment:
43 PORT: '3000'
44 DATABASE_URL: 'postgresql://angular:{{ env.vars.DB_PASSWORD }}@postgres:5432/angular'
45 JWT_SECRET: '{{ env.vars.JWT_SECRET }}'
46 CORS_ORIGIN: 'https://app-{{ env.base_domain }}'
47 ports:
48 - '3000:3000'
49 dependsOn:
50 - postgres
51 hosts:
52 - hostname: 'api-{{ env.base_domain }}'
53 path: /
54 servicePort: 3000
55
56 # ── PostgreSQL Database ──
57 - kind: Database
58 name: postgres
59 dockerCompose:
60 image: 'postgres:16-alpine'
61 environment:
62 POSTGRES_DB: angular
63 POSTGRES_USER: angular
64 POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
65 ports:
66 - '5432:5432'
67
68volumes:
69 - name: postgres-data
70 mount:
71 component: postgres
72 containerPath: /var/lib/postgresql/data
73 size: 1GiKey architecture notes:
- Angular frontend + API backend — Two separate components with independent builds and deploys. The Angular app talks to the API via the
API_URLenvironment variable, injected at container startup viaenvsubst. - CORS configuration — The API backend must allow requests from the Angular app's dynamic URL. Use
CORS_ORIGIN: 'https://app-{{ env.base_domain }}'so each preview environment has the correct CORS origin. - Runtime config —
API_URLandAUTH_URLare set as environment variables on the Angular container, injected into the Nginx-served config endpoint at startup.
Replace your-org/your-angular-repo and your-org/your-api-repo with your actual repositories. Save the configuration.
Step 2 (Alternative): SSR Mode Configuration
If your Angular app uses SSR with @angular/ssr, replace the Angular frontend component:
1 # ── Angular SSR Application (Node) ──
2 - kind: Application
3 name: angular-app
4 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
5 gitBranch: main
6 gitApplicationPath: /
7 dockerCompose:
8 build:
9 context: .
10 dockerfile: Dockerfile
11 environment:
12 PORT: '4000'
13 API_URL: 'https://api-{{ env.base_domain }}'
14 AUTH_URL: 'https://api-{{ env.base_domain }}/auth'
15 ports:
16 - '4000:4000'
17 dependsOn:
18 - api-backend
19 hosts:
20 - hostname: 'app-{{ env.base_domain }}'
21 path: /
22 servicePort: 4000Step 3: Deploy
Click the Deploy button, select your Kubernetes cluster, and click Deploy Environment. Bunnyshell will:
- Build your Angular Docker image (multi-stage: Node build + Nginx serve, or Node SSR)
- Build the API backend Docker image
- Pull the PostgreSQL image
- Deploy everything into an isolated Kubernetes namespace
- Generate HTTPS URLs automatically with DNS
Monitor the deployment in the environment detail page. When status shows Running, click Endpoints to access your live Angular app.
Step 4: Enable Automatic Preview Environments
This is the magic step — no CI/CD configuration needed:
- In your environment, go to Settings
- Find the Ephemeral environments section
- 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
That's it. Bunnyshell automatically adds a webhook to your Git provider (GitHub, GitLab, or Bitbucket). From now on:
- Open a PR — Bunnyshell creates an ephemeral environment with the PR's branch
- Push to PR — The environment redeploys with the latest changes
- Bunnyshell posts a comment on the PR with a link to the live deployment
- Merge or close the PR — The ephemeral environment is automatically destroyed
The primary environment must be in Running or Stopped status before ephemeral environments can be created from it.
Approach B: Docker Compose Import
Already have a docker-compose.yml for local development? Bunnyshell can import it directly and convert it to its environment format. No manual YAML writing required.
Step 1: Add a docker-compose.yml to Your Repo
If you don't already have one, create docker-compose.yml in your repo root:
1version: '3.8'
2
3services:
4 angular-app:
5 build:
6 context: .
7 dockerfile: Dockerfile
8 environment:
9 API_URL: 'http://localhost:3000'
10 AUTH_URL: 'http://localhost:3000/auth'
11 ports:
12 - '8080:8080'
13 depends_on:
14 - api-backend
15
16 api-backend:
17 build:
18 context: ./api
19 dockerfile: Dockerfile
20 environment:
21 PORT: '3000'
22 DATABASE_URL: 'postgresql://angular:secret@postgres:5432/angular'
23 JWT_SECRET: 'dev-secret-change-me'
24 CORS_ORIGIN: 'http://localhost:8080'
25 ports:
26 - '3000:3000'
27 depends_on:
28 - postgres
29
30 postgres:
31 image: postgres:16-alpine
32 environment:
33 POSTGRES_DB: angular
34 POSTGRES_USER: angular
35 POSTGRES_PASSWORD: secret
36 volumes:
37 - postgres-data:/var/lib/postgresql/data
38 ports:
39 - '5432:5432'
40
41volumes:
42 postgres-data:Step 2: Import into Bunnyshell
- Create a Project and Environment in Bunnyshell (same as Approach A, Step 1)
- Click Define environment
- Select your Git account and repository
- Set the branch (e.g.,
main) and the path todocker-compose.yml(use/if it's in the root) - Click Continue — Bunnyshell parses and validates your Docker Compose file
Bunnyshell automatically detects all services, exposed ports, build configurations, and volumes.
Step 3: Adjust the Configuration
After import, go to Configuration in the environment view and update:
Replace hardcoded secrets with SECRET["..."] syntax:
1environmentVariables:
2 DB_PASSWORD: SECRET["your-db-password"]
3 JWT_SECRET: SECRET["your-jwt-secret"]Add dynamic URLs using Bunnyshell interpolation:
1# Angular app — runtime config
2environment:
3 API_URL: 'https://api-{{ env.base_domain }}'
4 AUTH_URL: 'https://api-{{ env.base_domain }}/auth'
5
6# API backend — CORS
7environment:
8 CORS_ORIGIN: 'https://app-{{ env.base_domain }}'Add ingress hosts if not auto-detected:
1# Angular app
2hosts:
3 - hostname: 'app-{{ env.base_domain }}'
4 path: /
5 servicePort: 8080
6
7# API backend
8hosts:
9 - hostname: 'api-{{ env.base_domain }}'
10 path: /
11 servicePort: 3000Step 4: Deploy and Enable Preview Environments
Same as Approach A — click Deploy, then go to Settings and toggle on ephemeral environments.
Best Practices for Docker Compose with Bunnyshell
- Remove local volumes —
volumes: ['.:/app']is for local dev withng serve. Remove these in Bunnyshell — the Docker image already contains the built app - Use Bunnyshell interpolation for dynamic values:
1# Local docker-compose.yml
2API_URL: http://localhost:3000
3
4# Bunnyshell environment config (after import)
5API_URL: 'https://api-{{ env.base_domain }}'- Ensure CORS matches — The API backend's
CORS_ORIGINmust match the Angular app's ingress URL. Use Bunnyshell interpolation so both resolve to the same base domain - Design for startup resilience — Kubernetes doesn't guarantee
depends_onordering. Make your API backend retry database connections on startup
Approach C: Helm Charts
For teams with existing Helm infrastructure or complex Kubernetes requirements (custom ingress, service mesh, multiple microservices). Helm gives you full control over every Kubernetes resource.
Step 1: Create a Helm Chart
Structure your Angular Helm chart in your repo:
1helm/angular/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5 ├── deployment.yaml
6 ├── service.yaml
7 └── ingress.yamlA minimal values.yaml:
1replicaCount: 1
2image:
3 repository: ""
4 tag: latest
5service:
6 port: 8080 # Nginx for SPA, 4000 for SSR
7ingress:
8 enabled: true
9 className: bns-nginx
10 host: ""
11env:
12 API_URL: ""
13 AUTH_URL: ""
14 PORT: "4000"Step 2: Define the Bunnyshell Configuration
Create a bunnyshell.yaml using Helm components:
1kind: Environment
2name: angular-helm
3type: primary
4
5environmentVariables:
6 DB_PASSWORD: SECRET["your-db-password"]
7 JWT_SECRET: SECRET["your-jwt-secret"]
8
9components:
10 # ── Docker Image Builds ──
11 - kind: DockerImage
12 name: angular-image
13 context: /
14 dockerfile: Dockerfile
15 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
16 gitBranch: main
17 gitApplicationPath: /
18
19 - kind: DockerImage
20 name: api-image
21 context: /api
22 dockerfile: api/Dockerfile
23 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
24 gitBranch: main
25 gitApplicationPath: /api
26
27 # ── PostgreSQL via Helm (Bitnami) ──
28 - kind: Helm
29 name: postgres
30 runnerImage: 'dtzar/helm-kubectl:3.8.2'
31 deploy:
32 - |
33 cat << EOF > pg_values.yaml
34 global:
35 storageClass: bns-network-sc
36 auth:
37 database: angular
38 username: angular
39 password: {{ env.vars.DB_PASSWORD }}
40 EOF
41 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
42 - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
43 --post-renderer /bns/helpers/helm/bns_post_renderer
44 -f pg_values.yaml postgres bitnami/postgresql --version 13.4.4'
45 - |
46 PG_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
47 destroy:
48 - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
49 start:
50 - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}
51 statefulset/postgres-postgresql'
52 stop:
53 - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}
54 statefulset/postgres-postgresql'
55 exportVariables:
56 - PG_HOST
57
58 # ── Angular App via Helm ──
59 - kind: Helm
60 name: angular-app
61 runnerImage: 'dtzar/helm-kubectl:3.8.2'
62 deploy:
63 - |
64 cat << EOF > angular_values.yaml
65 replicaCount: 1
66 image:
67 repository: {{ components.angular-image.image }}
68 service:
69 port: 8080
70 ingress:
71 enabled: true
72 className: bns-nginx
73 host: app-{{ env.base_domain }}
74 env:
75 API_URL: 'https://api-{{ env.base_domain }}'
76 AUTH_URL: 'https://api-{{ env.base_domain }}/auth'
77 EOF
78 - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
79 --post-renderer /bns/helpers/helm/bns_post_renderer
80 -f angular_values.yaml angular-{{ env.unique }} ./helm/angular'
81 destroy:
82 - 'helm uninstall angular-{{ env.unique }} --namespace {{ env.k8s.namespace }}'
83 start:
84 - 'helm upgrade --namespace {{ env.k8s.namespace }}
85 --post-renderer /bns/helpers/helm/bns_post_renderer
86 --reuse-values --set replicaCount=1 angular-{{ env.unique }} ./helm/angular'
87 stop:
88 - 'helm upgrade --namespace {{ env.k8s.namespace }}
89 --post-renderer /bns/helpers/helm/bns_post_renderer
90 --reuse-values --set replicaCount=0 angular-{{ env.unique }} ./helm/angular'
91 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
92 gitBranch: main
93 gitApplicationPath: /helm/angular
94
95 # ── API Backend via Helm ──
96 - kind: Helm
97 name: api-backend
98 runnerImage: 'dtzar/helm-kubectl:3.8.2'
99 deploy:
100 - |
101 cat << EOF > api_values.yaml
102 replicaCount: 1
103 image:
104 repository: {{ components.api-image.image }}
105 service:
106 port: 3000
107 ingress:
108 enabled: true
109 className: bns-nginx
110 host: api-{{ env.base_domain }}
111 env:
112 PORT: '3000'
113 DATABASE_URL: 'postgresql://angular:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.PG_HOST }}:5432/angular'
114 JWT_SECRET: '{{ env.vars.JWT_SECRET }}'
115 CORS_ORIGIN: 'https://app-{{ env.base_domain }}'
116 EOF
117 - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
118 --post-renderer /bns/helpers/helm/bns_post_renderer
119 -f api_values.yaml api-{{ env.unique }} ./helm/api'
120 destroy:
121 - 'helm uninstall api-{{ env.unique }} --namespace {{ env.k8s.namespace }}'
122 start:
123 - 'helm upgrade --namespace {{ env.k8s.namespace }}
124 --post-renderer /bns/helpers/helm/bns_post_renderer
125 --reuse-values --set replicaCount=1 api-{{ env.unique }} ./helm/api'
126 stop:
127 - 'helm upgrade --namespace {{ env.k8s.namespace }}
128 --post-renderer /bns/helpers/helm/bns_post_renderer
129 --reuse-values --set replicaCount=0 api-{{ env.unique }} ./helm/api'
130 gitRepo: 'https://github.com/your-org/your-angular-repo.git'
131 gitBranch: main
132 gitApplicationPath: /helm/apiAlways include --post-renderer /bns/helpers/helm/bns_post_renderer in your helm commands. This adds labels so Bunnyshell can track resources, show logs, and manage component lifecycle.
Step 3: Deploy and Enable Preview Environments
Same flow: paste the config in Configuration, hit Deploy, then enable ephemeral environments in Settings.
Enabling Preview Environments (All Approaches)
Regardless of which approach you used, enabling automatic preview environments is the same:
- Ensure your primary environment has been deployed at least once (Running or Stopped status)
- Go to Settings in your environment
- Toggle "Create ephemeral environments on pull request" to ON
- Toggle "Destroy environment after merge or close pull request" to ON
- Select the target Kubernetes cluster
What happens next:
- Bunnyshell adds a webhook to your Git provider automatically
- When a developer opens a PR, Bunnyshell creates an ephemeral environment cloned from the primary, using the PR's branch
- Bunnyshell posts a comment on the PR with a direct link to the running deployment
- When the PR is merged or closed, the ephemeral environment is automatically destroyed
No GitHub Actions. No GitLab CI pipelines. No maintenance. It just works.
Optional: CI/CD Integration via CLI
If you prefer to control preview environments from your CI/CD pipeline, you can use the Bunnyshell CLI:
1# Install
2brew install bunnyshell/tap/bunnyshell-cli
3
4# Authenticate
5export BUNNYSHELL_TOKEN=your-api-token
6
7# Create and deploy
8bns environments create --from-path bunnyshell.yaml --name "pr-123" --project PROJECT_ID --k8s CLUSTER_ID
9bns environments deploy --id ENV_ID --waitRemote Development and Debugging
Bunnyshell makes it easy to develop and debug directly against any environment — primary or ephemeral:
Port Forwarding
Connect your local tools to remote services:
1# Forward PostgreSQL to local port 15432
2bns port-forward 15432:5432 --component POSTGRES_COMPONENT_ID
3
4# Connect with psql or any DB tool
5psql -h 127.0.0.1 -p 15432 -U angular -d angular
6
7# Forward the API backend to local port 13000
8bns port-forward 13000:3000 --component API_COMPONENT_IDExecute Commands
1# Check the Angular build output
2bns exec COMPONENT_ID -- ls -la /usr/share/nginx/html
3
4# For SSR: check Node process
5bns exec COMPONENT_ID -- node -e "console.log(process.env.PORT)"
6
7# Run database migrations on the API backend
8bns exec API_COMPONENT_ID -- npx prisma migrate deploy
9
10# Seed test data
11bns exec API_COMPONENT_ID -- node dist/seed.jsLive Logs
1# Stream logs in real time
2bns logs --component COMPONENT_ID -f
3
4# Last 200 lines
5bns logs --component COMPONENT_ID --tail 200
6
7# Logs from the last 5 minutes
8bns logs --component COMPONENT_ID --since 5mLive Code Sync
For active development, sync your local code changes to the remote container in real time:
1bns remote-development up --component COMPONENT_ID
2# Edit files locally — changes sync automatically
3# When done:
4bns remote-development downLive code sync for the Angular SPA (Nginx) container requires a rebuild (ng build) to see changes. For faster iteration, sync against the API backend instead — Node servers typically support hot reload. Alternatively, develop the Angular frontend locally with ng serve pointed at the remote API URL.
Troubleshooting
| Issue | Solution |
|---|---|
| 404 on route refresh | Missing try_files $uri $uri/ /index.html; in Nginx config. Without this, Nginx can't find files for Angular routes and returns 404. |
| API calls failing (CORS) | CORS_ORIGIN on the API backend doesn't match the Angular app's URL. Use Bunnyshell interpolation: 'https://app-{{ env.base_domain }}'. |
environment.prod.ts has wrong API URL | Don't use compile-time environment files for preview environments. Switch to runtime config via /app-config endpoint or window.__env. |
| Blank page / white screen | Check browser console. Usually a missing <base href="/"> in index.html, or the Angular build failed silently. Check build logs with bns logs. |
| SSR server not starting | Verify the entry point path: node dist/your-app/server/server.mjs. The project name in the path must match your angular.json project name. |
ng serve in production | Never use ng serve in a Dockerfile. It's a dev server. Use Nginx for SPA or node dist/*/server/server.mjs for SSR. |
| Large Docker image (1GB+) | Missing multi-stage build. node_modules from the build stage shouldn't be in the final Nginx image. Only copy dist/*/browser to Nginx. |
| Slow builds | Add a .dockerignore with node_modules, dist, .angular, .git. Use npm ci instead of npm install. |
| Mixed content errors | API URLs use http:// but the Angular app is served over HTTPS. Always use https:// in your Bunnyshell interpolation URLs. |
| 502 Bad Gateway (SSR) | Node process crashed. Check PORT environment variable matches the exposed port. Verify with bns logs --component COMPONENT_ID. |
What's Next?
- Add authentication — Integrate Auth0, Firebase Auth, or Keycloak as a separate component so each preview environment has its own auth instance
- Add Storybook — Deploy Storybook as a separate component (
npx storybook build+ Nginx) for UI component review alongside the full app - Add E2E tests — Run Cypress or Playwright against the preview environment URL in your CI pipeline
- Add a CDN layer — Configure Cloudflare or CloudFront in front of the Nginx service for production-like caching behavior
- Monitor with Angular DevTools — Angular DevTools browser extension works on preview environments — debug change detection and component trees on any PR
Related Resources
- Bunnyshell Quickstart Guide
- Docker Compose with Bunnyshell
- Helm with Bunnyshell
- Bunnyshell CLI Reference
- Ephemeral Environments — Learn more about the concept
- 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.