Preview Environments for NestJS: Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202612 min read

Preview Environments for NestJS: Automated Per-PR Deployments with Bunnyshell

Why Preview Environments for NestJS?

Every NestJS team knows the friction: you write a Prisma migration, test it locally against SQLite, push to staging — and the migration fails because staging has a different Postgres version or a conflicting schema from another branch. Or someone force-pushed queue consumers to staging right before your API demo. Or two feature branches are sharing the same Redis keyspace and interfering with each other's caches.

Preview environments solve this. Every pull request gets its own isolated deployment — NestJS app running on Node, PostgreSQL database, Redis for caching and queues — all running in Kubernetes with production-like configuration. Reviewers click a link and test the actual running API, not just the diff.

With Bunnyshell, you get:

  • Automatic deployment — A new environment spins up for every PR
  • Production parity — Same Docker images, same database engine, same queue driver
  • 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 NestJS. Pick the one that fits your workflow:

ApproachBest forComplexityCI/CD maintenance
Approach A: Bunnyshell UITeams that want the fastest setup with zero pipeline maintenanceEasiestNone — Bunnyshell manages webhooks automatically
Approach B: Docker Compose ImportTeams already using docker-compose.yml for local developmentEasyNone — import converts to Bunnyshell config automatically
Approach C: Helm ChartsTeams with existing Helm infrastructure or complex K8s needsAdvancedOptional — 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 NestJS App

Regardless of which approach you choose, your NestJS app needs a proper Docker setup and the right configuration for running behind a Kubernetes ingress.

1. Create a Production-Ready Dockerfile

NestJS compiles TypeScript to JavaScript and runs directly on Node. Here's a multi-stage Dockerfile using Node 20 Alpine:

Dockerfile
1# ── Stage 1: Build ──
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5
6# Install dependencies first (layer caching)
7COPY package.json package-lock.json ./
8RUN npm ci
9
10# Copy source and compile
11COPY . .
12RUN npm run build
13
14# ── Stage 2: Production runtime ──
15FROM node:20-alpine AS runtime
16
17RUN apk add --no-cache dumb-init
18
19WORKDIR /app
20
21# Copy only production dependencies
22COPY package.json package-lock.json ./
23RUN npm ci --omit=dev && npm cache clean --force
24
25# Copy compiled output from builder
26COPY --from=builder /app/dist ./dist
27
28# Copy Prisma schema if using Prisma (adjust path as needed)
29COPY --from=builder /app/prisma ./prisma
30
31# Non-root user
32USER node
33
34EXPOSE 3000
35
36CMD ["dumb-init", "node", "dist/main"]

Why dumb-init? NestJS runs as a Node.js process. In a container, PID 1 needs to handle signals correctly (SIGTERM for graceful shutdown). dumb-init is a minimal init process that forwards signals to the Node process.

2. Configure NestJS for Kubernetes

NestJS needs specific settings to work correctly behind a Kubernetes ingress (which terminates TLS). Edit your main.ts:

TypeScript
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3
4async function bootstrap() {
5  const app = await NestFactory.create(AppModule);
6
7  // Trust the Kubernetes ingress proxy — required for correct HTTPS URL generation
8  // and for req.ip to show the real client IP, not the ingress IP.
9  const expressApp = app.getHttpAdapter().getInstance();
10  expressApp.set('trust proxy', true);
11
12  // Listen on all interfaces — required in containers
13  const port = process.env.PORT ?? 3000;
14  await app.listen(port, '0.0.0.0');
15
16  console.log(`Application is running on port ${port}`);
17}
18bootstrap();

Without trust proxy, NestJS (via Express) will receive HTTPS requests forwarded as HTTP from the ingress, and any code that inspects req.protocol or generates absolute URLs will produce http:// links instead of https://.

3. Set Up Database Migrations

Option A: Prisma

If you're using Prisma, run migrations at startup via an entrypoint script or a Kubernetes init container. The simplest approach is to add it to your package.json start script:

JSON
1{
2  "scripts": {
3    "start:prod": "npx prisma migrate deploy && node dist/main"
4  }
5}

Then update your Dockerfile CMD:

Dockerfile
CMD ["dumb-init", "sh", "-c", "npx prisma migrate deploy && node dist/main"]

