41.7 Deploying Go Microservices to Kubernetes
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.