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://, 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:8080You 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-apiBut 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-apiEither 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:
[](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:
- 1Build logs — shows
go buildoutput, Docker image push, Helm install. - 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-apiIf 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_URLAfter 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:
- 1The old pod is still running. Kubernetes might be slow to replace it. Wait 30 seconds and refresh.
- 2The health check still fails. Verify
/healthreturns 200 with:
`bash
curl -H "Host: gin-api-abc123.pandastack.app" http://
`
If the pod IP is unknown, check the dashboard or use kubectl get pods.
- 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/)