prisma migrate deploy is safe to run on every startup — it only applies pending migrations and is idempotent. It does not reset your database.

Option B: TypeORM

If you're using TypeORM, enable synchronize: false in production and use migrations instead. Configure your data source:

TypeScript
1// src/database/data-source.ts
2import { DataSource } from 'typeorm';
3
4export const AppDataSource = new DataSource({
5  type: 'postgres',
6  url: process.env.DATABASE_URL,
7  entities: ['dist/**/*.entity.js'],
8  migrations: ['dist/migrations/*.js'],
9  synchronize: false,  // Never use true in production
10  migrationsRun: true, // Run migrations automatically on app start
11  ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
12});

4. Health Checks via @nestjs/terminus

Add health checks so Kubernetes knows when your app is ready to receive traffic:

Bash
npm install @nestjs/terminus
TypeScript
1// src/health/health.controller.ts
2import { Controller, Get } from '@nestjs/common';
3import { HealthCheck, HealthCheckService, HttpHealthIndicator, TypeOrmHealthIndicator } from '@nestjs/terminus';
4
5@Controller('health')
6export class HealthController {
7  constructor(
8    private health: HealthCheckService,
9    private http: HttpHealthIndicator,
10    private db: TypeOrmHealthIndicator,
11  ) {}
12
13  @Get()
14  @HealthCheck()
15  check() {
16    return this.health.check([
17      () => this.db.pingCheck('database'),
18    ]);
19  }
20}

Register the TerminusModule in your AppModule.

5. Environment Variables

Update your .env.example with Bunnyshell-friendly defaults:

.env
1PORT=3000
2NODE_ENV=production
3
4DATABASE_URL=postgresql://nestjs:password@postgres:5432/nestjs
5
6REDIS_URL=redis://redis:6379
7
8JWT_SECRET=your-jwt-secret-here
9JWT_EXPIRES_IN=7d

NestJS Deployment Checklist

  • Multi-stage Dockerfile: node:20-alpine builder + runtime
  • dumb-init for correct signal handling in containers
  • app.set('trust proxy', true) in main.ts for K8s ingress TLS termination
  • listen(port, '0.0.0.0') — binds to all interfaces, not just localhost
  • Database migrations run at startup (prisma migrate deploy or TypeORM migrationsRun: true)
  • Health check endpoint at /health via @nestjs/terminus
  • DATABASE_URL and JWT_SECRET will be set via environment variables
  • PORT env var respected, defaults to 3000

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

  1. Log into Bunnyshell
  2. Click Create project and name it (e.g., "NestJS App")
  3. Inside the project, click Create environment and name it (e.g., "nestjs-main")

Step 2: Define the Environment Configuration

Click Configuration in your environment view and paste this bunnyshell.yaml:

YAML
1kind: Environment
2name: nestjs-preview
3type: primary
4
5environmentVariables:
6  DB_PASSWORD: SECRET["your-db-password"]
7  JWT_SECRET: SECRET["your-jwt-secret"]
8
9components:
10  # ── NestJS Application ──
11  - kind: Application
12    name: nestjs-app
13    gitRepo: 'https://github.com/your-org/your-nestjs-repo.git'
14    gitBranch: main
15    gitApplicationPath: /
16    dockerCompose:
17      build:
18        context: .
19        dockerfile: Dockerfile
20      environment:
21        NODE_ENV: production
22        PORT: '3000'
23        DATABASE_URL: 'postgresql://nestjs:{{ env.vars.DB_PASSWORD }}@postgres:5432/nestjs'
24        REDIS_URL: 'redis://redis:6379'
25        JWT_SECRET: '{{ env.vars.JWT_SECRET }}'
26        APP_URL: 'https://{{ components.nestjs-app.ingress.hosts[0] }}'
27      ports:
28        - '3000:3000'
29    dependsOn:
30      - postgres
31      - redis
32    hosts:
33      - hostname: 'app-{{ env.base_domain }}'
34        path: /
35        servicePort: 3000
36
37  # ── PostgreSQL Database ──
38  - kind: Database
39    name: postgres
40    dockerCompose:
41      image: 'postgres:16-alpine'
42      environment:
43        POSTGRES_USER: nestjs
44        POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
45        POSTGRES_DB: nestjs
46      ports:
47        - '5432:5432'
48
49  # ── Redis Cache & Queue ──
50  - kind: Service
51    name: redis
52    dockerCompose:
53      image: 'redis:7-alpine'
54      command: ['redis-server', '--appendonly', 'yes']
55      ports:
56        - '6379:6379'
57
58volumes:
59  - name: postgres-data
60    mount:
61      component: postgres
62      containerPath: /var/lib/postgresql/data
63    size: 1Gi
64  - name: redis-data
65    mount:
66      component: redis
67      containerPath: /data
68    size: 512Mi

