Preview Environments for Go Gin: Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202611 min read

Preview Environments for Go Gin: Automated Per-PR Deployments with Bunnyshell

Why Preview Environments for Go Gin?

Go Gin APIs are fast to build and fast to compile — but they're still as vulnerable as any other stack to the classic "works on my machine" problem. A PR that refactors a database query, adds a new endpoint, or changes GORM migrations looks fine in CI, but when it hits the shared staging server you discover it conflicts with migrations from another branch, or the DATABASE_URL points to the wrong host.

Preview environments solve this. Every pull request gets its own isolated deployment — Go Gin app, PostgreSQL database — 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, no manual steps
  • Production parity — Same static binary, same database engine, same infrastructure as prod
  • Isolation — Each PR environment is fully independent; no shared staging conflicts
  • Automatic cleanup — Environments are destroyed when the PR is merged or closed

Go Gin is an especially good fit for preview environments because of its multi-stage Docker build: the final runtime image contains only the compiled static binary plus a minimal Alpine base, coming in at roughly 15MB. This means preview environments start in under a minute and use minimal cluster resources.

Choose Your Approach

Bunnyshell supports three ways to set up preview environments for Go Gin. 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 Go Gin App

Regardless of which approach you choose, your Go Gin app needs two things: a multi-stage Dockerfile and the right configuration for running behind a Kubernetes ingress.

1. Create a Multi-Stage Dockerfile

The multi-stage approach keeps your final image tiny (~15MB) by using a full Go build environment only for compilation, then copying just the compiled binary into a minimal Alpine runtime:

Dockerfile
1# ── Stage 1: Build ──
2FROM golang:1.22-alpine AS builder
3
4# Install build dependencies (for CGO with lib/pq if needed)
5RUN apk add --no-cache gcc musl-dev
6
7WORKDIR /app
8
9# Download dependencies first (better layer caching)
10COPY go.mod go.sum ./
11RUN go mod download
12
13# Copy source and build
14COPY . .
15RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
16    go build -ldflags="-w -s" -o server ./cmd/server
17
18# ── Stage 2: Runtime ──
19FROM alpine:3.19
20
21# Add CA certificates for HTTPS outbound calls
22RUN apk add --no-cache ca-certificates tzdata
23
24WORKDIR /app
25
26# Copy only the compiled binary
27COPY --from=builder /app/server .
28
29# Run as non-root user
30RUN addgroup -S appgroup && adduser -S appuser -G appgroup
31USER appuser
32
33EXPOSE 8080
34CMD ["./server"]

This produces an image of approximately 15MB — dramatically smaller than a golang:1.22 image (~800MB) or even python:3.12-slim (~150MB). Each preview environment uses less disk on your cluster and pulls faster.

Important: The app must listen on 0.0.0.0, not 127.0.0.1. Use the PORT environment variable (defaulting to 8080) so Bunnyshell can inject the correct port if needed.

If you use lib/pq with CGO enabled (CGO_ENABLED=1), you need gcc and musl-dev in the builder stage and must link against musl statically. For a fully static binary (no Alpine needed), use pgx/v5 with CGO_ENABLED=0 instead — it's a pure Go PostgreSQL driver.

2. Configure Go Gin for Kubernetes

Gin running behind a Kubernetes ingress needs two things: release mode and proper handling of X-Forwarded-Proto headers.

