Files
development-monorepo-pnpm-3…/references/containers-and-helm.md
T
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

129 lines
5.0 KiB
Markdown

# 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.
```bash
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
```text
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
```bash
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:
```bash
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.