Key architecture notes:

  • Single container — Unlike PHP-FPM + Nginx, NestJS is a single process that handles HTTP directly. No sidecar needed.
  • DATABASE_URL interpolation — The connection string is built dynamically using env.vars.DB_PASSWORD, so the password is never hardcoded.
  • APP_URL — Injected as https://{{ components.nestjs-app.ingress.hosts[0] }} so the app always knows its public URL.

Replace your-org/your-nestjs-repo with your actual repository. Save the configuration.

Step 3: Deploy

Click the Deploy button, select your Kubernetes cluster, and click Deploy Environment. Bunnyshell will:

  1. Build your NestJS Docker image from the Dockerfile
  2. Pull PostgreSQL and Redis images
  3. Deploy everything into an isolated Kubernetes namespace
  4. Generate HTTPS URLs automatically with DNS

Monitor the deployment in the environment detail page. When status shows Running, click Endpoints to access your live NestJS app.

Step 4: Verify Migrations and Health

After deployment, verify migrations ran and the health check endpoint responds:

Bash
1export BUNNYSHELL_TOKEN=your-api-token
2bns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'
3
4# Check health endpoint
5curl https://app-YOUR_ENV_DOMAIN.bunnyshell.com/health
6
7# View startup logs to confirm migrations ran
8bns logs --component COMPONENT_ID --tail 100
9
10# Run Prisma migrations manually if needed
11bns exec COMPONENT_ID -- npx prisma migrate deploy
12
13# Open a shell for debugging
14bns exec COMPONENT_ID -- sh

Step 5: Enable Automatic Preview Environments

This is the magic step — no CI/CD configuration needed:

  1. In your environment, go to Settings
  2. Find the Ephemeral environments section
  3. Toggle "Create ephemeral environments on pull request" to ON
  4. Toggle "Destroy environment after merge or close pull request" to ON
  5. 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? Most NestJS projects do. 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:

YAML
1version: '3.8'
2
3services:
4  nestjs-app:
5    build:
6      context: .
7      dockerfile: Dockerfile
8    ports:
9      - '3000:3000'
10    environment:
11      NODE_ENV: development
12      PORT: '3000'
13      DATABASE_URL: 'postgresql://nestjs:secret@postgres:5432/nestjs'
14      REDIS_URL: 'redis://redis:6379'
15      JWT_SECRET: 'local-dev-secret'
16    depends_on:
17      postgres:
18        condition: service_healthy
19      redis:
20        condition: service_started
21    volumes:
22      - .:/app
23      - /app/node_modules
24
25  postgres:
26    image: postgres:16-alpine
27    environment:
28      POSTGRES_USER: nestjs
29      POSTGRES_PASSWORD: secret
30      POSTGRES_DB: nestjs
31    volumes:
32      - postgres-data:/var/lib/postgresql/data
33    ports:
34      - '5432:5432'
35    healthcheck:
36      test: ['CMD-SHELL', 'pg_isready -U nestjs']
37      interval: 10s
38      timeout: 5s
39      retries: 5
40
41  redis:
42    image: redis:7-alpine
43    command: ['redis-server', '--appendonly', 'yes']
44    volumes:
45      - redis-data:/data
46    ports:
47      - '6379:6379'
48
49volumes:
50  postgres-data:
51  redis-data:

