Stage 7 · Master
Shipping It — Docker & Kubernetes
ConfigMaps, Secrets & Resource Limits
Fieldwork keeps images immutable, injects config at runtime, and sizes pods from observed workload instead of wishful thinking.
What Belongs Outside the Image
Once Fieldwork's service images became immutable artifacts, the next question was what not to bake into them. Anything environment-specific stayed out: database hostnames, Redis URLs, feature flags, log level defaults, and OAuth issuer settings all change between local, staging, and production. If those values are compiled into the image or copied in during docker build, you no longer have one artifact promoted through environments. You have several almost-identical artifacts that happen to share a repository. That breaks one of the biggest benefits of containerization.
ConfigMaps were the right home for non-secret runtime settings because they let us declare environment-specific behavior without rebuilding the binary. They were not a generic dumping ground, though. We avoided huge catch-all ConfigMaps full of unrelated values. Each service received the small set of configuration it actually consumed. That kept the contract between Kubernetes and the service understandable: tasks-service gets its HTTP port, log level, metrics namespace, and public dependency endpoints; it does not inherit the entire world's settings just because envFrom can make that easy.
| Configuration style | Why it feels convenient | Why Fieldwork did or did not |
|---|---|---|
| Bake env-specific values into image | One less Kubernetes object to manage | Rejected. Produces different artifacts per environment and makes rollback ambiguous. |
| One giant shared ConfigMap | Simple to wire with envFrom | Rejected. Weak ownership and too much accidental coupling between services. |
| Small service-specific ConfigMaps | A little more explicit YAML | Chosen. Keeps config scoped to the binary that actually consumes it. |
How We Handled Secrets Without Cheating
Secrets were handled with the same discipline, but with a stricter rule: no credentials in git, no credentials in Dockerfiles, and no credentials copied into images during CI. Fieldwork used Kubernetes Secrets as the immediate injection mechanism for DATABASE_URL, JWT signing material references, and third-party webhook tokens. In a larger platform we would likely back that with an external secret manager, but the essential lesson is the same either way. The runtime should receive credentials at deploy time, not inherit them forever from a build artifact that anybody can pull months later.
We also avoided the common half-measure of putting secret-looking values in ConfigMaps because "the cluster is private anyway." That is not a design rule; that is a hope. The object type should communicate sensitivity, access should be narrower, and operational tooling should know which values demand more careful handling. Using Secrets does not solve every security problem by itself, but skipping them guarantees that secret management never becomes explicit.
apiVersion: v1
kind: ConfigMap
metadata:
name: tasks-service-config
namespace: fieldwork
data:
APP_ENV: production
HTTP_PORT: "8080"
LOG_LEVEL: info
TASKS_METRICS_NAMESPACE: fieldwork_tasks
---
apiVersion: v1
kind: Secret
metadata:
name: tasks-service-secrets
namespace: fieldwork
type: Opaque
stringData:
DATABASE_URL: postgres://fieldwork:***@postgres.fieldwork.svc.cluster.local:5432/tasks
JWT_PUBLIC_KEY_PEM: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tasks-service
namespace: fieldwork
spec:
template:
spec:
containers:
- name: tasks-service
image: ghcr.io/thesyscoder/fieldwork/tasks-service:sha-9f2a6c1
envFrom:
- configMapRef:
name: tasks-service-config
- secretRef:
name: tasks-service-secretsNon-secret settings and credentials are separate Kubernetes objects, but the service receives both as environment variables at runtime. The image stays unchanged across environments.
Kubernetes Secret manifests are often serialized as base64 in rendered YAML, but that is transport encoding, not protection. Treat secret sources, access control, and auditability as the real security boundary.
Resource Values Came From Measurement
Requests and limits were the next place where fake certainty could sneak in. It is easy to paste 100m CPU and 128Mi memory into every Deployment because the examples on the internet do. Fieldwork did not do that. We based requests on observed steady-state usage under representative traffic and based limits on headroom for burst plus garbage collection behavior. tasks-service, for example, had modest CPU needs at idle but spiky memory usage during larger task list queries with authorization joins. A too-tight memory limit would not have made the service efficient. It would have made it OOMKill under perfectly normal traffic.
We also distinguished between requests and limits operationally. Requests exist to tell the scheduler what the pod needs to run reliably. Limits exist to prevent one pod from consuming unbounded node resources. When both are guessed badly, the cluster pays twice: the scheduler places pods poorly and the kernel kills them under load. So the numbers were tuned from metrics, not copied from a blog post.
- Start with metrics from load tests and staging traffic, not arbitrary defaults.
- Set CPU requests near typical usage so the scheduler can pack pods honestly.
- Set memory limits with real query spikes and Go GC behavior in mind.
- Revisit values after major features that change request fan-out or payload size.
Wiring Config Into the Services
Runtime injection only works well if the service treats configuration as a typed contract instead of a pile of getenv calls scattered through handlers. Fieldwork centralized config loading near service startup. That kept validation in one place and made readiness fail fast when required values were missing. The service either starts with a coherent config or it does not start at all. That is a much better failure mode than discovering twenty minutes later that only one code path forgot to parse a secret or that a misspelled environment variable silently turned off metrics.
A service that knows exactly which variables it requires is easier to deploy, test, and rotate than one that slurps a hundred environment variables and hopes the right subset exists.
What We Actually Shipped
Fieldwork shipped immutable service images, injected non-secret runtime settings through service-specific ConfigMaps, injected credentials through Secrets, and loaded both into typed startup configuration inside each Go service. Resource requests and limits were based on measured behavior rather than generic defaults, which let the scheduler place pods honestly and kept normal traffic from turning into restart noise.
The deeper decision was refusing to blur boundaries. Images are for code, ConfigMaps are for non-secret runtime settings, Secrets are for credentials, and resource numbers are operational data points rather than decoration. Once those categories stay clear, the deployment model remains predictable. Once they blur, every rollout becomes an archaeology exercise in where a value actually came from.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.