Files
development-monorepo-pnpm-3…/references/containers-and-helm.md
root-at-skicandClaude Opus 5 ff69bcca2f development-monorepo-pnpm skill: initial import
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>
2026-08-12 23:49:02 +03:00

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 Secret by 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:

  1. the service's own config (main.ts, config factory),
  2. the Dockerfile EXPOSE,
  3. the chart's values and service.yaml,
  4. ingress or gateway routing,
  5. every frontend caller,
  6. 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 from out/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_files in 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.