Skip to main content

Deploying Go Applications to Kubernetes

Published: May 8, 2026 Updated: August 28, 2026 Larry Qu 7 min read

Kubernetes has become the standard deployment platform for Go microservices. This guide covers everything from a production-ready Dockerfile to Kubernetes manifests, health checks, autoscaling, and using the client-go library to interact with Kubernetes from within your Go code.

Production Dockerfile for Go

A well-structured multi-stage Dockerfile produces minimal, secure images:

# Stage 1: Build
FROM golang:1.22-bookworm AS builder

WORKDIR /app

# Cache dependencies separately from source code
COPY go.mod go.sum ./
RUN go mod download

# Copy source and build
COPY . .
# CGO_ENABLED=0 produces a fully static binary — no libc required
# -ldflags "-s -w" strips debug symbols (reduces binary ~30%)
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -X main.version=$(git describe --tags --always)" \
    -o /app/server ./cmd/server

# Stage 2: Runtime — distroless has no shell, no package manager
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /app/server /server

# Run as non-root (distroless:nonroot uses uid 65532)
USER nonroot:nonroot

EXPOSE 8080

ENTRYPOINT ["/server"]

Build and push:

docker build -t myregistry/myapp:$(git rev-parse --short HEAD) .
docker push myregistry/myapp:$(git rev-parse --short HEAD)

Health Check Endpoints

Kubernetes uses three probes. Implement all three properly:

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "net/http"
    "sync/atomic"
    "time"
)

type HealthChecker struct {
    db      *sql.DB
    started atomic.Bool
    ready   atomic.Bool
}

func (h *HealthChecker) Startup(w http.ResponseWriter, r *http.Request) {
    // Startup probe: runs during initialization, stops once started
    // Kubernetes won't send liveness/readiness probes until this passes
    if !h.started.Load() {
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]string{"status": "starting"})
        return
    }
    json.NewEncoder(w).Encode(map[string]string{"status": "started"})
}

func (h *HealthChecker) Live(w http.ResponseWriter, r *http.Request) {
    // Liveness: is this process healthy? If not, Kubernetes restarts the pod.
    // Don't check external dependencies here — only internal process health.
    json.NewEncoder(w).Encode(map[string]string{
        "status": "alive",
        "time":   time.Now().UTC().Format(time.RFC3339),
    })
}

func (h *HealthChecker) Ready(w http.ResponseWriter, r *http.Request) {
    // Readiness: can this instance handle traffic?
    // If not, Kubernetes removes it from the Service endpoints (no traffic routed).
    if !h.ready.Load() {
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]string{"status": "not ready"})
        return
    }

    // Check DB connectivity
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()
    if err := h.db.PingContext(ctx); err != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]string{"status": "db unavailable"})
        return
    }

    json.NewEncoder(w).Encode(map[string]string{"status": "ready"})
}

func main() {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    health := &HealthChecker{db: db}

    mux := http.NewServeMux()
    mux.HandleFunc("/health/startup", health.Startup)
    mux.HandleFunc("/health/live",    health.Live)
    mux.HandleFunc("/health/ready",   health.Ready)
    mux.HandleFunc("/api/",           apiHandler)

    // Mark ready after startup tasks
    go func() {
        connectDB(db)
        runMigrations(db)
        health.started.Store(true)
        health.ready.Store(true)
    }()

    http.ListenAndServe(":8080", mux)
}

Kubernetes Manifests

Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
  labels:
    app: order-service
    version: "1.0"
spec:
  replicas: 3
  # Zero-downtime rolling update
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1    # at most 1 pod unavailable during update
      maxSurge: 1          # at most 1 extra pod during update
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
        version: "1.0"
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: "/metrics"
        prometheus.io/port: "8080"
    spec:
      # Graceful shutdown: allow in-flight requests to complete
      terminationGracePeriodSeconds: 30

      containers:
      - name: order-service
        image: myregistry/order-service:abc1234
        ports:
        - containerPort: 8080
          name: http

        env:
        - name: PORT
          value: "8080"
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: order-service-config
              key: log_level
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: order-service-secrets
              key: database_url

        # Resource management — always set both requests and limits
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"

        # Startup probe — checks during slow initialization
        startupProbe:
          httpGet:
            path: /health/startup
            port: 8080
          failureThreshold: 30    # 30 × 10s = 5 minutes max startup time
          periodSeconds: 10

        # Liveness — restart if dead
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          initialDelaySeconds: 0
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3

        # Readiness — stop sending traffic if not ready
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 0
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

        # Graceful shutdown signal
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sleep", "5"] # Allow load balancer to drain

      # Spread pods across nodes for high availability
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: order-service

Service

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: production
spec:
  selector:
    app: order-service
  ports:
  - name: http
    port: 80
    targetPort: 8080
    protocol: TCP
  type: ClusterIP  # Internal only; use Ingress for external access

