Best Practices From Day One (Before Bad Habits Stick)

Local labs forgive sloppiness; production does not. The good news: a small set of habits—namespaces, labels, resource declarations, sane security defaults, and config in objects instead of images—will make your manifests look like a platform team wrote them from the start.

In short

Isolate by namespace, label with app.kubernetes.io/*, set requests and limits, run non-root where you can, store config in ConfigMaps, keep secrets out of Git, and treat kubectl apply -f as your default workflow.

Namespaces: soft walls between teams and environments

apiVersion: v1
kind: Namespace
metadata:
  name: learn-dev
  labels:
    environment: dev
kubectl apply -f ns-learn-dev.yaml
kubectl apply -f web.yaml -n learn-dev
kubectl get all -n learn-dev

Namespaces do not replace security by themselves, but they organize RBAC, quotas, and DNS (service.learn-dev.svc.cluster.local). Avoid dumping everything into default once you share a cluster.

Labels: the address book of the cluster

Use the recommended labels where they fit:

  • app.kubernetes.io/name — application name (used in selectors).
  • app.kubernetes.io/instance — this deployment of the app (e.g. web-prod).
  • app.kubernetes.io/version — version string or image tag.
  • app.kubernetes.io/componentapi, worker, database.
  • app.kubernetes.io/part-of — larger system (e.g. billing).
  • app.kubernetes.io/managed-byhelm, kustomize, or your team name.

Consistent labels unlock filtering, NetworkPolicies, cost allocation, and GitOps diffs that humans can read.

Resource requests and limits

Without requests, the scheduler guesses. Without limits, one noisy neighbor can starve a node.

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 250m
    memory: 256Mi

Start conservative in labs; tune with metrics later. cpu is measured in cores (100m = 0.1 core). memory uses binary suffixes (Mi, Gi).

Security context basics (least privilege)

securityContext:
  runAsNonRoot: true
  runAsUser: 101
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

Not every image supports non-root or read-only roots on day one—nginx and distroless examples differ. Treat these fields as goals: fix the image or mount a writable emptyDir for temp paths instead of disabling security “because local.”

Configuration: ConfigMap, not baked into the image

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
  namespace: learn-dev
data:
  APP_MODE: "lab"

Mount as env vars or files in the Pod template. Changing config becomes a reviewed YAML change, not a rebuild of a golden image for every toggle.

Secrets: never commit real ones

  • Local lab: kubectl create secret generic demo --from-literal=token=test is enough to learn wiring.
  • Real clusters: use sealed secrets, external secrets operators, or cloud KMS—see GitOps principles.
  • Add *.secret.yaml patterns to .gitignore if you must keep local samples.

Workflow habits that scale

  • Prefer kubectl apply -f over kubectl edit for anything you care about—edit drifts from Git.
  • One concern per manifest file or one kustomization base; avoid thousand-line blobs.
  • Pin images to tags or digests (nginx:1.25-alpine, not latest).
  • Probes: add readinessProbe and livenessProbe before you call a Deployment “production ready”—see probes in depth (Part 5 shows debugging when they fail).
  • Policy later: admission controllers (OPA Gatekeeper, Kyverno) encode what you already practice manually.

Lab challenge

Refactor your Part 3 web Deployment into namespace learn-dev with recommended labels, resource requests/limits, and a ConfigMap env var. Apply from a folder in Git. Delete the namespace to wipe the experiment: kubectl delete namespace learn-dev.

← Part 3 · Blog index · Part 5 →

Part 3 Part 5