Containers from the CLI: docker & kubectl
Build and run Go images with docker locally, then debug pods in Kubernetes with kubectl logs, exec, and port-forward.
Summary
Go's static binaries fit minimal container images, but production issues still surface through container CLIs: a pod crash loop, an image that works on your laptop but not in CI, or a health check that never goes ready.
These commands are the fastest path from symptom to process inside the cluster.
Recipe
Quick-reference recipe card - copy-paste ready.
# Dockerfile (multi-stage)
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/api /api
USER nonroot:nonroot
ENTRYPOINT ["/api"]# Local
docker build -t api:dev .
docker run --rm -p 8080:8080 api:dev
docker logs -f <container_id>
# Kubernetes
kubectl get pods -l app=api
kubectl logs deploy/api --tail=200
kubectl exec -it deploy/api -- /api -version 2>/dev/null || kubectl exec -it deploy/api -- /bin/sh
kubectl port-forward deploy/api 6060:6060When to reach for this:
- Reproduce a production image locally before pushing to a registry.
- A Go pod is
CrashLoopBackOffand you need logs and exit codes fast. - You must reach a debug or pprof port without exposing it on a public Service.
Working Example
#!/usr/bin/env bash
set -euo pipefail
# --- docker: build and smoke test ---
cat > Dockerfile <<'EOF'
FROM golang:1.26 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /api ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=build /api /api
USER 65532:65532
ENTRYPOINT ["/api"]
EOF
docker build -t api:local .
CID=$(docker run -d -p 18080:8080 api:local)
sleep 2
curl -sf localhost:18080/healthz
docker logs "$CID" | tail -20
docker rm -f "$CID"
# --- kubectl: inspect deployment (names are examples) ---
kubectl get deploy,po -l app=api
kubectl rollout status deploy/api --timeout=120s
kubectl logs deploy/api --tail=100 --timestamps
kubectl describe pod -l app=api | rg -n "State:|Reason:|Exit Code:|Liveness|Readiness"What this demonstrates:
- Multi-stage build producing a distroless runtime image for Go.
- Smoke test with published port and log tail before cleanup.
- kubectl rollout wait plus describe output filtered with ripgrep for probe failures.
Deep Dive
How It Works
- docker build runs Dockerfile stages; the compile stage needs Go toolchain and module cache, the runtime stage often only copies the static binary.
- docker run starts a container with port mappings (
-p host:container) and optional env (-e) mirroring K8s env vars. - kubectl logs reads container stdout/stderr from the kubelet;
-ffollows,--previousshows the last crashed instance. - kubectl exec runs a process in the pod network namespace - essential when the runtime image has no shell (distroless).
- kubectl port-forward tunnels a local port to a pod port for pprof or admin APIs without changing the Service.
Command Reference
| Task | docker | kubectl |
|---|---|---|
| Build image | docker build -t name:tag . | kubectl apply -f (after push) |
| Run / deploy | docker run -p ... | kubectl rollout restart deploy/name |
| Stream logs | docker logs -f id | kubectl logs -f deploy/name |
| Shell | docker exec -it id sh | kubectl exec -it po -- sh |
| Debug port | -p 6060:6060 | kubectl port-forward po 6060:6060 |
| Inspect exit | docker inspect --format='{{.State.ExitCode}}' | kubectl describe po Events |
Go Notes
# Cache modules in build stage
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/api ./cmd/apiPin the golang image tag to the same minor your go.mod declares; align with GOTOOLCHAIN policy in CI.
Gotchas
- Building on wrong architecture - Apple Silicon builds
linux/arm64while cluster isamd64. Fix:docker build --platform linux/amd64or CI native builders. - Distroless without shell -
kubectl exec shfails. Fix: debug toolkit image, ephemeral debug container, or scratch + copy binary to a debug pod. - Logging to files inside containers - Files vanish on restart; kubelet only ships stdout/stderr. Fix: log to stdout; use centralized logging.
- Forgotten
USER- Container runs as root despite Go best practice. Fix: non-rootUSERmatching KubernetessecurityContext.runAsNonRoot. - Liveness killing slow starts - Go JVM-less startup is fast, but migrations can delay ready. Fix: separate readiness vs liveness; tune
initialDelaySeconds. - port-forward left running - Tunnels bypass network policy locally. Fix: close after debugging; never share admin ports on
0.0.0.0.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| docker CLI | Laptop and CI image build | Production fleet only on Kubernetes |
| podman | Rootless containers on RHEL/Fedora | Team standardized on Docker Desktop |
| kubectl debug | Ephemeral debug containers (K8s 1.23+) | Simple log-only triage |
| Helm / GitOps | Declarative deploys | You only need one-shot log grep |
| systemd on VM | No orchestrator | Organization mandates Kubernetes |
FAQs
Should Go images use alpine or distroless?
distroless/static minimizes attack surface for pure Go binaries.
alpine adds a shell and package manager when operators want apk add debugging tools.
How do I see why a pod restarted?
kubectl describe pod Events and kubectl logs --previous for the last terminated container.
Can I docker run the same image as production?
Pull by digest from your registry and run with the same env vars and entrypoint.
Validates image integrity outside the cluster.
How do I copy a profile out of a pod?
kubectl port-forward plus curl locally, or kubectl cp if the pod wrote a file to a volume.
Why is my image huge despite Go static binaries?
The build stage or accidental COPY . includes test data and .git.
Use .dockerignore and multi-stage builds; runtime should only contain the binary.
What kubectl context should I use?
kubectl config current-context before destructive commands.
Namespace flags (-n prod) prevent cross-environment mistakes.
How do I test gRPC services?
grpcurl via kubectl port-forward to the gRPC port, or run grpcurl in an ephemeral debug pod on the cluster network.
Does docker compose replace kubectl for local dev?
Compose models multi-service dev stacks.
kubectl remains required for cluster behavior: probes, ConfigMaps, and network policies.
How do I pass GOMAXPROCS or GOGC?
Set container env vars in the Deployment manifest - same as shell exports on VMs.
What if logs are empty?
The process may log to a file or crash before writing.
Check kubectl describe exit codes and run the image with docker run --rm -it locally.
Should I install kubectl plugins?
krew plugins like ctx and ns speed context switching.
Core verbs cover most Go service debugging.
How do I restart one deployment after a config change?
kubectl rollout restart deploy/api triggers a rolling update with the same image tag if only env changed via manifest apply.
Related
- Build, Test & Profile from the Shell - pprof via port-forward
- systemd & Service Unit Files for Go Binaries - VM deploy alternative
- Search, ripgrep & jq for Debugging - filter log dumps
- Linux CLI Skills for Go Engineers - container vs VM flows
- Cloud Deploy Basics - cloud runtime patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).