Go
1// main.go
2package main
3
4import (
5    "log"
6    "os"
7
8    "github.com/gin-gonic/gin"
9    "gorm.io/driver/postgres"
10    "gorm.io/gorm"
11)
12
13func main() {
14    // Use release mode in production — disables debug logging
15    gin.SetMode(gin.ReleaseMode)
16
17    port := os.Getenv("PORT")
18    if port == "" {
19        port = "8080"
20    }
21
22    // Database connection via DATABASE_URL or individual vars
23    dsn := os.Getenv("DATABASE_URL")
24    if dsn == "" {
25        dsn = buildDSN()
26    }
27
28    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
29    if err != nil {
30        log.Fatalf("failed to connect to database: %v", err)
31    }
32
33    // Run auto-migrations (or use golang-migrate for versioned migrations)
34    if err := db.AutoMigrate(&User{}, &Post{}); err != nil {
35        log.Fatalf("failed to run migrations: %v", err)
36    }
37
38    r := gin.New()
39    r.Use(gin.Logger(), gin.Recovery())
40
41    // Trust the Kubernetes ingress as a proxy
42    // This makes c.Request.TLS populated correctly and ensures
43    // redirect URLs use https:// rather than http://
44    if err := r.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}); err != nil {
45        log.Fatalf("failed to set trusted proxies: %v", err)
46    }
47
48    // Health check endpoint (Kubernetes liveness/readiness probe)
49    r.GET("/healthz", func(c *gin.Context) {
50        c.JSON(200, gin.H{"status": "ok"})
51    })
52
53    // Your routes here
54    api := r.Group("/api/v1")
55    {
56        api.GET("/users", listUsers(db))
57        api.POST("/users", createUser(db))
58    }
59
60    log.Printf("Starting server on :%s", port)
61    if err := r.Run("0.0.0.0:" + port); err != nil {
62        log.Fatalf("failed to start server: %v", err)
63    }
64}
65
66func buildDSN() string {
67    host := os.Getenv("DB_HOST")
68    if host == "" {
69        host = "localhost"
70    }
71    port := os.Getenv("DB_PORT")
72    if port == "" {
73        port = "5432"
74    }
75    user := os.Getenv("DB_USER")
76    if user == "" {
77        user = "gin"
78    }
79    password := os.Getenv("DB_PASSWORD")
80    name := os.Getenv("DB_NAME")
81    if name == "" {
82        name = "gin_db"
83    }
84    return "host=" + host + " port=" + port + " user=" + user +
85        " password=" + password + " dbname=" + name + " sslmode=disable"
86}

gin.SetMode(gin.ReleaseMode) must be called before gin.New() or gin.Default(). In release mode, Gin omits debug route printing and uses more efficient logging. If you forget this, your preview environment logs will be flooded with debug output.

Go Gin Deployment Checklist

  • Multi-stage Dockerfile with golang:1.22-alpine builder + alpine:3.19 runtime
  • gin.SetMode(gin.ReleaseMode) called before router initialization
  • r.SetTrustedProxies(...) configured for Kubernetes pod CIDR ranges
  • App listens on 0.0.0.0:8080 (not localhost or 127.0.0.1)
  • PORT env var respected with fallback to 8080
  • DATABASE_URL or individual DB_* vars configured
  • GIN_MODE=release set in Kubernetes environment
  • /healthz endpoint for Kubernetes liveness/readiness probes
  • CGO_ENABLED=0 for a fully static binary (recommended)

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., "Go Gin API")
  3. Inside the project, click Create environment and name it (e.g., "gin-main")

Step 2: Define the Environment Configuration

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

YAML
1kind: Environment
2name: gin-preview
3type: primary
4
5environmentVariables:
6  DB_PASSWORD: SECRET["your-db-password"]
7
8components:
9  # ── Go Gin Application ──
10  - kind: Application
11    name: gin-app
12    gitRepo: 'https://github.com/your-org/your-gin-repo.git'
13    gitBranch: main
14    gitApplicationPath: /
15    dockerCompose:
16      build:
17        context: .
18        dockerfile: Dockerfile
19      environment:
20        GIN_MODE: release
21        PORT: '8080'
22        DATABASE_URL: 'postgres://gin:{{ env.vars.DB_PASSWORD }}@postgres:5432/gin_db?sslmode=disable'
23      ports:
24        - '8080:8080'
25    hosts:
26      - hostname: 'api-{{ env.base_domain }}'
27        path: /
28        servicePort: 8080
29    dependsOn:
30      - postgres
31
32  # ── PostgreSQL Database ──
33  - kind: Database
34    name: postgres
35    dockerCompose:
36      image: 'postgres:16-alpine'
37      environment:
38        POSTGRES_DB: gin_db
39        POSTGRES_USER: gin
40        POSTGRES_PASSWORD: '{{ env.vars.DB_PASSWORD }}'
41      ports:
42        - '5432:5432'
43
44volumes:
45  - name: postgres-data
46    mount:
47      component: postgres
48      containerPath: /var/lib/postgresql/data
49    size: 1Gi

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

Because GORM AutoMigrate runs at startup, your schema is always up to date when the preview environment starts. If you use golang-migrate instead, add a migration init container or run bns exec COMPONENT_ID -- ./migrate -path ./migrations -database "$DATABASE_URL" up after deploy.

