Preview Environments for SvelteKit: Automated Per-PR Deployments with Bunnyshell
Why Preview Environments for SvelteKit?
Every SvelteKit team has been through it: you add a new form action with a database migration, test it locally with a dev database, push to the shared staging server — and the page throws a CSRF error because staging's ORIGIN environment variable still points to the old domain. Or someone deployed their half-finished hooks.server.ts changes to staging and now every request fails the authentication check. Or the designer can't review your layout changes because another developer's broken load function is returning 500s on every page.
Preview environments solve this. Every pull request gets its own isolated deployment — SvelteKit app running on Node.js, PostgreSQL database with a clean schema, Redis if needed — all deployed 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 database engine, same environment variables
- Isolation — Each PR environment is fully independent, no shared staging conflicts
- Automatic cleanup — Environments are destroyed when the PR is merged or closed
SvelteKit is a natural fit for preview environments. Form actions, load functions, and server hooks all run server-side — there's no separate API layer to deploy. The adapter-node output is a single, self-contained Node.js server. And because SvelteKit handles SSR, CSRF protection, and routing in one unified framework, a preview environment gives reviewers the complete picture of every change.
Choose Your Approach
Bunnyshell supports three ways to set up preview environments for SvelteKit. 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 SvelteKit App
Regardless of which approach you choose, your SvelteKit app needs a proper Docker setup, the adapter-node configured, and the right environment variables for running behind a Kubernetes ingress.
1. Install and Configure adapter-node
SvelteKit needs @sveltejs/adapter-node to produce a standalone Node.js server. Install it and update your config:
npm install -D @sveltejs/adapter-nodeUpdate svelte.config.js:
1import adapter from '@sveltejs/adapter-node';
2import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
4/** @type {import('@sveltejs/kit').Config} */
5const config = {
6 preprocess: vitePreprocess(),
7 kit: {
8 adapter: adapter({
9 // Output to build/ directory
10 out: 'build',
11 // Read HOST and PORT from environment
12 envPrefix: ''
13 })
14 }
15};
16
17export default config;SvelteKit defaults to adapter-auto, which tries to detect your deployment platform. For Kubernetes/Docker, you must switch to adapter-node explicitly. Without it, vite build won't produce a standalone server.
2. Create a Production-Ready Dockerfile
SvelteKit with adapter-node builds to a standalone Node.js server at build/index.js. Here's a multi-stage Dockerfile:
1FROM node:20-alpine AS base
2WORKDIR /app
3
4# Install dependencies only when package files change
5FROM base AS deps
6COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
7COPY prisma ./prisma/
8RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
9 elif [ -f package-lock.json ]; then npm ci; \
10 elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
11 else echo "No lockfile found" && exit 1; fi
12
13# Generate Prisma client (if using Prisma)
14RUN npx prisma generate || true
15
16# Build the SvelteKit app
17FROM base AS build
18COPY /app/node_modules ./node_modules
19COPY . .
20RUN npm run build
21
22# Prune dev dependencies
23RUN npm prune --production
24
25# Production image — minimal footprint
26FROM base AS production
27ENV NODE_ENV=production
28ENV HOST=0.0.0.0
29ENV PORT=3000
30
31# Copy only what's needed to run
32COPY /app/build ./build
33COPY /app/node_modules ./node_modules
34COPY /app/package.json ./
35COPY /app/prisma ./prisma
36
37# Run database migrations on startup, then start the server
38COPY docker-entrypoint.sh ./
39RUN chmod +x docker-entrypoint.sh
40
41EXPOSE 3000
42ENTRYPOINT ["./docker-entrypoint.sh"]3. Create the Entrypoint Script
Create docker-entrypoint.sh in your repo root:
1#!/bin/sh
2set -e
3
4echo "Running Prisma migrations..."
5npx prisma migrate deploy
6
7echo "Starting SvelteKit server..."
8exec node build/index.jsIf you use Drizzle ORM instead of Prisma, replace the migration line with npx drizzle-kit migrate. Both ORMs work well with SvelteKit in preview environments.
4. Configure SvelteKit for Kubernetes
SvelteKit needs specific environment variables to work correctly behind a Kubernetes ingress:
HOST=0.0.0.0 — The Node.js server must bind to all interfaces, not just localhost.
PORT=3000 — Standard port. Must match the EXPOSE in your Dockerfile.
ORIGIN — This is the critical one. SvelteKit uses ORIGIN for CSRF protection on form actions. It must match the public URL of your preview environment, including the protocol. If ORIGIN doesn't match the incoming request's origin, SvelteKit rejects all POST requests with a 403 CSRF error.
# ORIGIN must match exactly — protocol + hostname, no trailing slash
ORIGIN=https://app-abc123.bunnyshell.devIn Bunnyshell, you'll set this dynamically using interpolation:
ORIGIN: 'https://{{ components.sveltekit-app.ingress.hosts[0] }}'BODY_SIZE_LIMIT — If your app handles file uploads, you may need to increase the default body size limit:
# Default is 512KB. Set to 0 for unlimited, or specify a value
BODY_SIZE_LIMIT=10485760 # 10MB5. Server Hooks for Authentication
If your app uses hooks.server.ts for authentication or session management, ensure it reads from environment variables:
1// src/hooks.server.ts
2import type { Handle } from '@sveltejs/kit';
3import { env } from '$env/dynamic/private';
4
5export const handle: Handle = async ({ event, resolve }) => {
6 // Read session from cookie
7 const sessionId = event.cookies.get('session_id');
8
9 if (sessionId) {
10 // Validate session against database
11 const session = await getSession(sessionId);
12 if (session) {
13 event.locals.user = session.user;
14 }
15 }
16
17 return resolve(event);
18};Use $env/dynamic/private (not $env/static/private) for values that change between environments. Dynamic env vars are read at runtime, which is what you need for preview environments where the URL differs per PR.
6. Environment Variables
Create or update your .env.example with Bunnyshell-friendly defaults:
1NODE_ENV=production
2HOST=0.0.0.0
3PORT=3000
4ORIGIN=https://example.com
5
6DATABASE_URL=postgresql://sveltekit:password@postgres:5432/sveltekit_db?schema=public
7SESSION_SECRET=your-session-secret
8
9# Optional
10BODY_SIZE_LIMIT=10485760
11REDIS_URL=redis://redis:6379SvelteKit Deployment Checklist
-
adapter-nodeinstalled and configured insvelte.config.js - Dockerfile uses multi-stage build with Node 20 Alpine
-
docker-entrypoint.shruns migrations before starting the server -
HOST=0.0.0.0so the server binds to all interfaces -
ORIGINwill use Bunnyshell interpolation to match the preview URL (required for CSRF) -
SESSION_SECRETwill be set viaSECRET["..."]in Bunnyshell -
$env/dynamic/privateused for runtime environment variables - No Nginx sidecar needed —
adapter-nodeserves HTTP directly
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., "SvelteKit App")
- Inside the project, click Create environment and name it (e.g., "sveltekit-main")
Step 2: Define the Environment Configuration
Click Configuration in your environment view and paste this bunnyshell.yaml:
1kind: Environment
2name: sveltekit-preview
3type: primary
4
5environmentVariables:
6 SESSION_SECRET: SECRET["your-session-secret"]
7 DB_PASSWORD: SECRET["your-db-password"]
8 DB_USER: sveltekit
9 DB_NAME: sveltekit_db
10
11components:
12 # ── SvelteKit Application ──
13 - kind: Application
14 name: sveltekit-app
15 gitRepo: 'https://github.com/your-org/your-sveltekit-repo.git'
16 gitBranch: main
17 gitApplicationPath: /
18 dockerCompose:
19 build:
20 context: .
21 dockerfile: Dockerfile
22 environment:
23 NODE_ENV: production
24 HOST: '0.0.0.0'
25 PORT: '3000'
26 ORIGIN: 'https://{{ components.sveltekit-app.ingress.hosts[0] }}'
27 DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@postgres:5432/{{ env.vars.DB_NAME }}?schema=public'
28 SESSION_SECRET: '{{ env.vars.SESSION_SECRET }}'
29 BODY_SIZE_LIMIT: '10485760'
30 ports:
31 - '3000:3000'
32 dependsOn:
33 - postgres
34 hosts:
35 - hostname: 'app-{{ env.base_domain }}'
36 path: /
37 servicePort: 3000
38
39 # ── PostgreSQL Database ──
40 - kind: Database
41 name: postgres
42 dockerCompose:
43 image: 'postgres:16-alpine'
44 environment:
45 POSTGRES_USER: '{{ env.vars.DB_USER }}'
46 POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
47 POSTGRES_DB: '{{ env.vars.DB_NAME }}'
48 ports:
49 - '5432:5432'
50
51 # ── Redis (Sessions / Cache) ──
52 - kind: Service
53 name: redis
54 dockerCompose:
55 image: 'redis:7-alpine'
56 ports:
57 - '6379:6379'
58
59volumes:
60 - name: postgres-data
61 mount:
62 component: postgres
63 containerPath: /var/lib/postgresql/data
64 size: 1GiKey architecture notes:
ORIGINinterpolation — This is the most important SvelteKit-specific setting. Without a correctORIGIN, every form action POST will fail with a 403 CSRF error. Bunnyshell interpolation ensures it always matches the actual preview URL.- Single container —
adapter-nodeproduces a standalone HTTP server. No Nginx sidecar or reverse proxy needed. BODY_SIZE_LIMIT— Set to 10MB. Adjust based on your app's needs (file uploads, large form payloads). Set to0to disable the limit entirely.- Migrations run automatically — The
docker-entrypoint.shrunsnpx prisma migrate deploybefore starting the server.
Replace your-org/your-sveltekit-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:
- Build your SvelteKit Docker image from the Dockerfile
- Pull PostgreSQL and Redis images
- 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 SvelteKit app.
Step 4: Verify the Deployment
After deployment, verify everything is working:
1export BUNNYSHELL_TOKEN=your-api-token
2bns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'
3
4# Check migration status
5bns exec COMPONENT_ID -- npx prisma migrate status
6
7# Seed data (if you have a seed script)
8bns exec COMPONENT_ID -- npx prisma db seed
9
10# Verify ORIGIN is set correctly
11bns exec COMPONENT_ID -- printenv ORIGIN
12
13# Check server logs
14bns logs --component COMPONENT_ID --tail 50Always verify that ORIGIN matches the actual URL. If you see 403 Forbidden on form submissions, this is almost certainly the cause. Run bns exec COMPONENT_ID -- printenv ORIGIN and compare it to the URL in your browser.
Step 5: 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. Each ephemeral environment gets its own unique ORIGIN value through Bunnyshell interpolation, so CSRF protection works correctly across all preview environments.
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 sveltekit-app:
5 build:
6 context: .
7 dockerfile: Dockerfile
8 ports:
9 - '3000:3000'
10 environment:
11 NODE_ENV: development
12 HOST: '0.0.0.0'
13 PORT: '3000'
14 ORIGIN: 'http://localhost:3000'
15 DATABASE_URL: 'postgresql://sveltekit:secret@postgres:5432/sveltekit_db?schema=public'
16 SESSION_SECRET: 'local-dev-secret'
17 BODY_SIZE_LIMIT: '10485760'
18 depends_on:
19 postgres:
20 condition: service_healthy
21
22 postgres:
23 image: postgres:16-alpine
24 environment:
25 POSTGRES_USER: sveltekit
26 POSTGRES_PASSWORD: secret
27 POSTGRES_DB: sveltekit_db
28 volumes:
29 - postgres-data:/var/lib/postgresql/data
30 ports:
31 - '5432:5432'
32 healthcheck:
33 test: ['CMD-SHELL', 'pg_isready -U sveltekit -d sveltekit_db']
34 interval: 5s
35 timeout: 5s
36 retries: 5
37
38 redis:
39 image: redis:7-alpine
40 ports:
41 - '6379:6379'
42
43volumes:
44 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 (sveltekit-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:
1environmentVariables:
2 SESSION_SECRET: SECRET["your-session-secret"]
3 DB_PASSWORD: SECRET["your-db-password"]Set ORIGIN dynamically — this is the most critical adjustment for SvelteKit:
1# Docker Compose (local)
2ORIGIN: 'http://localhost:3000'
3
4# Bunnyshell environment config (after import)
5ORIGIN: 'https://{{ components.sveltekit-app.ingress.hosts[0] }}'Add dynamic database URL using Bunnyshell interpolation:
DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@postgres:5432/{{ env.vars.DB_NAME }}?schema=public'Remove local dev volumes — volumes: ['.:/app'] is for local hot reload. Remove these in Bunnyshell — the Docker image already contains the built app.
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
- No reverse proxy needed —
adapter-nodehandles HTTP directly. Don't add an Nginx service unless you have a specific need - Always set
ORIGIN— This is the number one gotcha for SvelteKit in preview environments. Without it, every form action fails - Healthchecks matter — The Postgres
healthcheckensures SvelteKit doesn't try to run migrations before the database is ready. Bunnyshell respects healthchecks from your Compose file - Use
$env/dynamic/privatefor environment variables that differ between environments:
1// src/lib/server/config.ts
2import { env } from '$env/dynamic/private';
3
4export const config = {
5 databaseUrl: env.DATABASE_URL,
6 sessionSecret: env.SESSION_SECRET,
7 origin: env.ORIGIN,
8};- Design for startup resilience — Kubernetes doesn't guarantee
depends_onordering. Yourdocker-entrypoint.shshould handle the case where Postgres isn't ready yet (Prisma retries connections by default)
Approach C: Helm Charts
For teams with existing Helm infrastructure or complex Kubernetes requirements (custom ingress, service mesh, auto-scaling). Helm gives you full control over every Kubernetes resource.
Step 1: Create a Helm Chart
Structure your SvelteKit Helm chart in your repo:
1helm/sveltekit/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5 ├── deployment.yaml
6 ├── service.yaml
7 ├── ingress.yaml
8 ├── configmap.yaml
9 └── migration-job.yamlA minimal values.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 HOST: "0.0.0.0"
14 PORT: "3000"
15 ORIGIN: ""
16 DATABASE_URL: ""
17 SESSION_SECRET: ""
18 BODY_SIZE_LIMIT: "10485760"Step 2: Define the Bunnyshell Configuration
Create a bunnyshell.yaml using Helm components:
1kind: Environment
2name: sveltekit-helm
3type: primary
4
5environmentVariables:
6 SESSION_SECRET: SECRET["your-session-secret"]
7 DB_PASSWORD: SECRET["your-db-password"]
8 DB_USER: sveltekit
9 DB_NAME: sveltekit_db
10
11components:
12 # ── Docker Image Build ──
13 - kind: DockerImage
14 name: sveltekit-image
15 context: /
16 dockerfile: Dockerfile
17 gitRepo: 'https://github.com/your-org/your-sveltekit-repo.git'
18 gitBranch: main
19 gitApplicationPath: /
20
21 # ── PostgreSQL via Helm (Bitnami) ──
22 - kind: Helm
23 name: postgres
24 runnerImage: 'dtzar/helm-kubectl:3.8.2'
25 deploy:
26 - |
27 cat << EOF > pg_values.yaml
28 global:
29 storageClass: bns-network-sc
30 auth:
31 postgresPassword: {{ env.vars.DB_PASSWORD }}
32 database: {{ env.vars.DB_NAME }}
33 username: {{ env.vars.DB_USER }}
34 password: {{ env.vars.DB_PASSWORD }}
35 EOF
36 - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
37 - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
38 --post-renderer /bns/helpers/helm/bns_post_renderer
39 -f pg_values.yaml postgres bitnami/postgresql --version 13.4.4'
40 - |
41 PG_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
42 destroy:
43 - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
44 start:
45 - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}
46 statefulset/postgres-postgresql'
47 stop:
48 - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}
49 statefulset/postgres-postgresql'
50 exportVariables:
51 - PG_HOST
52
53 # ── SvelteKit App via Helm ──
54 - kind: Helm
55 name: sveltekit-app
56 runnerImage: 'dtzar/helm-kubectl:3.8.2'
57 deploy:
58 - |
59 cat << EOF > sveltekit_values.yaml
60 replicaCount: 1
61 image:
62 repository: {{ components.sveltekit-image.image }}
63 service:
64 port: 3000
65 ingress:
66 enabled: true
67 className: bns-nginx
68 host: app-{{ env.base_domain }}
69 env:
70 NODE_ENV: production
71 HOST: '0.0.0.0'
72 PORT: '3000'
73 ORIGIN: 'https://app-{{ env.base_domain }}'
74 DATABASE_URL: 'postgresql://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.PG_HOST }}:5432/{{ env.vars.DB_NAME }}?schema=public'
75 SESSION_SECRET: '{{ env.vars.SESSION_SECRET }}'
76 BODY_SIZE_LIMIT: '10485760'
77 EOF
78 - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
79 --post-renderer /bns/helpers/helm/bns_post_renderer
80 -f sveltekit_values.yaml sveltekit-{{ env.unique }} ./helm/sveltekit'
81 destroy:
82 - 'helm uninstall sveltekit-{{ 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 sveltekit-{{ env.unique }} ./helm/sveltekit'
87 stop:
88 - 'helm upgrade --namespace {{ env.k8s.namespace }}
89 --post-renderer /bns/helpers/helm/bns_post_renderer
90 --reuse-values --set replicaCount=0 sveltekit-{{ env.unique }} ./helm/sveltekit'
91 gitRepo: 'https://github.com/your-org/your-sveltekit-repo.git'
92 gitBranch: main
93 gitApplicationPath: /helm/sveltekit
94
95 # ── Redis ──
96 - kind: Service
97 name: redis
98 dockerCompose:
99 image: 'redis:7-alpine'
100 ports:
101 - '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:
- 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" → ON
- Toggle "Destroy environment after merge or close pull request" → 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 (e.g., for custom seed scripts), 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, deploy, and seed 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
10bns exec COMPONENT_ID -- npx prisma db seedRemote 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:
1# Forward PostgreSQL to local port 15432
2bns port-forward 15432:5432 --component POSTGRES_COMPONENT_ID
3
4# Connect with psql, TablePlus, Prisma Studio, or any DB tool
5psql -h 127.0.0.1 -p 15432 -U sveltekit -d sveltekit_db
6
7# Forward Redis to local port 16379
8bns port-forward 16379:6379 --component REDIS_COMPONENT_ID
9redis-cli -p 16379Execute SvelteKit Commands
1# Check migration status
2bns exec COMPONENT_ID -- npx prisma migrate status
3
4# Run migrations manually
5bns exec COMPONENT_ID -- npx prisma migrate deploy
6
7# If using Drizzle instead of Prisma:
8bns exec COMPONENT_ID -- npx drizzle-kit migrate
9
10# Open Prisma Studio (forward the port first)
11bns exec COMPONENT_ID -- npx prisma studio --port 5555 &
12bns port-forward 5555:5555 --component COMPONENT_ID
13
14# Seed data
15bns exec COMPONENT_ID -- npx prisma db seed
16
17# Verify ORIGIN is correct
18bns exec COMPONENT_ID -- printenv ORIGIN
19
20# Check the build output
21bns exec COMPONENT_ID -- ls -la build/
22
23# Check Node.js process
24bns exec COMPONENT_ID -- ps auxLive 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# SvelteKit dev server will hot-reload on file changes
4# When done:
5bns remote-development downThis 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
| Issue | Solution |
|---|---|
| 403 Forbidden on form actions | ORIGIN doesn't match the actual URL. This is the #1 SvelteKit issue in preview environments. Run bns exec COMPONENT_ID -- printenv ORIGIN and compare to your browser URL. They must match exactly (protocol + hostname). Use interpolation: 'https://{{ components.sveltekit-app.ingress.hosts[0] }}'. |
| 502 Bad Gateway | SvelteKit server isn't running. Check that HOST=0.0.0.0 is set (not localhost). Verify PORT=3000 matches the ingress servicePort. Check container logs for startup errors. |
| ECONNREFUSED to database | PostgreSQL isn't ready yet. Verify the environment status shows Running. Check that DATABASE_URL uses postgres (the component name) as the host, not localhost. |
| Prisma migration failed | Check that the Prisma schema matches the database engine. Ensure DATABASE_URL includes ?schema=public for PostgreSQL. Run bns exec COMPONENT_ID -- npx prisma migrate status to debug. |
adapter-node not found | @sveltejs/adapter-node is not installed. Add it to devDependencies and update svelte.config.js. Without it, vite build produces static files instead of a Node server. |
build/index.js not found | The build output directory doesn't exist. Check that adapter-node is configured with out: 'build' in svelte.config.js and that npm run build succeeds in the Docker build stage. |
| Session not persisting | SESSION_SECRET not set or changes between deployments. Use SECRET["..."] in environmentVariables so the value stays consistent across redeployments. |
| Static assets 404 | SvelteKit's built static assets should be served automatically by adapter-node. Verify the build/client/ directory exists in the container. |
| 413 Request Entity Too Large | The request body exceeds BODY_SIZE_LIMIT. Increase it in the environment config or set to 0 to disable. Also check the Kubernetes ingress annotation nginx.ingress.kubernetes.io/proxy-body-size. |
| Drizzle migrations not running | Replace npx prisma migrate deploy with npx drizzle-kit migrate in docker-entrypoint.sh. Ensure drizzle-kit is in dependencies (not devDependencies). |
| 522 Connection timed out | Cluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller. |
What's Next?
- Add Redis for sessions — Use a Redis-backed session store for multi-instance deployments with libraries like
svelte-kit-connect-redis - Add Mailpit — Test email sending with a local SMTP server (
axllent/mailpitas a Service component) - Seed test data — Add
npx prisma db seedto yourdocker-entrypoint.shwith a conditional flag - Add MinIO — S3-compatible object storage for file uploads (
minio/minioas a Service component) - Storybook preview — Deploy Storybook alongside your SvelteKit app for component review
- Add Drizzle Studio — Use
npx drizzle-kit studioas a port-forwarded tool for database inspection
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.