How to Write Kubernetes Manifests for a Go Service

Create YAML manifests for Deployment and Service resources, then apply them using kubectl to run your Go service in Kubernetes.

The problem with running Go binaries in production

You compile a Go binary. It runs fine on your laptop. You push it to a remote server, and it works until the server reboots, or the process crashes, or you need to scale to handle a traffic spike. Running a single process on a single machine is fine for a script. It is not fine for a service. Kubernetes solves this by treating your binary as a disposable unit that can be restarted, replaced, or multiplied without manual intervention. The bridge between your compiled Go executable and that self-healing infrastructure is a set of YAML files.

What Kubernetes actually does with your YAML

Think of Kubernetes as a restaurant kitchen. Your Go binary is the chef. The deployment manifest is the head chef's order ticket. It tells the kitchen exactly how many chefs to station at the grill, what tools they need, and what happens if one drops a pan. The service manifest is the waiter. It does not cook. It takes orders from the dining room and routes them to whichever chef is currently standing at the right station. The waiter does not care if Chef A or Chef B handles the order, as long as the ticket matches the station.

Kubernetes does not execute your Go binary directly. It hands the manifest to a container runtime, which pulls an image, creates an isolated filesystem, and starts the process. The control plane watches the running state, compares it to your YAML, and fixes any drift. If a node dies, the scheduler places a replacement pod on another node. If your Go program panics and exits, the container runtime restarts it. You define the desired state. Kubernetes fights to maintain it.

Define your state clearly. The control plane only knows what you tell it.

The minimal deployment manifest

Here is the simplest deployment that runs a Go service, sets resource boundaries, and tells Kubernetes how to identify the pods.

apiVersion: apps/v1
kind: Deployment
# declares the API version and resource type
metadata:
  name: go-service
  # unique identifier within the namespace
spec:
  replicas: 1
  # target number of running pods
  selector:
    matchLabels:
      app: go-service
  # tells the deployment which pods it owns
  template:
    metadata:
      labels:
        app: go-service
      # must match the selector above
    spec:
      containers:
      - name: go-service
        image: my-registry/go-service:v1.2.0
        # never use latest in production
        ports:
        - containerPort: 8080
        # documents the port the binary listens on
        resources:
          limits:
            cpu: "500m"
            memory: "128Mi"
          requests:
            cpu: "250m"
            memory: "64Mi"

Apply the manifest with kubectl apply -f deployment.yaml. The API server validates the syntax, stores it in etcd, and the deployment controller begins reconciling the state.

Labels are the glue that holds Kubernetes objects together. The community standard is app.kubernetes.io/name instead of app, but both work. Stick to one convention across your team.

How the control loop reads your configuration

When you run kubectl apply, the request hits the API server. The server checks permissions, validates the schema, and writes the object to etcd. The deployment controller watches etcd for new or updated Deployment objects. It notices replicas: 1 and creates a ReplicaSet. The ReplicaSet controller then creates a Pod object matching the template.

The scheduler picks up the unscheduled Pod. It evaluates node resources, taints, tolerations, and affinity rules. It assigns the Pod to a node that fits. The kubelet on that node receives the assignment, pulls the container image from the registry, and starts the container. Your Go binary runs as PID 1 inside the container.

If the container exits with a non-zero code, the kubelet restarts it. If it restarts too many times in a short window, the pod enters a CrashLoopBackOff state. The control loop does not guess why your program failed. It only knows the container stopped. You must add health checks to tell Kubernetes when the process is actually ready to handle traffic.

Health checks are not optional. A crashing pod will exhaust your node resources before you notice.

Exposing the service to the network

Pods get ephemeral IP addresses. They disappear when the pod dies. You need a stable network endpoint that routes traffic to whatever pod is currently running. That is what a Service does.

Here is the standard service manifest that fronts the deployment.

apiVersion: v1
kind: Service
# creates a stable network endpoint
metadata:
  name: go-service
  # DNS name inside the cluster
spec:
  selector:
    app: go-service
  # routes traffic to pods with this label
  ports:
  - protocol: TCP
    port: 80
    # the port other services connect to
    targetPort: 8080
  # the port the Go binary actually listens on
  type: ClusterIP
  # keeps the service internal to the cluster

Apply it with kubectl apply -f service.yaml. Kubernetes creates a virtual IP and configures kube-proxy to forward traffic matching port 80 to the targetPort on any pod that matches the selector. Other services inside the cluster can reach your Go program at http://go-service:80. The DNS name resolves automatically.

The selector on the Service must match the labels on the Pod template. If they do not match, the service has no endpoints and returns connection refused.

When things go wrong

Kubernetes errors are usually straightforward but verbose. The API server rejects invalid manifests with error validating data: [spec.template.spec.containers[0].resources.limits.memory: Invalid value: "128M": must be an integer]. Always use Mi for mebibytes, not M. The scheduler refuses to place pods when resources are overcommitted, returning 0/3 nodes are available: 3 Insufficient memory. The container runtime fails to start when the image tag does not exist, producing Failed to pull image "my-registry/go-service:latest": rpc error: code = NotFound.

Go services often exit immediately if they cannot bind to a port or if an environment variable is missing. Kubernetes sees a zero exit code and assumes success. The pod stays in Running but your HTTP handler never starts. Add a readiness probe to prevent Kubernetes from routing traffic until your server is actually listening.

        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5

The probe hits /healthz every five seconds. Kubernetes removes the pod from the service endpoints until it returns a 200 status. If the probe fails three times, the pod is marked unhealthy. Traffic stops flowing. The process keeps running, giving you time to debug without dropping active requests.

Never trust a container status alone. Probes tell the truth about application health.

Which manifest type fits your workload

Use a Deployment when you need stateless replicas that can be replaced, scaled, or rolled back without preserving data. Use a StatefulSet when each pod requires a stable identity, persistent storage, and ordered startup, such as a database cluster or message queue. Use a DaemonSet when you need exactly one pod running on every node, such as a log collector or metrics exporter. Use a ClusterIP Service when your Go service only needs to be reachable by other workloads inside the cluster. Use a NodePort or LoadBalancer Service when you need to expose the service to external traffic. Use an Ingress resource when you need path-based routing, TLS termination, or host-based virtual hosting across multiple services.

Pick the simplest object that satisfies the requirement. Kubernetes rewards restraint.

Where to go next