Step 3: Deploy

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

  1. Build your Go Gin Docker image using the multi-stage Dockerfile (~15MB final image)
  2. Pull the PostgreSQL image
  3. Deploy everything into an isolated Kubernetes namespace
  4. Generate HTTPS URLs automatically with DNS

The build stage compiles your Go binary inside the cluster. The final image is pulled from the registry and started in seconds. Monitor the deployment in the environment detail page. When status shows Running, click Endpoints to access your live API.

Step 4: Verify the Deployment

Bash
1export BUNNYSHELL_TOKEN=your-api-token
2
3# Get component IDs
4bns components list --environment ENV_ID --output json | jq '._embedded.item[] | {id, name}'
5
6# Check health endpoint
7curl https://api-$(bns environments get ENV_ID --output json | jq -r '.baseDomain')/healthz
8
9# Run a database connectivity check
10bns exec COMPONENT_ID -- ./server --check-db

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 (Go recompiles in the build stage)
  • Bunnyshell posts a comment on the PR with a link to the live API
  • Merge or close the PR → The ephemeral environment is automatically destroyed

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

YAML
1version: '3.8'
2
3services:
4  gin-app:
5    build:
6      context: .
7      dockerfile: Dockerfile
8    ports:
9      - '8080:8080'
10    environment:
11      GIN_MODE: debug
12      PORT: '8080'
13      DATABASE_URL: 'postgres://gin:gin@postgres:5432/gin_db?sslmode=disable'
14    depends_on:
15      postgres:
16        condition: service_healthy
17
18  postgres:
19    image: postgres:16-alpine
20    environment:
21      POSTGRES_DB: gin_db
22      POSTGRES_USER: gin
23      POSTGRES_PASSWORD: gin
24    volumes:
25      - postgres-data:/var/lib/postgresql/data
26    healthcheck:
27      test: ["CMD-SHELL", "pg_isready -U gin -d gin_db"]
28      interval: 5s
29      timeout: 5s
30      retries: 5
31
32volumes:
33  postgres-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 (gin-app, postgres)
  • Exposed ports
  • Build configurations (Dockerfile, multi-stage is fully supported)
  • Volumes
  • Environment variables

It converts everything into a bunnyshell.yaml environment definition.

Important: 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
  • Switch GIN_MODE from debug to release
  • Update DATABASE_URL using Bunnyshell interpolation:
YAML
DATABASE_URL: 'postgres://gin:{{ env.vars.DB_PASSWORD }}@postgres:5432/gin_db?sslmode=disable'
GIN_MODE: release
  • Add the hosts block so Bunnyshell generates the ingress URL:
YAML
1hosts:
2  - hostname: 'api-{{ env.base_domain }}'
3    path: /
4    servicePort: 8080

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

  • Kubernetes doesn't guarantee depends_on ordering — The healthcheck condition in Docker Compose isn't respected in Kubernetes. Make your Go app retry database connections on startup. GORM's connection pooling handles transient failures gracefully, but you can add an explicit retry:
Go
1// Retry database connection up to 10 times with exponential backoff
2var db *gorm.DB
3var err error
4for i := 0; i < 10; i++ {
5    db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
6    if err == nil {
7        break
8    }
9    log.Printf("DB not ready (attempt %d/10): %v", i+1, err)
10    time.Sleep(time.Duration(i+1) * 2 * time.Second)
11}
12if err != nil {
13    log.Fatalf("could not connect to database after 10 attempts: %v", err)
14}
  • Use Bunnyshell interpolation for dynamic API base URLs in frontend configs:
YAML
# Bunnyshell environment config (after import)
API_BASE_URL: 'https://{{ components.gin-app.ingress.hosts[0] }}'

Approach C: Helm Charts

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

Step 1: Create a Helm Chart

Structure your Go Gin Helm chart in your repo:

Text
1helm/gin/
2├── Chart.yaml
3├── values.yaml
4└── templates/
5    ├── deployment.yaml
6    ├── service.yaml
7    ├── ingress.yaml
8    └── configmap.yaml

A minimal values.yaml:

YAML
1replicaCount: 1
2image:
3  repository: ""
4  tag: latest
5service:
6  port: 8080
7ingress:
8  enabled: true
9  className: bns-nginx
10  host: ""
11env:
12  GIN_MODE: release
13  PORT: "8080"
14  DATABASE_URL: ""
15resources:
16  requests:
17    memory: "32Mi"
18    cpu: "50m"
19  limits:
20    memory: "128Mi"
21    cpu: "500m"
22livenessProbe:
23  httpGet:
24    path: /healthz
25    port: 8080
26  initialDelaySeconds: 5
27readinessProbe:
28  httpGet:
29    path: /healthz
30    port: 8080
31  initialDelaySeconds: 3

Because the final Go Gin image is ~15MB and the binary starts in milliseconds, you can set very aggressive liveness and readiness probe intervals. This means Kubernetes detects healthy pods faster, reducing the time to green for each preview environment deploy.

Step 2: Define the Bunnyshell Configuration

Create a bunnyshell.yaml using Helm components:

YAML
1kind: Environment
2name: gin-helm
3type: primary
4
5environmentVariables:
6  DB_PASSWORD: SECRET["your-db-password"]
7  POSTGRES_DB: gin_db
8  POSTGRES_USER: gin
9
10components:
11  # ── Docker Image Build ──
12  - kind: DockerImage
13    name: gin-image
14    context: /
15    dockerfile: Dockerfile
16    gitRepo: 'https://github.com/your-org/your-gin-repo.git'
17    gitBranch: main
18    gitApplicationPath: /
19
20  # ── PostgreSQL via Helm ──
21  - kind: Helm
22    name: postgres
23    runnerImage: 'dtzar/helm-kubectl:3.8.2'
24    deploy:
25      - |
26        cat << EOF > pg_values.yaml
27          global:
28            storageClass: bns-network-sc
29          auth:
30            postgresPassword: {{ env.vars.DB_PASSWORD }}
31            database: {{ env.vars.POSTGRES_DB }}
32        EOF
33      - 'helm repo add bitnami https://charts.bitnami.com/bitnami'
34      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
35        --post-renderer /bns/helpers/helm/bns_post_renderer
36        -f pg_values.yaml postgres bitnami/postgresql --version 11.9.11'
37      - |
38        POSTGRES_HOST="postgres-postgresql.{{ env.k8s.namespace }}.svc.cluster.local"
39    destroy:
40      - 'helm uninstall postgres --namespace {{ env.k8s.namespace }}'
41    start:
42      - 'kubectl scale --replicas=1 --namespace {{ env.k8s.namespace }}
43        statefulset/postgres-postgresql'
44    stop:
45      - 'kubectl scale --replicas=0 --namespace {{ env.k8s.namespace }}
46        statefulset/postgres-postgresql'
47    exportVariables:
48      - POSTGRES_HOST
49
50  # ── Go Gin App via Helm ──
51  - kind: Helm
52    name: gin-app
53    runnerImage: 'dtzar/helm-kubectl:3.8.2'
54    deploy:
55      - |
56        cat << EOF > gin_values.yaml
57          replicaCount: 1
58          image:
59            repository: {{ components.gin-image.image }}
60          service:
61            port: 8080
62          ingress:
63            enabled: true
64            className: bns-nginx
65            host: api-{{ env.base_domain }}
66          env:
67            GIN_MODE: release
68            PORT: "8080"
69            DATABASE_URL: 'postgres://{{ env.vars.POSTGRES_USER }}:{{ env.vars.DB_PASSWORD }}@{{ components.postgres.exported.POSTGRES_HOST }}:5432/{{ env.vars.POSTGRES_DB }}?sslmode=disable'
70        EOF
71      - 'helm upgrade --install --namespace {{ env.k8s.namespace }}
72        --post-renderer /bns/helpers/helm/bns_post_renderer
73        -f gin_values.yaml gin-{{ env.unique }} ./helm/gin'
74    destroy:
75      - 'helm uninstall gin-{{ env.unique }} --namespace {{ env.k8s.namespace }}'
76    start:
77      - 'helm upgrade --namespace {{ env.k8s.namespace }}
78        --post-renderer /bns/helpers/helm/bns_post_renderer
79        --reuse-values --set replicaCount=1 gin-{{ env.unique }} ./helm/gin'
80    stop:
81      - 'helm upgrade --namespace {{ env.k8s.namespace }}
82        --post-renderer /bns/helpers/helm/bns_post_renderer
83        --reuse-values --set replicaCount=0 gin-{{ env.unique }} ./helm/gin'
84    gitRepo: 'https://github.com/your-org/your-gin-repo.git'
85    gitBranch: main
86    gitApplicationPath: /helm/gin