ConfigMap and Secrets

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
  namespace: production
data:
  log_level: "info"
  environment: "production"
  max_connections: "25"
---
# secrets.yaml — create with kubectl, never commit to git
# kubectl create secret generic order-service-secrets \
#   --from-literal=database_url="postgres://..." \
#   --from-literal=jwt_secret="..."
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secrets
  namespace: production
type: Opaque
# Values are base64-encoded in the manifest
# NEVER commit real secrets — create them with kubectl or a secrets operator

Ingress

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-service
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /api/orders
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 80

Horizontal Pod Autoscaler

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # wait 5 min before scaling down
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Pods
        value: 4
        periodSeconds: 60

Using client-go from Go Code

When your Go service needs to interact with Kubernetes (operator pattern, custom controllers):

go get k8s.io/[email protected]
go get k8s.io/[email protected]
go get k8s.io/[email protected]

In-Cluster Config (Running Inside Kubernetes)

import (
    "context"
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
)

func newInClusterClient() (*kubernetes.Clientset, error) {
    // Uses the service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/
    config, err := rest.InClusterConfig()
    if err != nil {
        return nil, fmt.Errorf("in-cluster config: %w", err)
    }
    return kubernetes.NewForConfig(config)
}

func listPods(ctx context.Context, client *kubernetes.Clientset, namespace string) error {
    pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
        LabelSelector: "app=order-service",
    })
    if err != nil {
        return fmt.Errorf("listing pods: %w", err)
    }

    for _, pod := range pods.Items {
        fmt.Printf("Pod: %s, Phase: %s, Node: %s\n",
            pod.Name, pod.Status.Phase, pod.Spec.NodeName)
    }
    return nil
}

Watching Resources

import (
    "k8s.io/apimachinery/pkg/watch"
    corev1 "k8s.io/api/core/v1"
)

func watchDeployments(ctx context.Context, client *kubernetes.Clientset, namespace string) {
    watcher, err := client.AppsV1().Deployments(namespace).Watch(ctx, metav1.ListOptions{})
    if err != nil {
        log.Fatalf("watch error: %v", err)
    }
    defer watcher.Stop()

    for event := range watcher.ResultChan() {
        switch event.Type {
        case watch.Added:
            fmt.Printf("Deployment added: %s\n", event.Object.(metav1.Object).GetName())
        case watch.Modified:
            fmt.Printf("Deployment modified: %s\n", event.Object.(metav1.Object).GetName())
        case watch.Deleted:
            fmt.Printf("Deployment deleted: %s\n", event.Object.(metav1.Object).GetName())
        }
    }
}

Out-of-Cluster Config (Development)

import "k8s.io/client-go/tools/clientcmd"

func newOutOfClusterClient() (*kubernetes.Clientset, error) {
    // Uses ~/.kube/config
    loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
    configOverrides := &clientcmd.ConfigOverrides{}
    kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
        loadingRules, configOverrides,
    )
    config, err := kubeConfig.ClientConfig()
    if err != nil {
        return nil, err
    }
    return kubernetes.NewForConfig(config)
}

Essential kubectl Commands for Go Services

# Deploy
kubectl apply -f deployment.yaml -f service.yaml

# Watch rollout
kubectl rollout status deployment/order-service -n production

# View logs (all replicas)
kubectl logs -l app=order-service -n production --follow

# Get events (for debugging crashes)
kubectl describe pod <pod-name> -n production

# Scale manually
kubectl scale deployment/order-service --replicas=5 -n production

# Rolling update: change image
kubectl set image deployment/order-service order-service=myregistry/order-service:v2.0

# Rollback to previous version
kubectl rollout undo deployment/order-service -n production

# Port forward for local debugging
kubectl port-forward svc/order-service 8080:80 -n production

# Execute command in pod
kubectl exec -it <pod-name> -n production -- /bin/sh

# View resource usage
kubectl top pods -n production -l app=order-service

# Check HPA status
kubectl get hpa -n production

RBAC for client-go

When your Go service uses client-go, it needs Kubernetes RBAC permissions:

# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service
  namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-service-role
  namespace: production
rules:
- apiGroups: [""]
  resources: ["pods", "services"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-service-binding
  namespace: production
subjects:
- kind: ServiceAccount
  name: order-service
  namespace: production
roleRef:
  kind: Role
  name: order-service-role
  apiGroup: rbac.authorization.k8s.io

Summary

  • Use multi-stage Docker builds with distroless or alpine for minimal, secure images
  • Always set both requests and limits on CPU and memory
  • Implement all three health probes: startup, liveness, and readiness — each serves a different purpose
  • Use terminationGracePeriodSeconds + preStop hook for zero-downtime deployments
  • Add HPA for production services — Kubernetes won’t auto-scale without it
  • Use client-go with in-cluster config for operators and controllers running inside Kubernetes

Resources

Comments

👍 Was this article helpful?