From devops
Write a Dockerfile for a service — multi-stage build, minimal image, security hardening.
npx claudepluginhub hpsgd/turtlestack --plugin devopsThis skill is limited to using the following tools:
Write a Dockerfile for $ARGUMENTS.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Write a Dockerfile for $ARGUMENTS.
Before writing the Dockerfile:
Dockerfile, Dockerfile.*, docker-compose.ymlEvery Dockerfile uses multi-stage builds. Minimum two stages, often three:
# Stage 1: Dependencies (cached layer — changes only when lockfile changes)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production=false
# Stage 2: Build (changes when source changes)
FROM deps AS build
COPY . .
RUN npm run build
# Stage 3: Runtime (minimal — only production artifacts)
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
Rules:
deps, build, runtime, test)Layer order matters. Layers that change less frequently go first:
1. Base image (changes: rarely)
2. System package installation (changes: rarely)
3. Dependency lockfile copy + install (changes: when deps change)
4. Source code copy (changes: every build)
5. Build command (changes: every build)
Rules:
npm ci (not npm install) for reproducible installsRUN commands with && to reduce layers, but keep logical separation for cache efficiencyCOPY . . before installing dependencies — it invalidates the cache on every source change| Stack | Build stage | Runtime stage |
|---|---|---|
| Node.js | node:22-alpine | node:22-alpine or gcr.io/distroless/nodejs22 |
| .NET | mcr.microsoft.com/dotnet/sdk:9.0 | mcr.microsoft.com/dotnet/aspnet:9.0-alpine |
| Python | python:3.13-slim | python:3.13-slim or gcr.io/distroless/python3 |
| Go | golang:1.23-alpine | gcr.io/distroless/static-debian12 (scratch for CGO_ENABLED=0) |
| Rust | rust:1.80-slim | gcr.io/distroless/cc-debian12 or scratch |
Rules:
latest tag — pin to a specific version-alpine variants for smaller images (unless native deps require glibc)distroless for production — no shell, no package manager, smaller attack surfacenode:22-alpine@sha256:abc123...# Create non-root user
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
# Switch to non-root user BEFORE copying application files
USER appuser
# Copy application files (owned by non-root user)
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
Security checklist:
| Control | Implementation | Required |
|---|---|---|
| Non-root user | USER appuser | MANDATORY |
| No secrets in image | Use runtime env vars or mounted secrets | MANDATORY |
| Read-only filesystem | --read-only flag at runtime | RECOMMENDED |
| Drop capabilities | --cap-drop=ALL at runtime | RECOMMENDED |
| No shell (distroless) | Use distroless base | RECOMMENDED |
| Minimal packages | No curl, wget, bash in production | RECOMMENDED |
| CVE scanning | trivy image or docker scout | MANDATORY |
Rules:
--privileged in runtimeENV NODE_ENV=production (or equivalent) in the runtime stageHEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] \
|| exit 1
For distroless images (no shell, no wget):
# Use the application's built-in health endpoint
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/app/healthcheck"]
Rules:
start-period accounts for application startup timeretries=3 before marking unhealthy — avoid flappingEvery Dockerfile is accompanied by a .dockerignore file:
# Version control
.git
.gitignore
# Dependencies (installed in container)
node_modules
__pycache__
.venv
# Build output (built in container)
dist
build
bin
obj
# Development files
*.md
LICENSE
.editorconfig
.eslintrc*
.prettierrc*
tsconfig*.json
jest.config.*
vitest.config.*
# IDE
.vscode
.idea
*.swp
*.swo
# Environment and secrets
.env
.env.*
*.pem
*.key
# Tests
tests
test
__tests__
*.test.ts
*.spec.ts
coverage
# Docker
Dockerfile*
docker-compose*
.dockerignore
# CI
.github
.gitlab-ci.yml
Rules:
.dockerignore MUST exist — without it, COPY . . sends everything to the daemon (including .git, node_modules, secrets).env files are NEVER included in the imageFROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
RUN npm prune --production # Remove dev dependencies
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 app && adduser --system --uid 1001 --ingroup app app
USER app
COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/package.json ./
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
CMD ["node", "dist/index.js"]
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 app && adduser --system --uid 1001 --ingroup app app
USER app
COPY --from=build --chown=app:app /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
ENTRYPOINT ["dotnet", "MyApp.dll"]
FROM python:3.13-slim AS build
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY . .
FROM python:3.13-slim AS runtime
WORKDIR /app
RUN groupadd --system --gid 1001 app && useradd --system --uid 1001 --gid app app
COPY --from=build /install /usr/local
COPY --from=build --chown=app:app /app .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
COPY . . before dependency install — invalidates the dependency cache on every source changelatest tag — non-reproducible builds. Pin versionsapt-get install curl vim wget in production images.dockerignore — sending .git and node_modules to the build contextnpm install instead of npm ci — npm install modifies the lockfile and is non-deterministicDeliver:
Dockerfile with multi-stage build, security hardening, and health check.dockerignore filedocker-compose.yml additions needed for local development/devops:write-pipeline — the CI/CD pipeline builds and pushes the Docker image. Update the pipeline when the Dockerfile changes.