Key: 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 API
  • 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.

The Go multi-stage build runs inside the cluster during each deploy. Because Go compilation is deterministic and Docker layer caching is used, subsequent builds (e.g., when you push a new commit to a PR) are fast — only changed files are recompiled.

Optional: CI/CD Integration via CLI

If you prefer to control preview environments from your CI/CD pipeline (e.g., to run post-deploy smoke tests or apply golang-migrate migrations), 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 run migrations 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 golang-migrate migrations (if not using GORM AutoMigrate)
12bns exec COMPONENT_ID -- ./migrate -path ./migrations -database "$DATABASE_URL" up
13
14# Run API smoke tests against the preview environment
15bns exec COMPONENT_ID -- go test ./tests/integration/... -tags=integration

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
5psql -h localhost -p 15432 -U gin gin_db
6
7# Forward the API to local port 18080 for direct testing
8bns port-forward 18080:8080 --component GIN_COMPONENT_ID
9
10# Hit the API locally
11curl http://localhost:18080/healthz
12curl http://localhost:18080/api/v1/users

Execute Commands in the Container

Bash
1# Run a health check
2bns exec COMPONENT_ID -- ./server --check-db
3
4# Apply golang-migrate migrations manually
5bns exec COMPONENT_ID -- ./migrate -path ./migrations -database "$DATABASE_URL" up
6
7# Show current migration version
8bns exec COMPONENT_ID -- ./migrate -path ./migrations -database "$DATABASE_URL" version
9
10# Open a shell (Alpine has sh, not bash)
11bns exec COMPONENT_ID -- sh
12
13# Check binary size (highlights the ~15MB image benefit)
14bns exec COMPONENT_ID -- ls -lh ./server

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 Go files locally — changes sync automatically
3# Note: Go requires recompilation; use Air for hot-reload in development mode
4bns remote-development down

For live code sync to work with Go hot-reload, include Air in your development Dockerfile and use a separate Dockerfile.dev for remote development. Air watches for file changes and recompiles automatically. Your production Dockerfile should remain the multi-stage build without Air.


Troubleshooting

IssueSolution
502 Bad GatewayGin isn't listening on 0.0.0.0:8080. Check your server startup: r.Run("0.0.0.0:" + port) — never localhost.
Mixed content / HTTPS issuesConfigure r.SetTrustedProxies(...) with your cluster CIDR. Without this, Gin doesn't trust X-Forwarded-Proto.
Redirect loops on HTTPSEnsure SetTrustedProxies includes the ingress controller's IP range. Gin uses X-Forwarded-Proto only for trusted proxies.
dial tcp: connection refused to postgresPostgreSQL isn't ready yet. Add a retry loop in your main function before AutoMigrate.
GORM AutoMigrate failsCheck that DATABASE_URL uses postgres (the component name) as the host, not localhost.
golang-migrate: no changeEnsure migration files are embedded in the binary with //go:embed or copied into the image.
Build fails: go: no module providingRun go mod tidy locally and commit the updated go.sum. The build stage runs go mod download.
Image build slowgo.mod and go.sum are copied before source files — this enables Docker layer caching for dependencies. Ensure you haven't broken this order.
Container exits immediatelyThe binary panicked on startup. Add log.Fatalf error handling around DB connection and r.Run().
522 Connection timed outCluster may be behind a firewall. Verify Cloudflare IPs are whitelisted on the ingress controller.

What's Next?

  • Add Redis for caching — Add a Redis component and use go-redis/redis for caching hot query results in preview environments
  • Add a worker process — Add another Application component for background job processing (e.g., using gocraft/work or riverqueue/river)
  • Integration test against preview URLs — In your CI, hit the Bunnyshell API to get the preview URL and run go test ./tests/integration/... against it
  • Monitor with Sentry — Pass SENTRY_DSN as an environment variable and use getsentry/sentry-go for error tracking in preview environments
  • Optimize build caching — Use Bunnyshell's registry to push built images and skip the build step on unchanged branches

Ship faster starting today.

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