Author and harden Dockerfiles for Node/.NET/Angular on AKS. Trigger: create/audit a Dockerfile, container findings.
65
77%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./ywai/skills/docker/SKILL.mdEvery image here runs non-root under the AKS runtime contract (below): read-only rootfs, dropped caps, least privilege. Lead with distroless runtime bases, multi-stage builds, minimal attack surface. Two ways in:
references/review-checklist.md, severize, plan, fix.Detect the stack, copy the matching template; never write a Dockerfile from scratch when one applies.
| Project signal | Template |
|---|---|
package.json with a server entrypoint (NestJS, Express, Fastify) | templates/node-backend.Dockerfile |
angular.json, or a Vite/React SPA (static output) | templates/spa-nginx.Dockerfile + templates/spa-nginx.conf |
*.csproj / global.json (.NET) | templates/dotnet-api.Dockerfile |
Pair every image with its .dockerignore (templates/node.dockerignore / templates/dotnet.dockerignore). Adapt versions to the project — Node major from engines, .NET from global.json — and never silently bump the runtime major.
Both branches enforce these; a review reports each violation by number.
# syntax=docker/dockerfile:1 — unlocks BuildKit cache mounts, secrets, --chmod.node:24-alpine@sha256:…). Resolve with docker buildx imagetools inspect <img>:<tag>; automate bumps with Renovate. No network → leave the tag plus a commented @sha256: placeholder and tell the user to resolve it.deps → build → prod-deps → runtime. The runtime stage holds only artifacts and production dependencies — never source, package managers, or compilers.gcr.io/distroless/nodejs<major>-debian12:nonroot; .NET → mcr.microsoft.com/dotnet/aspnet:<ver>-noble-chiseled; SPA → nginxinc/nginx-unprivileged:<ver>-alpine (uid 101, port 8080). Fall back to -alpine/-slim only if the container genuinely needs a shell — say so in a comment.:nonroot, chiseled, and nginx-unprivileged already comply; on any other base add an explicit USER.COPY --chown=<runtime-user> source or node_modules — the runtime user needs read-only access, and a compromised process must not be able to rewrite its own code. Only /tmp is writable (K8s emptyDir).--ignore-scripts --no-audit --no-fund with a cache mount (--mount=type=cache,target=/root/.npm), and npm ci over npm install. --ignore-scripts is supply-chain defence — never drop it to "fix" a build without flagging it.CMD/ENTRYPOINT (JSON array) so the process receives SIGTERM directly.ARG/ENV/COPY — not even self-signed dev certs or an .npmrc. A build-time secret uses RUN --mount=type=secret. The .dockerignore must exclude .env*, .npmrc, keys, and certs.HEALTHCHECK. AKS Helm probes own liveness/readiness and distroless has nothing to run it with; expose an HTTP /health_check endpoint instead.{{chartversion}} and pushing is the docker-acr-build-and-push pipeline (see devops).EXPOSE matches the Helm containerPort in values.yaml.Every image must run clean under this securityContext — if it can't, the Dockerfile is wrong, not the chart:
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: [ALL] }
seccompProfile: { type: RuntimeDefault }Anything written at runtime goes to /tmp via an emptyDir (e.g. the SPA env.js runtime-config pattern).
Dockerfile*: every .dockerignore (one per build context), docker-compose*.y*ml, and what the image COPYs or runs — entrypoints (*.sh) and server configs (nginx.conf, default.conf). These ship inside the image, so they are attack surface.COPY, ENV/ARG, and the logic of every script and config.references/review-checklist.md — every item. Each finding: file:line, severity, impact, concrete fix..dockerignore (not just the repo root).docker build and a smoke test: docker run + id (confirms non-root) + hit /health_check.## Findings
| # | file:line | Finding | Severity | Fix |
## Iteration plan
- Iteration 1 (no build break): non-root, port > 1024, .dockerignore per context, ${VAR} in compose.
- Iteration 2 (larger): digest-pin, minimal/supported base, read-only rootfs, drop the package manager from the runtime stage.
- Iteration 3 (modernise): replace EOL bases, wire a CI image scan (Trivy/Grype) that fails on critical CVEs, move any baked secret to BuildKit secrets / Secret Store.A secret in the repo or in a layer is always Critical — even a self-signed dev cert.
Generic guides relax two things; we don't — keep them from being "corrected":
HEALTHCHECK (Rule 10) — Helm probes own it on AKS; the instruction is dead weight and distroless can't run it.--chown of app code to the runtime user (Rule 6) — root-owned code is the defence against a compromised process rewriting itself. Generic COPY --chown=app … patterns violate this.36f5af5
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.