Step 2: Import into Bunnyshell

  1. Create a Project and Environment in Bunnyshell (same as Approach A, Step 1)
  2. Click Define environment
  3. Select your Git account and repository
  4. Set the branch (e.g., main) and the path to docker-compose.yml (use / if it's in the root)
  5. Click Continue — Bunnyshell parses and validates your Docker Compose file

Bunnyshell automatically detects:

  • All services (nestjs-app, postgres, redis)
  • Exposed ports
  • Build configurations (Dockerfiles)
  • Volumes
  • Environment variables

It converts everything into a bunnyshell.yaml environment definition.

The docker-compose.yml is only read during the initial import. Subsequent changes to the file won't auto-propagate — edit the environment configuration in Bunnyshell instead.

Step 3: Adjust the Configuration

After import, go to Configuration in the environment view and update:

Replace hardcoded secrets with SECRET["..."] syntax:

YAML
1environmentVariables:
2  DB_PASSWORD: SECRET["your-db-password"]
3  JWT_SECRET: SECRET["your-jwt-secret"]

Add dynamic URLs using Bunnyshell interpolation:

YAML
APP_URL: 'https://{{ components.nestjs-app.ingress.hosts[0] }}'
DATABASE_URL: 'postgresql://nestjs:{{ env.vars.DB_PASSWORD }}@postgres:5432/nestjs'

Remove local dev volumes — The volumes: ['.:/app'] mount is for live reload in local development. Remove it in Bunnyshell — the Docker image already contains the built app:

YAML
1# Remove this from the nestjs-app component:
2# volumes:
3#   - .:/app
4#   - /app/node_modules

Add an ingress host to expose the app:

YAML
1hosts:
2  - hostname: 'app-{{ env.base_domain }}'
3    path: /
4    servicePort: 3000

Step 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 dev volumes — Local bind mounts (.:/app) break container builds in Kubernetes. The image contains the compiled app.
  • Use Bunnyshell interpolation for dynamic values:
YAML
1# Local docker-compose.yml
2DATABASE_URL: postgresql://nestjs:secret@postgres:5432/nestjs
3
4# Bunnyshell environment config (after import)
5DATABASE_URL: 'postgresql://nestjs:{{ env.vars.DB_PASSWORD }}@postgres:5432/nestjs'
  • Design for startup resilience — Kubernetes doesn't guarantee depends_on ordering. NestJS with TypeORM will retry DB connections, but add explicit retry logic in your bootstrap if your app fails fast on connection errors.

Approach C: Helm Charts

For teams with existing Helm infrastructure or complex Kubernetes requirements (custom ingress, service mesh, advanced scaling, multiple worker processes). Helm gives you full control over every Kubernetes resource.

Step 1: Create a Helm Chart

Structure your NestJS Helm chart in your repo:

Text
1helm/nestjs/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5    ├── deployment.yaml
6    ├── service.yaml
7    ├── ingress.yaml
8    ├── configmap.yaml
9    └── migration-job.yaml

A minimal values.yaml:

YAML
1replicaCount: 1
2image:
3  repository: ""
4  tag: latest
5service:
6  port: 3000
7ingress:
8  enabled: true
9  className: bns-nginx
10  host: ""
11env:
12  NODE_ENV: production
13  PORT: "3000"
14  DATABASE_URL: ""
15  REDIS_URL: ""
16  JWT_SECRET: ""
17  APP_URL: ""
18resources:
19  requests:
20    memory: "256Mi"
21    cpu: "100m"
22  limits:
23    memory: "512Mi"
24    cpu: "500m"

Step 2: Define the Bunnyshell Configuration

Create a bunnyshell.yaml using Helm components:

YAML
1kind: Environment
2name: nestjs-helm
3type: primary
4
5environmentVariables:
6  DB_PASSWORD: SECRET["your-db-password"]
7  JWT_SECRET: SECRET["your-jwt-secret"]
8
9components:
10  # ── Docker Image Build ──
11  - kind: DockerImage
12    name: nestjs-image
13    context: /
14    dockerfile: Dockerfile
15    gitRepo: 'https://github.com/your-org/your-nestjs-repo.git'
16    gitBranch: main
17    gitApplicationPath: /
18
19  # ── PostgreSQL via Helm (Bitnami) ──
20  - kind: Helm
21    name: postgres
22    runnerImage: 'dtzar/helm-kubectl:3.8.2'
23    deploy:
24      - |
25        cat << EOF > postgres_values.yaml
26          global:
27            storageClass: bns-network-sc
28          auth:
29            postgresPassword: {{ env.vars.DB_PASSWORD }}
30            username: nestjs
31            password: {{ env.vars.DB_PASSWORD }}
32            database: nestjs
33        EOF
34      - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
35      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
36        --post-renderer /bns/helpers/helm/bns_post_renderer
37        -f postgres_values.yaml postgres bitnami/postgresql --version 12.12.10'
38      - |
39        POSTGRES_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
40    destroy:
41      - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
42    start:
43      - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }} statefulset/postgres-postgresql'
44    stop:
45      - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }} statefulset/postgres-postgresql'
46    exportVariables:
47      - POSTGRES_HOST
48
49  # ── NestJS App via Helm ──
50  - kind: Helm
51    name: nestjs-app
52    runnerImage: 'dtzar/helm-kubectl:3.8.2'
53    deploy:
54      - |
55        cat << EOF > nestjs_values.yaml
56          replicaCount: 1
57          image:
58            repository: {{ components.nestjs-image.image }}
59          service:
60            port: 3000
61          ingress:
62            enabled: true
63            className: bns-nginx
64            host: app-{{ env.base_domain }}
65          env:
66            NODE_ENV: production
67            PORT: "3000"
68            DATABASE_URL: 'postgresql://nestjs:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.POSTGRES_HOST }}:5432/nestjs'
69            REDIS_URL: 'redis://redis:6379'
70            JWT_SECRET: '{{ env.vars.JWT_SECRET }}'
71            APP_URL: 'https://app-{{ env.base_domain }}'
72        EOF
73      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
74        --post-renderer /bns/helpers/helm/bns_post_renderer
75        -f nestjs_values.yaml nestjs-{{ env.unique }} ./helm/nestjs'
76    destroy:
77      - 'helm uninstall nestjs-{{ env.unique }} --namespace {{ env.k8s.namespace }}'
78    start:
79      - 'helm upgrade --namespace {{ env.k8s.namespace }}
80        --post-renderer /bns/helpers/helm/bns_post_renderer
81        --reuse-values --set replicaCount=1 nestjs-{{ env.unique }} ./helm/nestjs'
82    stop:
83      - 'helm upgrade --namespace {{ env.k8s.namespace }}
84        --post-renderer /bns/helpers/helm/bns_post_renderer
85        --reuse-values --set replicaCount=0 nestjs-{{ env.unique }} ./helm/nestjs'
86    gitRepo: 'https://github.com/your-org/your-nestjs-repo.git'
87    gitBranch: main
88    gitApplicationPath: /helm/nestjs
89
90  # ── Redis ──
91  - kind: Service
92    name: redis
93    dockerCompose:
94      image: 'redis:7-alpine'
95      command: ['redis-server', '--appendonly', 'yes']
96      ports:
97        - '6379:6379'

