Back to Blog
Tutorial12 min read2026-08-07

From Build Success to 502 Errors — Debugging a Go Gin Deployment

Your Go build passes, the image is pushed, but the app never responds. Trace port binding issues, missing health checks, and module path mistakes that break Gin APIs.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

The build logs end with "Successfully tagged...". The deployment status shows "Running." You visit the URL and get a 502 Bad Gateway. The container is up — you can see it in the dashboard — but the ingress can't reach it. No error in the app logs, no crash loop, just silence and a 502.

This is the classic Go deployment failure mode. The binary compiled, the container started, but the HTTP server isn't listening where Kubernetes expects it. Here's how to diagnose and fix it, step by step.

The Gin app that looks correct

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    r.GET("/api/users", func(c *gin.Context) {
        c.JSON(200, gin.H{"users": []string{"alice", "bob"}})
    })

    r.Run() // Defaults to :8080
}

This works locally (go run main.go and visit localhost:8080). You push it, deploy it, and get a 502. What's wrong?

Failure mode 1: The default port is wrong

r.Run() binds to :8080 by default. If your platform expects port 3000 (a common default), the health check probes port 3000, gets no response, marks the pod unhealthy, and removes it from the load balancer.

Fix: read the PORT environment variable and bind explicitly.

import (
    "os"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    r.Run(":" + port)
}

Now the app reads $PORT at startup and binds to that. Deploy with:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "gin-api",
    "repositoryName": "yourorg/gin-api",
    "branch": "main",
    "autoDeploy": true,
    "language": "go",
    "startCommand": "./main",
    "healthCheckPath": "/health",
    "env": [
      { "name": "PORT", "value": "8080" }
    ]
  }'

The health check now hits http://:8080/health, gets a 200, and the pod becomes ready.

Failure mode 2: Binding to localhost instead of all interfaces

Even if the port is correct, Gin's default behaviour might be binding to 127.0.0.1. Unlike 0.0.0.0, this makes the server unreachable from outside the container.

Check the Gin startup logs. If you see:

[GIN] Listening and serving HTTP on 127.0.0.1:8080

You need to force 0.0.0.0. Gin's Run() method accepts an address:

r.Run("0.0.0.0:" + port)

Now the server binds to all interfaces, and the ingress can connect.

Failure mode 3: The binary doesn't exist

The Dockerfile builds the binary, but you forgot to copy it into the final stage. Example:

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .

FROM alpine:latest
WORKDIR /app
# Missing: COPY --from=builder /app/main .
CMD ["./main"]

The CMD runs, but ./main doesn't exist in the final image. The container crashes with "file not found." Add the copy:

COPY --from=builder /app/main .

Failure mode 4: Go module path mismatch

Your go.mod says:

module github.com/yourorg/gin-api

But you import packages as:

import "gin-api/handlers"

The build fails with "cannot find module providing package gin-api/handlers." Go expects imports to match the module path exactly. Fix:

import "github.com/yourorg/gin-api/handlers"

Or change go.mod to:

module gin-api

Either way, the import path and the module name must align.

Failure mode 5: Missing go.sum

If you commit go.mod but not go.sum, the build might fail or download different dependency versions than your local environment. Always commit both:

git add go.mod go.sum
git commit -m "Lock Go dependencies"

A working Dockerfile for Gin

FROM golang:1.22-alpine AS builder

WORKDIR /app

# Copy go.mod and go.sum first (better layer caching)
COPY go.mod go.sum ./
RUN go mod download

# Copy source and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:latest

RUN apk --no-cache add ca-certificates
WORKDIR /app

COPY --from=builder /app/main .

EXPOSE 8080

CMD ["./main"]

Deploy it with:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "gin-api",
    "repositoryName": "yourorg/gin-api",
    "branch": "main",
    "autoDeploy": true,
    "language": "docker",
    "dockerfilePath": "Dockerfile",
    "healthCheckPath": "/health",
    "env": [
      { "name": "PORT", "value": "8080" }
    ]
  }'

The language: "docker" tells the platform to build with the Dockerfile instead of auto-detecting Go and using buildpacks.

Using pandastack.json for consistency

For a repo others might deploy, commit the config:

{
  "type": "container",
  "name": "gin-api",
  "language": "docker",
  "dockerfilePath": "Dockerfile",
  "healthCheckPath": "/health",
  "env": [
    { "key": "PORT", "value": "8080" },
    {
      "key": "DATABASE_URL",
      "description": "PostgreSQL connection string"
    }
  ]
}

Now the deploy button works:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](https://dashboard.pandastack.io/deploy?repo=yourorg/gin-api)

Debugging with the dashboard logs

If the build succeeds but the app never responds, check two log sources:

  1. 1Build logs — shows go build output, Docker image push, Helm install.
  2. 2App logs — shows stdout/stderr from the running container.

If the app logs are empty, the binary isn't running. Common cause: the CMD in the Dockerfile is wrong. Verify it locally:

docker build -t gin-api .
docker run -p 8080:8080 -e PORT=8080 gin-api

If it works locally but fails in the platform, check the environment variables. A missing PORT or DATABASE_URL might cause a silent exit.

Connecting to a managed PostgreSQL database

If the Gin app queries a database, provision it separately and link via DATABASE_URL:

curl -X POST https://api.pandastack.io/v1/databases \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "gin-db",
    "engine": "postgres",
    "version": "16"
  }'

Add the connection to the app:

import (
    "database/sql"
    "os"
    _ "github.com/lib/pq"
)

func main() {
    r := gin.Default()

    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        panic(err)
    }
    defer db.Close()

    r.GET("/users", func(c *gin.Context) {
        var users []string
        rows, _ := db.Query("SELECT name FROM users")
        defer rows.Close()
        for rows.Next() {
            var name string
            rows.Scan(&name)
            users = append(users, name)
        }
        c.JSON(200, gin.H{"users": users})
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    r.Run("0.0.0.0:" + port)
}

Deploy with DATABASE_URL injected:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "gin-api",
    "repositoryName": "yourorg/gin-api",
    "branch": "main",
    "autoDeploy": true,
    "language": "docker",
    "dockerfilePath": "Dockerfile",
    "healthCheckPath": "/health",
    "env": [
      { "name": "PORT", "value": "8080" },
      { "name": "DATABASE_URL", "value": "postgresql://user:pass@host:5432/db?sslmode=require" }
    ]
  }'

Using the CLI

For quick iteration:

panda login
panda projects create \
  --repo yourorg/gin-api \
  --branch main \
  --name gin-api \
  --type container \
  --dockerfile Dockerfile \
  --env PORT=8080 \
  --env DATABASE_URL=$DATABASE_URL

After the first deploy, redeploy with:

panda projects deploy <project-id>

The CLI streams build logs in real-time (though this feature is currently unreliable — use the dashboard for production deploys).

Why the 502 persists even after the fix

If you fix the port binding and redeploy, but the 502 continues:

  1. 1The old pod is still running. Kubernetes might be slow to replace it. Wait 30 seconds and refresh.
  2. 2The health check still fails. Verify /health returns 200 with:

`bash

curl -H "Host: gin-api-abc123.pandastack.app" http://:8080/health

`

If the pod IP is unknown, check the dashboard or use kubectl get pods.

  1. 1The ingress cache is stale. Kong (the ingress controller) caches routes. A redeploy purges this automatically, but manual ingress changes might lag. Contact support if it persists beyond five minutes.

What you've deployed

A Go Gin API running in a container, built with Docker, deployed via Helm, and serving traffic through a Kong ingress. The health check runs against /health, the app binds to 0.0.0.0:8080, and environment variables inject the database connection string.

PandaStack's container builds use rootless BuildKit in ephemeral Kubernetes pods (no host Docker socket), push images to Google Artifact Registry, and deploy via Helm. Free-tier apps run in gVisor (GKE Sandbox) for extra isolation and scale to zero after inactivity. Paid tiers run on stable nodes with no scale-to-zero.

SSL certificates are provisioned automatically via Let's Encrypt. Custom domains are added through the dashboard, and the platform handles DNS and cert renewal.

References

  • [Gin documentation](https://gin-gonic.com/docs/)
  • [Go modules](https://go.dev/ref/mod)
  • [Dockerfile best practices](https://docs.docker.com/develop/dev-best-practices/)
  • [PandaStack container projects](https://docs.pandastack.io/projects/containers/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also