Full-stack pnpm + Turborepo monorepo skill, independent of the separate backend and frontend monorepo skills it draws on. Repository shape is split four ways at the top level: applications-backend, applications-frontend, packages-backend, packages-frontend — so a path states which runtime a file ends up in. Defaults: NestJS for backend services, React with IBM Carbon Design for browser applications; the project's stated architecture overrides both. Covers workspace globs, the Turborepo pipeline, filtered validation, blast-radius checks, Module Federation, turbo prune image builds, and Helm. Documents one sanctioned cross-boundary dependency: API contract types owned by the backend, imported type-only by the frontend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.0 KiB
Containers and Helm
Building images for both sides of the monorepo, and the Helm assets that deploy them.
Read this when editing a Dockerfile, adding a chart, wiring an image tag, changing values or secrets, or propagating a port or base-path change into deployment.
1. turbo prune is what makes image builds sane
Both sides build the same way: prune the monorepo to one workspace and its dependencies, install, build, then copy the result into a minimal runtime image.
turbo prune --scope=@<scope>/<workspace> --docker
This produces out/json/ (package manifests plus the lockfile) and out/full/ (source).
Copying and installing out/json/ before out/full/ is the whole point: dependency
installation caches on manifests alone, so editing source does not reinstall, and an unrelated
workspace's change does not invalidate this image at all.
Without pruning, every image rebuilds on any change anywhere in the repository.
2. The two runtime shapes
| backend | frontend | |
|---|---|---|
| Runtime stage | node:22-alpine |
nginx:alpine |
| Ships | compiled server + node_modules |
static assets only |
| Serves | the Node process | nginx |
Needs try_files fallback |
no | yes |
| Runtime env vars | yes — read at startup | no — baked in at build time |
That last row is the one that causes incidents. A frontend build inlines its environment variables into the bundle, so they are public and fixed at build time. A secret in a frontend build argument is a published secret. Anything genuinely per-environment on the frontend must be fetched at runtime from an endpoint, not baked in.
Full Dockerfiles: references/applications-backend.md §6 and
references/applications-frontend.md §5.
3. Chart layout
helm-charts/<chart-name>/
├── Chart.yaml
├── values.yaml # defaults — never environment-specific secrets
├── values-<env>.yaml # per-environment overrides
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── configmap.yaml
└── _helpers.tpl
Rules:
- Never hardcode an image tag in a template. It comes from values, supplied by the pipeline. A hardcoded tag makes every environment deploy whatever was current when someone last edited the chart.
- Ports and base paths in values must match the service's actual configuration (§5).
- Resource requests and limits are set for every container. A container without them competes unboundedly with everything else on the node.
- Liveness and readiness probes point at the service's two distinct health endpoints — see
references/applications-backend.md§4.
4. Secrets
- No secret values in
values.yaml,values-<env>.yaml, templates, or the repository. - Reference an existing Kubernetes
Secretby name, or use an external secrets provider that materialises it in-cluster. - A base64 string in a committed manifest is encoding, not encryption — treat it as plaintext.
- Never log a secret's value from a container's startup path.
5. Port and base-path propagation
Changing a service's port or base path touches, at minimum:
- the service's own config (
main.ts, config factory), - the Dockerfile
EXPOSE, - the chart's values and
service.yaml, - ingress or gateway routing,
- every frontend caller,
- the paired code-agent
memory-bank/record.
Miss any one and the failure appears at runtime, in a different component from the one you edited. Treat it as a blast-radius change (SKILL.md §8) and update all six in the same change.
6. Validation
helm lint helm-charts/<chart-name>
helm template <release> helm-charts/<chart-name> -f helm-charts/<chart-name>/values-<env>.yaml
# what would actually change in the cluster
helm diff upgrade <release> helm-charts/<chart-name> -f values-<env>.yaml
helm lint checks structure, not intent — a chart that lints cleanly can still point at the
wrong port. Always read the rendered output for the values that matter: image tag, port, base
path, probe paths, resource limits.
Verify an image locally before shipping it:
docker build -f applications-backend/<service>/Dockerfile -t <service>:local .
docker run --rm -p <port>:<port> <service>:local
Build from the repository root — the Dockerfile's COPY . . expects the whole monorepo as
context, because turbo prune runs inside the image.
7. Mistakes to avoid
- Building an image without
turbo prune. - Copying
out/full/before installing fromout/json/, losing all dependency-layer caching. - Running a container as root.
- A secret in a frontend build argument or any committed values file.
- A hardcoded image tag in a template.
- Missing
try_filesin the frontend nginx config. - Containers with no resource requests or limits.
- A liveness probe that checks dependencies.
- Changing a port or base path in fewer than all six places in §5.
- Building the image from the workspace directory instead of the repository root.
- Editing rendered manifests instead of the chart that produces them.