Always 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:

  1. Ensure your primary environment has been deployed at least once (Running or Stopped status)
  2. Go to Settings in your environment
  3. Toggle "Create ephemeral environments on pull request" → ON
  4. Toggle "Destroy environment after merge or close pull request" → ON
  5. 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 (e.g., for custom seed scripts or integration tests), you can use the Bunnyshell CLI:

Bash
1# Install
2brew install bunnyshell/tap/bunnyshell-cli
3
4# Authenticate
5export BUNNYSHELL_TOKEN=your-api-token
6
7# Create, deploy, and verify in one flow
8bns environments create --from-path bunnyshell.yaml --name "pr-123" --project PROJECT_ID --k8s CLUSTER_ID
9bns environments deploy --id ENV_ID --wait
10
11# Run Prisma migrations
12bns exec COMPONENT_ID -- npx prisma migrate deploy
13
14# Seed test data
15bns exec COMPONENT_ID -- node dist/scripts/seed.js

Remote 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 the remote database:

Bash
1# Forward PostgreSQL to local port 15432
2bns port-forward 15432:5432 --component POSTGRES_COMPONENT_ID
3
4# Connect with psql, TablePlus, or any DB tool
5psql -h 127.0.0.1 -p 15432 -U nestjs nestjs
6
7# Forward Redis to local port 16379
8bns port-forward 16379:6379 --component REDIS_COMPONENT_ID
9redis-cli -p 16379
10
11# Forward the NestJS app to local port 3001 (for local frontend testing against remote API)
12bns port-forward 3001:3000 --component NESTJS_COMPONENT_ID

