Skip to content
Back to skills

Containerize Service

BSecurity

Use when a backend or full-stack service needs a production container image — multi-stage Dockerfile per track (Node/pnpm, Go, Python/uv), non-root, digest-pinned bases, health check, SBOM and provenance, image scan.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 10, 2026
developmentpythongoshellbashnextjsnodefastapidockerkubernetesgit

Works with

  • api

Security analysis

B88/100
  • criticalAccesses sensitive system or user directories

Pro scans all 3 files and shows the line behind each finding

Scanned October 10, 2026

npx -y skills add VelimirMueller/lab-claude-skills --skill containerize-service --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Containerize Service?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Containerize Service
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-containerize-service/badge)](https://www.skillsdirectory.com/skills/velimirmueller-containerize-service)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: containerize-service
description: Use when a backend or full-stack service needs a production container image — multi-stage Dockerfile per track (Node/pnpm, Go, Python/uv), non-root, digest-pinned bases, health check, SBOM and provenance, image scan.
---

# Containerize Service

One image per service, built once, run as a non-root user, healthy only when the service says so. The same image goes to `dev`, `stg` and `prd` ([environments.md](../_shared/environments.md)).

## 1. Audit current state (change nothing)

```bash
cat .claude/stack-profile.md 2>/dev/null                       # languages, package_manager, backend.track, hosting
ls Dockerfile* .dockerignore compose*.y*ml deploy/ 2>/dev/null
ls pnpm-lock.yaml go.mod pyproject.toml uv.lock 2>/dev/null     # which track(s)
grep -rn "healthz\|/health" --include=*.ts --include=*.go --include=*.py . 2>/dev/null | grep -v node_modules | head -3
docker --version && docker buildx version
```

Read from the profile: `languages`, `package_manager` (pnpm, go, uv), `backend.track` (hono, nextjs, go, fastapi), `hosting`. If there is no profile, detect from the lockfiles above. Ask one question only when two tracks match (a monorepo) and the choice changes the output: which service to containerize. Suggest `set-up-stack-profile` if the profile is missing.

## 2. Decide
- No `Dockerfile` → full (steps 3 to 7).
- `Dockerfile` exists → delta: check each line of the checklist below, fix only what fails.
- All checks pass and `trivy` is clean → say "already in place" and stop.

Checklist: multi-stage; every `FROM` has `@sha256:`; final stage non-root; `.dockerignore` excludes `.git`, `node_modules`, `.env*`; `HEALTHCHECK` present; no secret in `ENV`, `ARG` or `COPY`; `--frozen-lockfile`, `--locked` or `go mod download` before source copy.

## 3. Detect the track
| Evidence | Track | Template in [dockerfile-patterns.md](./dockerfile-patterns.md) |
|---|---|---|
| `package.json` + `pnpm-lock.yaml` (Hono, Fastify, Nest) | Node/pnpm | "Node (pnpm)" |
| `package.json` with `next` | Node/pnpm, Next.js | "Node (pnpm)" plus the standalone note |
| `go.mod` | Go static | "Go" |
| `pyproject.toml` + `uv.lock` (FastAPI) | Python/uv | "Python (uv)" |
| `npm`/`yarn`/`bun` lockfile | same shape; swap the install line | keep the stage layout |

Hosted on Vercel (`hosting: [vercel]`) and no worker or long-running process: no image needed. Stop and say so; use [deploy-to-vercel](../deploy-to-vercel/SKILL.md).

## 4. Install only what is missing
Nothing on the host except Docker with BuildKit (`docker buildx version`). Do not install language toolchains for the image; the build stage carries them. Create `.dockerignore` first:

```gitignore
.git
node_modules
dist
.venv
__pycache__
bin
*.test
.env*
!.env.example
Dockerfile
.dockerignore
```

## 5. Generate the seams
1. **Health seam.** The service exposes `GET /healthz` returning 200 when it can serve. The image probes it with the service's own binary, because distroless has no `curl` or shell. Code for each track is in [dockerfile-patterns.md](./dockerfile-patterns.md).
2. **Dockerfile** from the track template. Set the three `ARG … IMAGE=` lines to the digests in [stack-versions.md](../_shared/stack-versions.md); re-read the digest with `docker buildx imagetools inspect <image:tag> --format '{{.Manifest.Digest}}'`.
3. **Graceful shutdown.** The process handles `SIGTERM` and finishes in-flight requests within 10 s. Compose and Kubernetes send `SIGTERM` first.

## 6. Wire
- **Build locally** with attestations and check them:
  ```bash
  docker buildx build --sbom=true --provenance=mode=max -t localhost:5005/app:test --push .
  ```
  Attestations need `--push`, or the containerd image store, or a `docker-container` builder. In CI the pipeline builds with `provenance: false` and signs once with `actions/attest`; see [set-up-delivery-pipeline](../set-up-delivery-pipeline/SKILL.md). Do not add a second build path here.
- **Scan** with Trivy from a pinned image, not the GitHub Action (its tags were rewritten in March 2026):
  ```bash
  docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache \
    ghcr.io/aquasecurity/trivy:0.75.0@sha256:af6acf9a6b85dfe389a1941505c0ce9efef52a4719635e1a962f022a3d855daa \
    image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:test
  ```
  Grype (`anchore/grype`) is an equal choice if you already use Syft SBOMs.
- **Probes.** Compose uses the image `HEALTHCHECK`. Kubernetes ignores it; define `readinessProbe` and `livenessProbe` against `/healthz`.
- Compose and the host: [deploy-to-hetzner](../deploy-to-hetzner/SKILL.md), [deploy-to-ionos](../deploy-to-ionos/SKILL.md).

## 7. Verify
```bash
docker build -t app:test . && docker image inspect app:test --format '{{.Config.User}}'
```
Expect a non-root user (`65532`, `nonroot` or `10001:10001`), never empty or `0`.
```bash
docker run -d --name app-test -p 3000:3000 app:test && sleep 12 && curl -fsS localhost:3000/healthz
docker inspect app-test --format '{{.State.Health.Status}}'     # healthy
docker rm -f app-test
```
Expect `ok` and `healthy`. Reproducibility check (same digest twice):
```bash
for i in 1 2; do docker buildx build --no-cache --provenance=false --sbom=false \
  --build-arg SOURCE_DATE_EPOCH=1760000000 --output type=oci,dest=/tmp/r$i.tar,rewrite-timestamp=true . ; done
for i in 1 2; do tar -xOf /tmp/r$i.tar index.json | grep -o 'sha256:[a-f0-9]*' | head -1; done
```
Expect two identical digests. Trivy exits 0.

## References
- [container-patterns.md](./container-patterns.md): the rules and why (base image, user, pinning, health, SBOM, scanning, secrets).
- [dockerfile-patterns.md](./dockerfile-patterns.md): the three templates, built and run during authoring.
- [../_shared/environments.md](../_shared/environments.md): build once, promote the digest.
- [../../core/_shared/security-baseline.md](../../core/_shared/security-baseline.md), [../../core/_shared/engineering-principles.md](../../core/_shared/engineering-principles.md).

Files in this skill

  • SKILL.md5.9 KB
  • container-patterns.md8.1 KB
  • dockerfile-patterns.md6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.