Right, so you’ve built your beautiful, elegant Go microservice. It’s probably a tiny, efficient little stateless ninja. Now we have to drop it into the jungle that is Kubernetes, where things get eaten if they don’t know the local customs. Deploying to k8s isn’t just about making it run; it’s about making it thrive and, more importantly, making it debuggable at 3 AM when everything is on fire.

The absolute bedrock of this is your Dockerfile. This isn’t just a box to ship your code; it’s the blueprint for your application’s runtime existence. We’re going to do this the right way, not the “it works on my machine” way, which in Kubernetes terms means “it fails mysteriously on everyone else’s cluster.”

Building a Slim, Secure Go Container

We’re using a multi-stage build. Why? Because your final container image doesn’t need the entire Go toolchain, all the source code, and the kitchen sink. It just needs the compiled binary. Shipping a full golang image is like moving a single book in a shipping container. It’s absurd.

# Build stage: where we compile the thing
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /usr/bin/my-service ./cmd/my-service

# Final stage: where we run the thing
FROM scratch

# Copy the CA certificates so your service can actually talk to the outside world (like a database)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy our compiled binary from the builder stage
COPY --from=builder /usr/bin/my-service /usr/bin/my-service

# Use a non-root user for security. K8s will thank you.
USER 1000:1000

ENTRYPOINT ["/usr/bin/my-service"]

Look at that final image. It’s built from scratch. It’s microscopic. It has exactly what it needs: the binary and the root certificates. This is a security and efficiency win. Alpine is a fine alternative to scratch if you need a shell for debugging, but for production, you can’t beat the sheer minimalism of this.

Your Kubernetes Deployment Manifest

Now, let’s tell Kubernetes how to run our beautifully slim container. This YAML is where you stop being a developer and start being a systems architect. Every line here matters.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
  labels:
    app: my-service
spec:
  replicas: 3  # Because one is none. Two is one. Three is a party.
  selector:
    matchLabels:
      app: my-service
  template:
    metadata:
      labels:
        app: my-service
    spec:
      containers:
      - name: my-service
        image: your-registry.com/your-project/my-service:1.0.0 # Use real tags, never `latest`.
        ports:
        - containerPort: 8080  # The port your Go HTTP server actually listens on
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: my-service-secrets
              key: database-url
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"  # That's 0.1 CPU core. Your Go service is efficient, remember?
          limits:
            memory: "128Mi"
            cpu: "250m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5  # Give the Go binary a second to start listening
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 5
        # This is CRITICAL for graceful shutdowns in Go
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"] # Give connections time to drain

Let’s talk about the stars of this show. The livenessProbe tells k8s if your app is alive (i.e., not deadlocked). The readinessProbe tells k8s if your app is ready to serve traffic (i.e., connected to its database, warmed up). Your Go service must implement these endpoints. It’s not optional.

And the preStop hook? That’s the polite way to shut down. When k8s decides to terminate your pod, it sends a SIGTERM to your Go process. Your server should catch this signal, stop accepting new connections, and finish processing existing ones. But the default grace period is only 30 seconds. That sleep 15 gives your code a fighting chance to finish what it’s doing before k8s gets impatient and sends a SIGKILL. It’s the difference between a graceful bow and the stage lights being cut mid-sentence.

The Service and The Ingress

A Deployment manages your pods. A Service gives them a stable network identity. An Ingress is how external traffic gets in.

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080  # Matches the `containerPort` from the Deployment
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-service-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: api.my-awesome-company.com
    http:
      paths:
      - path: /my-service/
        pathType: Prefix
        backend:
          service:
            name: my-service
            port:
              number: 80

The Service is straightforward: it finds all pods with the label app: my-service and load balances between them. The Ingress is where the real-world rubber meets the road, and its configuration is heavily dependent on your ingress controller (nginx, traefik, etc.). This example sets up a simple path-based routing.

The most common pitfall here? Forgetting that your application might need to know its public context path. A request coming in will be to /my-service/some/endpoint, but your Go HTTP router might be configured for /some/endpoint. You need to either handle this rewrite in the ingress (as the annotation suggests) or make your Go application aware of its root path. This trips up everyone. Everyone.

Deploying to Kubernetes is a dialogue between you and the system. You state your requirements and intentions clearly in YAML, and k8s does its best to meet them. The key is to be explicit, to think about the entire lifecycle of your application, and to never, ever assume anything about the network. Now go ship it.