Execute Commands

Bash
1# Run Prisma migrations manually
2bns exec COMPONENT_ID -- npx prisma migrate deploy
3
4# Prisma Studio (database GUI) — accessible via port forward
5bns exec COMPONENT_ID -- npx prisma studio --port 5555 --browser none
6
7# TypeORM migration commands
8bns exec COMPONENT_ID -- node dist/migration-runner.js
9
10# Seed the database
11bns exec COMPONENT_ID -- node dist/scripts/seed.js
12
13# Run a custom script
14bns exec COMPONENT_ID -- node dist/scripts/backfill-users.js
15
16# Interactive Node.js REPL
17bns exec COMPONENT_ID -- node

Live Logs

Bash
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 5m

Live Code Sync

For active development, sync your local code changes to the remote container in real time:

Bash
1bns remote-development up --component COMPONENT_ID
2# Edit files locally — changes sync automatically to the running container
3# Hot-reload (if configured with nodemon or ts-node-dev) picks up the changes
4# When done:
5bns remote-development down

This is especially useful for debugging issues that only reproduce in the Kubernetes environment — you get the fast feedback loop of local development with the infrastructure of production.


Troubleshooting

IssueSolution
App starts but health check failsVerify /health endpoint is exposed and the TerminusModule is registered in AppModule. Check DB connectivity — TypeORM/Prisma may not have connected yet.
connect ECONNREFUSED 127.0.0.1:5432NestJS is trying to connect to DB before it's ready. Add retry logic or use depends_on in bunnyshell.yaml. Verify DB_HOST is postgres (the component name), not localhost.
Mixed content / HTTPS errorsMissing app.set('trust proxy', true) in main.ts. NestJS via Express needs this to detect HTTPS behind the K8s ingress.
Cannot GET /The app is listening but no route is registered at /. NestJS returns 404 for unregistered routes — check your controller paths.
Prisma P1001: Can't reach databasePostgreSQL is not ready when migrations run. Add a startup retry loop or use an init container to wait for DB.
TypeORM synchronize: true errorNever use synchronize: true in production — it can drop columns. Use migrationsRun: true with explicit migration files instead.
JWT errors after redeployIf JWT_SECRET changed between deployments, existing tokens become invalid. Store it as SECRET["jwt-secret"] and keep it stable across deploys.
Port 3000 not accessibleVerify listen(port, '0.0.0.0') in main.ts. Without '0.0.0.0', Node defaults to 127.0.0.1 and is unreachable from the Kubernetes service.
Build fails: TypeScript errorsThe multi-stage Dockerfile compiles TypeScript. Run npm run build locally first to catch TS errors before pushing.
Redis ECONNREFUSEDEnsure REDIS_URL uses redis (the component name) as the hostname, not localhost. Check the Redis component is Running.
MODULE_NOT_FOUND at runtimeProduction node_modules not installed. Verify the npm ci --omit=dev step in the Dockerfile copies to the right path and dist/ exists.
522 Connection timed outCluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller.

What's Next?

  • Add BullMQ workers — Process background jobs with @nestjs/bull. Add a separate Service component with node dist/workers/email.worker.js
  • Add task scheduling — Use @nestjs/schedule for cron jobs that run inside the NestJS process, or add a dedicated worker component
  • Add Mailpit — Test transactional emails with a local SMTP catcher (axllent/mailpit as a Service component, point SMTP_HOST to mailpit)
  • Add MinIO — S3-compatible object storage for file uploads (minio/minio as a Service component)
  • Seed per-PR data — Run bns exec <ID> -- node dist/scripts/demo-seed.js after ephemeral environment creation via a post-deploy hook
  • Monitor with OpenTelemetry — Add @opentelemetry/sdk-node for distributed tracing across NestJS + PostgreSQL + Redis

Ship faster starting today.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.