
Docker Architect
- 12 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
docker-architect is a skill that designs, refactors, and security-hardens Docker images and Compose environments end-to-end.
About
This skill designs, implements, and hardens Docker images and Compose environments. Developers use it to create or rewrite Dockerfiles and docker-compose files, audit existing container setups for security and size issues, and add CI pipelines for build, test, scan, and publish. It favors multi-stage BuildKit builds and least-privilege runtime defaults.
- Produces secure, right-sized Docker images and Compose environments end-to-end
- Audits existing container setups for security and best practices
- Includes inventory, audit, and template-rendering scripts plus CI templates
Docker Architect by the numbers
- 12 all-time installs (skills.sh)
- Ranked #978 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
docker-architect capabilities & compatibility
- Capabilities
- devops · ci cd · security audit
- Works with
- docker · github
- Use cases
- devops · ci cd · security audit
What docker-architect says it does
Produce production-grade, secure, right-sized Docker images and Compose environments end-to-end: inventory → design → implement → test → CI.
Prefer minimal, reproducible builds (multi-stage + BuildKit) and least-privilege runtime defaults.
Secrets in image/build args, root/privileged runtime, overly broad mounts, host networking, “latest” tags, missing healthchecks.
npx skills add https://github.com/bjornmelin/dev-skills --skill docker-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Creating or auditing Dockerfiles and Compose files with multi-stage builds, security hardening, and CI build/scan pipelines.
Who is it for?
Developers containerizing apps or hardening existing Docker and Compose setups.
Skip if: Non-container deployment models or serverless-only stacks.
When should I use this skill?
Creating, rewriting, auditing, or debugging Dockerfiles, Compose files, or container CI pipelines.
By the numbers
- Includes GitHub Actions CI and publish templates
- Provides inventory, audit, and template-render scripts
Files
Docker Architect
Overview
Produce production-grade, secure, right-sized Docker images and Compose environments end-to-end: inventory → design → implement → test → CI. Prefer minimal, reproducible builds (multi-stage + BuildKit) and least-privilege runtime defaults.
Quick Start (always do this first)
1. Inventory the repo and existing container config:
- Run
python3 /home/bjorn/.codex/skills/docker-architect/scripts/docker_inventory.py --root .
2. Choose the target:
- New containerization → follow “New build workflow”
- Existing Dockerfiles/Compose → follow “Audit + refactor workflow”
3. Validate locally:
docker buildx versiondocker buildx build ...(ordocker build ...)docker compose configanddocker compose up --build
Template rendering example (edit variables per repo):
python3 /home/bjorn/.codex/skills/docker-architect/scripts/render_template.py --template .dockerignore --out .dockerignorepython3 /home/bjorn/.codex/skills/docker-architect/scripts/render_template.py --template compose/docker-compose.yml --out docker-compose.yml --var IMAGE_NAME=myapp:dev --var HOST_PORT=8000 --var CONTAINER_PORT=8000python3 /home/bjorn/.codex/skills/docker-architect/scripts/render_template.py --template compose/docker-compose.dev.yml --out docker-compose.dev.yml --var CONTAINER_PORT=8000 --var DEV_COMMAND='[\"python\",\"-m\",\"uvicorn\",\"myapp.api:app\",\"--host\",\"0.0.0.0\",\"--port\",\"8000\",\"--reload\"]'
Workflow Decision Tree
1. Scope:
- Dev-only (fast iteration, source mounts, hot reload) → prefer
docker-compose.dev.yml - Prod-like (immutable images, healthchecks, least privilege) → prefer
docker-compose.yml+docker-compose.prod.yml
2. Artifact type:
- Single service → one Dockerfile + optional compose for deps
- Multi-service → compose with explicit networks/volumes and healthchecks
3. Publish target:
- Local only → keep simple; optional CI smoke checks
- Registry publish → add CI build/test/scan/push + provenance/SBOM (if available)
New build workflow (Dockerfile + .dockerignore + compose)
1. Pick a base strategy (see references/dockerfile_patterns.md):
- Multi-stage build; runtime image is minimal; build tools stay in builder stage.
- Prefer slim/distroless where feasible; default to non-root user.
2. Add .dockerignore early (template in assets/templates/.dockerignore). 3. Create a Dockerfile from templates:
- Prefer
assets/templates/python/Dockerfile.uvfor modern Python/uv - Prefer
assets/templates/node/Dockerfile.pnpmfor Node + pnpm - Use
python3 /home/bjorn/.codex/skills/docker-architect/scripts/render_template.py ...to render with variables.
4. Add a compose file for dependencies (DB/cache) and dev/prod profiles:
- Start from
assets/templates/compose/docker-compose.yml+ an override (assets/templates/compose/docker-compose.dev.ymlorassets/templates/compose/docker-compose.prod.yml) - Optional deps file:
assets/templates/compose/docker-compose.deps.yml
5. Local validation:
bash /home/bjorn/.codex/skills/docker-architect/scripts/smoke_test_container.sh --help- Optional:
--build-check(Docker build checks) and--pull(fresh base images) - For compose:
bash /home/bjorn/.codex/skills/docker-architect/scripts/smoke_test_compose.sh --help
Audit + refactor workflow (existing Dockerfiles/Compose)
1. Inventory + static audit:
python3 /home/bjorn/.codex/skills/docker-architect/scripts/docker_audit.py --root .
2. Identify high-risk issues (see references/security_hardening.md):
- Secrets in image/build args, root/privileged runtime, overly broad mounts, host networking, “latest” tags, missing healthchecks.
3. Refactor incrementally:
- Keep behavior stable, then tighten (non-root, read-only fs, drop caps, pin images, shrink layers).
4. Validate end-to-end:
docker buildx build ...docker compose up --buildand run the app’s normal test/health commands.
5. Produce a concise report (template in references/review_template.md).
CI integration workflow (GitHub Actions default)
1. Start from assets/templates/ci/github-actions-docker-ci.yml.
- For publish + SBOM/provenance to GHCR:
assets/templates/ci/github-actions-docker-publish.yml
2. Ensure CI runs:
docker buildx build(cache enabled)docker compose config(compose validation)- optional: build checks (
docker build --check) and scan/SBOM/provenance steps
3. Prefer pinned action versions and least-privilege permissions (see references/ci_github_actions.md).
“Latest/correct” research rule (do not guess)
When “latest” matters (base images, distro versions, language runtimes, CVEs): 1. Use Exa to confirm current official guidance and tags (official sources preferred). 2. Use docker buildx imagetools inspect <image:tag> to confirm manifests/platforms. 3. If unsure, mark as UNVERIFIED and propose a safe default with a verification step.
Tooling leverage (when it helps)
- Exa: find current best practices, base image changes, CVE guidance, GitHub Actions deprecations.
- Context7: confirm framework-specific build outputs (e.g., Next.js, FastAPI, uvicorn/gunicorn, etc.).
- Zen: use
zen.secauditfor a structured container/security audit andzen.analyzefor architecture-sensitive compose design. - gh_grep: search public repos for battle-tested patterns (entrypoints, healthchecks, buildx/bake, compose profiles).
- opensrc: inspect dependency internals when container behavior depends on packaging details.
Bundled resources
Scripts
scripts/docker_inventory.py: detect stack + existing Docker/Compose files.scripts/docker_audit.py: heuristic linting of Dockerfiles/Compose for security/correctness.scripts/render_template.py: render templates with{{VARS}}into repo files.scripts/smoke_test_container.sh: build/run basic health check locally.scripts/smoke_test_compose.sh: validate + bring up compose and check health.
References (load as needed)
references/dockerfile_patterns.md: BuildKit, caching, multi-stage, runtime hardening.references/compose_patterns.md: compose patterns, profiles, healthchecks, secrets/configs.references/security_hardening.md: least privilege, capabilities, read-only fs, supply chain.references/ci_github_actions.md: CI build/test/scan/publish patterns.references/review_template.md: audit report format and deliverables checklist.
Assets (templates)
Templates live under assets/templates/ (Dockerfile variants, compose variants, CI workflow, .dockerignore, docker-bake.hcl).
.git
.github
.gitignore
.DS_Store
# Secrets / env
.env
.env.*
!.env.example
!.env.sample
# Python
__pycache__/
*.py[cod]
*.pyo
.mypy_cache/
.pytest_cache/
.ruff_cache/
.tox/
.venv/
venv/
dist/
build/
# Node
node_modules/
npm-debug.log
yarn-error.log
pnpm-debug.log
.next/
.turbo/
# General
coverage/
*.log
opensrc/
vendor/
third_party/
name: docker-ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Build (no push)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: false
load: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Validate compose (if present)
run: |
if ls docker-compose*.y*ml compose.y*ml >/dev/null 2>&1; then
docker compose config >/dev/null
fi
name: docker-publish
on:
push:
branches: [main]
tags: ["v*.*.*"]
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
- name: Build and push (multi-arch)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Docker docs recommend max-level provenance; SBOM is opt-in.
provenance: mode=max
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max
services:
# Optional dependencies; include this file with:
# docker compose -f docker-compose.yml -f docker-compose.deps.yml up
postgres:
image: postgres:{{POSTGRES_VERSION}}
environment:
- POSTGRES_USER={{POSTGRES_USER}}
- POSTGRES_PASSWORD={{POSTGRES_PASSWORD}}
- POSTGRES_DB={{POSTGRES_DB}}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{POSTGRES_USER}} -d {{POSTGRES_DB}}"]
interval: 5s
timeout: 2s
retries: 20
redis:
image: redis:{{REDIS_VERSION}}
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 2s
retries: 20
volumes:
postgres_data:
redis_data:
services:
app:
environment:
- PORT={{CONTAINER_PORT}}
volumes:
- ./:/app
working_dir: /app
command: {{DEV_COMMAND}}
# DEV_COMMAND can be a string or a YAML list. Prefer list form for correctness:
# ["python","-m","uvicorn","myapp.api:app","--host","0.0.0.0","--port","{{CONTAINER_PORT}}","--reload"]
# ["npm","run","dev"]
services:
app:
environment:
- PORT={{CONTAINER_PORT}}
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
# Healthchecks are app-specific. Prefer a lightweight HTTP/CLI check.
# Note: minimal/distroless images often lack a shell/curl; use an in-app CLI or add a tiny probe binary.
# healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:{{CONTAINER_PORT}}/healthz || exit 1"]
# interval: 10s
# timeout: 2s
# retries: 10
services:
app:
build:
context: .
dockerfile: Dockerfile
image: {{IMAGE_NAME}}
ports:
- "{{HOST_PORT}}:{{CONTAINER_PORT}}"
environment:
- PORT={{CONTAINER_PORT}}
group "default" {
targets = ["app"]
}
target "app" {
context = "."
dockerfile = "Dockerfile"
tags = ["{{IMAGE_NAME}}"]
}
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.25
ARG DEBIAN_RELEASE=bookworm
FROM golang:${GO_VERSION}-${DEBIAN_RELEASE} AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
ARG GO_MAIN=.
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app "${GO_MAIN}"
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
WORKDIR /
COPY --from=build /out/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# syntax=docker/dockerfile:1
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-slim AS base
# ---- deps ----
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# ---- build ----
FROM deps AS build
COPY . .
RUN npm run build \
&& npm prune --omit=dev
# ---- runtime ----
FROM base AS runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000
RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser
COPY --from=build /app ./
USER 10001
EXPOSE 3000
CMD ["npm", "run", "start"]
# syntax=docker/dockerfile:1
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
ENV PNPM_HOME="/pnpm" \
PATH="/pnpm:$PATH"
RUN corepack enable
# ---- deps (cacheable) ----
FROM base AS deps
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm fetch
# ---- build ----
FROM base AS build
COPY --from=deps /pnpm/store /pnpm/store
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build \
&& pnpm prune --prod
# ---- runtime ----
FROM base AS runtime
ENV NODE_ENV=production \
PORT=3000
RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser
COPY --from=build /app /app
USER 10001
EXPOSE 3000
CMD ["pnpm", "start"]
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12
ARG DEBIAN_RELEASE=bookworm
FROM python:${PYTHON_VERSION}-slim-${DEBIAN_RELEASE} AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
# ---- builder ----
FROM base AS builder
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements*.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
python -m venv /opt/venv \
&& . /opt/venv/bin/activate \
&& pip install -r requirements.txt
COPY . .
# ---- runtime ----
FROM base AS runtime
WORKDIR /app
ENV PATH="/opt/venv/bin:${PATH}" \
PORT=8000
RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
USER 10001
EXPOSE 8000
# Set a repo-specific CMD
CMD ["python", "-m", "app"]
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12
ARG DEBIAN_RELEASE=bookworm
FROM python:${PYTHON_VERSION}-slim-${DEBIAN_RELEASE} AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# ---- builder ----
FROM base AS builder
WORKDIR /app
# Install uv by copying binaries from the official distroless uv image.
# Best practice: pin to a specific uv version tag (and optionally digest).
ARG UV_VERSION=0.9.22
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /bin/
# Ensure certificates exist for TLS package downloads (PyPI, Git, etc.)
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy only dependency files first for caching
COPY pyproject.toml ./
COPY uv.lock* ./
# Create venv and sync deps (requires uv.lock for fully deterministic builds)
RUN --mount=type=cache,target=/root/.cache/uv \
uv venv /opt/venv \
&& . /opt/venv/bin/activate \
&& uv sync --frozen --no-dev --no-install-project
# Copy the rest of the app
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
. /opt/venv/bin/activate \
&& uv sync --frozen --no-dev
# ---- runtime ----
FROM base AS runtime
WORKDIR /app
ENV PATH="/opt/venv/bin:${PATH}" \
PORT=8000
# Create non-root user
RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
USER 10001
EXPOSE 8000
# Set a repo-specific CMD (examples):
# - FastAPI: ["uvicorn", "myapp.api:app", "--host=0.0.0.0", "--port=8000"]
# - CLI: ["python", "-m", "myapp"]
CMD ["python", "-m", "app"]
GitHub Actions patterns for Docker (build/test/scan/publish)
Baseline CI goals
- Build the image deterministically (BuildKit/buildx).
- Run unit/integration tests (either inside image or via compose).
- Validate compose configuration (
docker compose config). - Optionally run build checks, scan, generate SBOM/provenance, and push to registry.
Action pinning guidance
- Security-hardening option: pin actions by commit SHA.
- Maintainability option: use major version tags (e.g.,
@v3) and keep them updated.
If unsure, use major versions and add a follow-up task to pin by SHA for production repos.
Caching
- Prefer
cache-from/cache-towithtype=ghain CI. - Use
--mount=type=cachein Dockerfile for package caches.
Registry publishing
Use docker/login-action with:
- GitHub Container Registry (ghcr.io) via
GITHUB_TOKENwith scoped permissions, or - a dedicated registry token stored as a secret.
Scanning
Scanning tools vary by org; treat as optional:
- Trivy action
- Docker Scout
- Grype/Syft
Always verify the current recommended setup via Exa (official docs), since scanning actions evolve quickly.
Attestations (SBOM + provenance)
Docker’s official guidance for GitHub Actions is to use docker/metadata-action to generate tags and docker/build-push-action with:
provenance: mode=max(recommended for stronger provenance)sbom: true(SBOM isn’t automatic)
Important constraints (per Docker docs):
- Attestations require pushing to a registry (
push: true). Images loaded to the runner’s local store
don’t support attestations.
- Do not pass secrets via build args: build args can be included in provenance; use BuildKit secret mounts.
docker compose patterns
Principles
- Make dev fast (bind mounts, hot reload) without compromising prod hardening.
- Prefer explicit networks and named volumes; avoid host networking.
- Use healthchecks and condition-based dependencies where helpful.
Files strategy
Common split:
docker-compose.yml(base)docker-compose.dev.yml(dev overrides)docker-compose.prod.yml(prod overrides)
Use:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
Profiles
Profiles allow optional services:
profiles: ["dev"]for dev-only dependenciesprofiles: ["observability"]for optional tracing/log stacks
Activate profiles with:
docker compose --profile dev up- or
COMPOSE_PROFILES=dev docker compose up
Environment and .env
- Prefer
env_file:for local dev convenience. - Prefer explicit
environment:for required values. - Do not commit secrets; commit
.env.exampleinstead.
Healthchecks and dependencies
- Add
healthcheck:for critical services. - For Compose v2+,
depends_oncan be combined with service health conditions (support varies by implementation). - If uncertain, rely on app-level retries and healthchecks.
Volumes and permissions
- Named volumes for state (db data)
- Bind mounts for source code in dev
- Keep mounts narrow; avoid mounting host root.
Security knobs (Compose)
Prefer these for production-like local runs:
read_only: truetmpfs:for/tmpand other writable pathscap_drop: ["ALL"]+ minimalcap_addsecurity_opt: ["no-new-privileges:true"]- Avoid
privileged: true,pid: host,network_mode: host.
Resource sizing
For memory-bound services (DB, vector stores):
- set
deploy.resources.limits(Compose uses this mostly in Swarm; still useful as documentation) - set service-native memory flags (Postgres shared buffers, JVM heap, etc.)
Compose validation
Always run:
docker compose config(renders and validates)
Dockerfile patterns (BuildKit-first)
Goals
- Reproducible builds (pin base images/tags; optionally digests)
- Small runtime images (multi-stage; minimal runtime deps)
- Secure defaults (non-root runtime; least privilege; no secrets in layers)
- Fast builds (cache-friendly COPY order; BuildKit cache mounts)
Recommended Dockerfile header
Use the Dockerfile frontend syntax directive recommended by Docker to unlock BuildKit features:
# syntax=docker/dockerfile:1Optional (advanced): enable BuildKit “build checks” (lint/dry-run) in CI by using a check= directive or docker build --check (requires newer Buildx). Prefer documenting this choice, since it can make builds fail when new checks are introduced.
Multi-stage baseline
- builder stage: compilers, package managers, dependency resolution
- runtime stage: only runtime deps + app artifact
Common mistakes:
- Installing build tooling in the runtime stage
- Copying the whole repo before restoring dependencies (kills cache)
Pinning base images (tags vs digests)
- Prefer explicit version tags (avoid implicit
latest). - For highly reproducible production builds, pin a digest:
FROM python:3.12-slim-bookworm@sha256:...- Keep digest bumping as an explicit maintenance chore.
Use Exa (official sources) + docker buildx imagetools inspect to confirm current tags and platforms.
BuildKit cache + secrets mounts
- Cache mounts (speed up dependency installs):
RUN --mount=type=cache,target=/root/.cache ...- Secret mounts (avoid baking tokens into layers):
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc ...Never use ARG or ENV for secrets that must not leak into layers/history.
Build checks (lint/dry-run)
Docker can run “build checks” to flag common anti-patterns (like non-JSON CMD/ENTRYPOINT).
- Check without building:
docker build --check . - Fail builds on violations: add a
# check=error=truedirective (see Docker docs) or use
the BUILDKIT_DOCKERFILE_CHECK build arg.
Non-root runtime
Default to a non-root user in the final stage:
- Create user/group with stable UID/GID (helps volume permissions)
- Own only necessary directories
If you must run as root, document the reason and consider dropping privileges after startup.
Filesystem + process hardening (runtime)
Prefer these at runtime (Compose/K8s), not always in Dockerfile:
read_only: truetmpfsfor writable pathssecurity_opt: ["no-new-privileges:true"]- Drop capabilities and add back only what’s required
Healthchecks
Use one of:
HEALTHCHECKin Dockerfile (portable)healthcheck:in Compose (environment-specific)
If the app exposes HTTP, healthcheck should hit a lightweight endpoint.
Multi-arch builds (buildx)
If you publish images:
- Use
docker buildx build --platform linux/amd64,linux/arm64 ... - Prefer
buildxcache (type=ghain CI,type=locallocally) - Consider SBOM/provenance if supported by your environment.
Docker audit report template
Use this structure for reviews and refactors (adjust as needed):
Executive summary
- What’s being containerized / deployed
- Current risk level (high/medium/low) and why
- “Most important fixes” (3–5 bullets)
Inventory
- Dockerfiles found
- Compose files found
- Build/publish targets (local only vs registry)
Findings
Group by severity.
- High
- Finding + impact + proposed fix
- Medium
- Low / Info
Recommended target state
- Dockerfile strategy (multi-stage, base images, build system)
- Runtime hardening defaults (user, read-only fs, caps)
- Compose split (dev vs prod), profiles, healthchecks
- CI pipeline (build/test/scan/publish)
Validation plan (local)
docker buildx build ...docker compose configdocker compose up --build- App-specific smoke checks (endpoint/CLI)
Deliverables checklist
- [ ]
.dockerignore - [ ] Dockerfile(s)
- [ ] Compose files (+ dev/prod overrides)
- [ ] CI workflow(s)
- [ ] Documentation notes in PR description (run commands, env vars)
Container security hardening checklist
High-risk anti-patterns (fix first)
- Secrets in images or build args (
ARG TOKEN=...,ENV API_KEY=...) privileged: true,network_mode: host,pid: host- Mounting
/var/run/docker.sock - Broad host mounts (
/:/host,/etc:/etc, etc.) - Using
:latesttags or untagged images
Build hardening
- Prefer official base images; pin versions; optionally pin digests.
- Avoid
curl | shinstallers; verify checksums/signatures. - Use multi-stage builds to keep runtime minimal.
- Use BuildKit secrets for private registries and tokens.
- Keep layers small and deterministic (lockfiles, pinned deps).
- Prefer JSON array
CMD/ENTRYPOINT; consider Docker build checks to catch anti-patterns early.
Runtime hardening (Compose/K8s)
Default to:
- Non-root user (
USERin final stage;user:in Compose if needed) - Read-only root filesystem (
read_only: true) tmpfsfor writable paths (/tmp, app cache directories)- Drop Linux capabilities (
cap_drop: ["ALL"]) and add only what’s required security_opt: ["no-new-privileges:true"]
If your platform supports it, also consider:
- Seccomp/apparmor profiles
- User namespaces
Supply chain (optional but recommended)
If available in your environment:
- Vulnerability scanning (e.g., Trivy, Docker Scout)
- SBOM generation (SPDX/CycloneDX)
- Provenance attestations (SLSA-style provenance)
- Signing (cosign) and verification policies
Keep CI permissions minimal and pin Actions (ideally by commit SHA for high assurance).
Note: provenance attestations can include build argument values. Never pass secrets via build args; use secret mounts.
from __future__ import annotations
import argparse
import json
import os
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable, Literal
Severity = Literal["high", "medium", "low", "info"]
@dataclass(frozen=True)
class Finding:
severity: Severity
file: str
message: str
hint: str
IGNORE_DIR_NAMES = {
".git",
".hg",
".svn",
".venv",
"venv",
"node_modules",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"dist",
"build",
".tox",
".idea",
".vscode",
"opensrc",
"vendor",
"third_party",
"external",
}
def _walk_files(root: Path) -> Iterable[Path]:
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIR_NAMES]
base = Path(dirpath)
for filename in filenames:
yield base / filename
def _is_dockerfile_name(name: str) -> bool:
return name in {"Dockerfile", "Containerfile"} or name.startswith("Dockerfile.") or name.startswith("Containerfile.")
def _is_compose_name(name: str) -> bool:
lower = name.lower()
if lower in {"compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"}:
return True
if lower.startswith("docker-compose.") and (lower.endswith(".yml") or lower.endswith(".yaml")):
return True
return False
FROM_RE = re.compile(r"^\s*FROM\s+([^\s]+)", re.IGNORECASE)
INSTRUCTION_RE = re.compile(r"^\s*(?P<ins>CMD|ENTRYPOINT|ARG|ENV)\b(?P<rest>.*)$", re.IGNORECASE)
def _looks_like_secret_name(name: str) -> bool:
upper = name.upper()
secret_fragments = ("SECRET", "TOKEN", "PASSWORD", "PASSWD", "API_KEY", "ACCESS_KEY", "PRIVATE_KEY", "CREDENTIAL")
return any(f in upper for f in secret_fragments)
def audit_dockerfile(path: Path, root: Path) -> list[Finding]:
findings: list[Finding] = []
rel = str(path.relative_to(root))
text = path.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines()
from_images: list[str] = []
seen_cmd_or_entrypoint_json_issue = False
apt_update_seen = False
apt_lists_cleaned = False
for line in lines:
m = FROM_RE.match(line)
if m:
from_images.append(m.group(1))
if re.search(r"rm\s+-rf\s+/var/lib/apt/lists", line):
apt_lists_cleaned = True
if re.search(r"apt-get\s+update", line):
apt_update_seen = True
im = INSTRUCTION_RE.match(line)
if im:
ins = im.group("ins").upper()
rest = im.group("rest").strip()
if ins in {"CMD", "ENTRYPOINT"}:
# Best practice: JSON array form, helps signal handling and avoids shell pitfalls.
if rest and not rest.lstrip().startswith("["):
seen_cmd_or_entrypoint_json_issue = True
if ins in {"ARG", "ENV"}:
# Heuristic: flag obvious secret names being set in Dockerfile.
# - ARG NAME=... or ARG NAME
# - ENV NAME=value ... (only checks first assignment)
token = rest.split()[0] if rest else ""
name = token.split("=", 1)[0].strip()
if name and _looks_like_secret_name(name):
findings.append(
Finding(
severity="medium",
file=rel,
message=f"{ins} sets a likely secret variable '{name}' (heuristic).",
hint="Manual review: if this is a secret, do not bake it into the image. Use BuildKit secret mounts or runtime secrets.",
)
)
for img in from_images:
# Image pinning checks (heuristic)
if ":" not in img and "@" not in img:
findings.append(
Finding(
severity="medium",
file=rel,
message=f"Base image '{img}' has no explicit tag.",
hint="Pin to a tag (and optionally digest) for reproducibility; avoid implicit 'latest'.",
)
)
if img.endswith(":latest") or ":latest@" in img:
findings.append(
Finding(
severity="high",
file=rel,
message=f"Base image '{img}' uses the 'latest' tag.",
hint="Avoid 'latest' in production. Pin to an explicit version (and optionally digest).",
)
)
if re.search(r"^\s*ADD\s+", text, flags=re.IGNORECASE | re.MULTILINE):
findings.append(
Finding(
severity="low",
file=rel,
message="Uses ADD instruction.",
hint="Prefer COPY unless you explicitly need ADD features (tar auto-extract/URL).",
)
)
if re.search(r"curl\s+[^|]*\|\s*(sh|bash)", text):
findings.append(
Finding(
severity="high",
file=rel,
message="Pipes curl output to a shell.",
hint="Download to a file, verify checksum/signature, then execute.",
)
)
if re.search(r"^\s*USER\s+root\s*$", text, flags=re.IGNORECASE | re.MULTILINE):
findings.append(
Finding(
severity="medium",
file=rel,
message="Explicitly sets USER root.",
hint="Prefer a non-root runtime user; only use root for build steps when required.",
)
)
if not re.search(r"^\s*USER\s+", text, flags=re.IGNORECASE | re.MULTILINE):
findings.append(
Finding(
severity="medium",
file=rel,
message="No USER set in Dockerfile.",
hint="Set a non-root USER in the final runtime stage (or justify why root is required).",
)
)
if re.search(r"^\s*HEALTHCHECK\s+", text, flags=re.IGNORECASE | re.MULTILINE) is None:
findings.append(
Finding(
severity="info",
file=rel,
message="No HEALTHCHECK defined.",
hint="Consider adding HEALTHCHECK or enforce health via orchestrator (Compose/K8s) depending on needs.",
)
)
if "apt-get install" in text and "--no-install-recommends" not in text:
findings.append(
Finding(
severity="low",
file=rel,
message="apt-get install without --no-install-recommends (heuristic).",
hint="Use --no-install-recommends and remove apt lists to reduce image size.",
)
)
if re.search(r"^\s*COPY\s+\.?\s*\.\s*$", text, flags=re.IGNORECASE | re.MULTILINE):
findings.append(
Finding(
severity="low",
file=rel,
message="COPY . . detected (heuristic).",
hint="Ensure a strong .dockerignore; consider copying only needed files to improve cache and reduce leaks.",
)
)
if seen_cmd_or_entrypoint_json_issue:
findings.append(
Finding(
severity="low",
file=rel,
message="CMD/ENTRYPOINT not using JSON array form (heuristic).",
hint="Prefer JSON array syntax for CMD/ENTRYPOINT to improve signal handling and avoid shell pitfalls.",
)
)
if apt_update_seen and not apt_lists_cleaned:
findings.append(
Finding(
severity="low",
file=rel,
message="apt-get update detected without cleaning /var/lib/apt/lists (heuristic).",
hint="After apt installs, clean apt lists to reduce layer size: rm -rf /var/lib/apt/lists/*",
)
)
if re.search(r"apt-get\s+upgrade", text):
findings.append(
Finding(
severity="low",
file=rel,
message="apt-get upgrade detected (heuristic).",
hint="Avoid apt-get upgrade in images; prefer rebuilding regularly and pinning base images/deps.",
)
)
return findings
def audit_compose_file(path: Path, root: Path) -> list[Finding]:
findings: list[Finding] = []
rel = str(path.relative_to(root))
text = path.read_text(encoding="utf-8", errors="replace")
def flag(pattern: str, severity: Severity, message: str, hint: str) -> None:
if re.search(pattern, text, flags=re.IGNORECASE | re.MULTILINE):
findings.append(Finding(severity=severity, file=rel, message=message, hint=hint))
flag(
r"^\s*privileged:\s*true\s*$",
"high",
"Compose uses privileged: true.",
"Avoid privileged. Use capabilities/permissions narrowly; consider seccomp/apparmor/no-new-privileges.",
)
flag(
r"^\s*network_mode:\s*host\s*$",
"high",
"Compose uses network_mode: host.",
"Avoid host networking unless required; prefer explicit networks and published ports.",
)
flag(
r"^\s*pid:\s*host\s*$",
"high",
"Compose uses pid: host.",
"Avoid pid namespace sharing unless required; it weakens isolation.",
)
flag(
r"^\s*user:\s*[\"']?0([\"']|\s|$)",
"medium",
"Compose sets user: 0 (root).",
"Prefer a non-root user and set filesystem permissions appropriately.",
)
flag(
r"/var/run/docker\.sock",
"high",
"Compose mounts the Docker socket.",
"Avoid mounting docker.sock; it grants effectively root-on-host. Use a build service or scoped API if needed.",
)
flag(
r"^\s*-\s*/:\s*/",
"high",
"Compose mounts host root filesystem (/:/...).",
"Avoid broad host mounts; scope to specific directories and consider read-only mounts.",
)
flag(
r"^\s*cap_add:\s*$",
"info",
"Compose uses cap_add (manual review needed).",
"Ensure only minimal capabilities are added; prefer drop all + add a tiny set if required.",
)
if "healthcheck:" not in text:
findings.append(
Finding(
severity="info",
file=rel,
message="No healthcheck section found (heuristic).",
hint="Add healthchecks for critical services or document how health is determined.",
)
)
return findings
def main() -> int:
parser = argparse.ArgumentParser(description="Heuristic audit for Dockerfiles and Compose files.")
parser.add_argument("--root", type=Path, default=Path("."), help="Repo root (default: .)")
parser.add_argument("--json", action="store_true", help="Output JSON")
args = parser.parse_args()
root = args.root.resolve()
dockerfiles: list[Path] = []
compose_files: list[Path] = []
for p in _walk_files(root):
if not p.is_file():
continue
if _is_dockerfile_name(p.name):
dockerfiles.append(p)
if _is_compose_name(p.name):
compose_files.append(p)
findings: list[Finding] = []
for df in sorted(dockerfiles):
findings.extend(audit_dockerfile(df, root))
for cf in sorted(compose_files):
findings.extend(audit_compose_file(cf, root))
if dockerfiles and not (root / ".dockerignore").exists():
findings.append(
Finding(
severity="medium",
file=".dockerignore",
message="No .dockerignore present, but Dockerfiles exist.",
hint="Add a .dockerignore to avoid leaking secrets and to improve build performance.",
)
)
if args.json:
print(json.dumps([asdict(f) for f in findings], indent=2, sort_keys=True))
return 0
if not findings:
print("Docker Architect · Audit: no findings (heuristic).")
return 0
print("Docker Architect · Audit findings (heuristic)")
for f in findings:
print(f"- [{f.severity}] {f.file}: {f.message}")
print(f" hint: {f.hint}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import argparse
import json
import os
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class Inventory:
root: str
stacks: list[str]
dockerfiles: list[str]
compose_files: list[str]
has_dockerignore: bool
suggested_templates: list[str]
IGNORE_DIR_NAMES = {
".git",
".hg",
".svn",
".venv",
"venv",
"node_modules",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"dist",
"build",
".tox",
".idea",
".vscode",
"opensrc",
"vendor",
"third_party",
"external",
}
def _walk_files(root: Path) -> Iterable[Path]:
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIR_NAMES]
base = Path(dirpath)
for filename in filenames:
yield base / filename
def _is_dockerfile_name(name: str) -> bool:
if name in {"Dockerfile", "Containerfile"}:
return True
if name.startswith("Dockerfile.") or name.startswith("Containerfile."):
return True
return False
def _is_compose_name(name: str) -> bool:
lower = name.lower()
if lower in {"compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"}:
return True
if lower.startswith("docker-compose.") and (lower.endswith(".yml") or lower.endswith(".yaml")):
return True
return False
def _detect_stacks(files_by_name: set[str]) -> list[str]:
stacks: list[str] = []
python_markers = {
"pyproject.toml",
"requirements.txt",
"requirements-dev.txt",
"requirements-prod.txt",
"poetry.lock",
"uv.lock",
"pipfile",
"pipfile.lock",
}
node_markers = {
"package.json",
"pnpm-lock.yaml",
"yarn.lock",
"package-lock.json",
"npm-shrinkwrap.json",
}
go_markers = {"go.mod", "go.sum"}
rust_markers = {"cargo.toml", "cargo.lock"}
dotnet_markers = {".csproj", ".fsproj", ".vbproj"}
java_markers = {"pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"}
if any(m in files_by_name for m in python_markers):
stacks.append("python")
if any(m in files_by_name for m in node_markers):
stacks.append("node")
if any(m in files_by_name for m in go_markers):
stacks.append("go")
if any(m in files_by_name for m in rust_markers):
stacks.append("rust")
if any(m in files_by_name for m in java_markers):
stacks.append("java")
if any(name.endswith(tuple(dotnet_markers)) for name in files_by_name):
stacks.append("dotnet")
return stacks
def _suggest_templates(stacks: list[str], files_by_name: set[str]) -> list[str]:
suggestions: list[str] = []
if "python" in stacks:
if "uv.lock" in files_by_name or "pyproject.toml" in files_by_name:
suggestions.append("python/Dockerfile.uv")
if any(name.startswith("requirements") and name.endswith(".txt") for name in files_by_name):
suggestions.append("python/Dockerfile.pip")
if "node" in stacks:
if "pnpm-lock.yaml" in files_by_name:
suggestions.append("node/Dockerfile.pnpm")
else:
suggestions.append("node/Dockerfile.npm")
if "go" in stacks:
suggestions.append("go/Dockerfile")
suggestions.append(".dockerignore")
suggestions.append("compose/docker-compose.yml")
suggestions.append("compose/docker-compose.dev.yml")
suggestions.append("compose/docker-compose.prod.yml")
suggestions.append("compose/docker-compose.deps.yml")
suggestions.append("ci/github-actions-docker-ci.yml")
suggestions.append("ci/github-actions-docker-publish.yml")
suggestions.append("docker-bake.hcl")
# De-duplicate while preserving order
seen: set[str] = set()
out: list[str] = []
for s in suggestions:
if s not in seen:
out.append(s)
seen.add(s)
return out
def build_inventory(root: Path) -> Inventory:
root = root.resolve()
files = list(_walk_files(root))
files_by_name = {p.name.lower() for p in files}
dockerfiles = sorted(
str(p.relative_to(root))
for p in files
if _is_dockerfile_name(p.name) and p.is_file()
)
compose_files = sorted(
str(p.relative_to(root))
for p in files
if _is_compose_name(p.name) and p.is_file()
)
stacks = _detect_stacks(files_by_name)
suggested_templates = _suggest_templates(stacks, files_by_name)
return Inventory(
root=str(root),
stacks=stacks,
dockerfiles=dockerfiles,
compose_files=compose_files,
has_dockerignore=(root / ".dockerignore").exists(),
suggested_templates=suggested_templates,
)
def main() -> int:
parser = argparse.ArgumentParser(description="Inventory a repo for Docker/Compose work.")
parser.add_argument("--root", type=Path, default=Path("."), help="Repo root (default: .)")
parser.add_argument("--json", action="store_true", help="Output JSON")
args = parser.parse_args()
inventory = build_inventory(args.root)
if args.json:
print(json.dumps(asdict(inventory), indent=2, sort_keys=True))
return 0
print("Docker Architect · Inventory")
print(f"- Root: {inventory.root}")
print(f"- Stacks: {', '.join(inventory.stacks) if inventory.stacks else '(unknown)'}")
print(f"- Dockerfiles: {', '.join(inventory.dockerfiles) if inventory.dockerfiles else '(none)'}")
print(f"- Compose: {', '.join(inventory.compose_files) if inventory.compose_files else '(none)'}")
print(f"- .dockerignore: {'yes' if inventory.has_dockerignore else 'no'}")
print("- Suggested templates:")
for t in inventory.suggested_templates:
print(f" - assets/templates/{t}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
from __future__ import annotations
import argparse
import json
import os
import re
from dataclasses import dataclass
from pathlib import Path
PLACEHOLDER_RE = re.compile(r"{{\s*([A-Z0-9_]+)\s*}}")
@dataclass(frozen=True)
class RenderSpec:
template: str
out: str
variables: dict[str, str]
allow_missing: bool
chmod_x: bool
def _default_templates_dir() -> Path:
return Path(__file__).resolve().parent.parent / "assets" / "templates"
def _parse_vars(var_args: list[str]) -> dict[str, str]:
out: dict[str, str] = {}
for v in var_args:
if "=" not in v:
raise ValueError(f"Invalid --var (expected KEY=VALUE): {v}")
k, value = v.split("=", 1)
k = k.strip()
if not k:
raise ValueError(f"Invalid --var (empty key): {v}")
out[k] = value
return out
def _load_vars_file(path: Path) -> dict[str, str]:
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise ValueError("--vars-file must be a JSON object")
out: dict[str, str] = {}
for k, v in raw.items():
if not isinstance(k, str) or not isinstance(v, (str, int, float, bool)):
raise ValueError("--vars-file values must be JSON scalars")
out[k] = str(v)
return out
def render(spec: RenderSpec, templates_dir: Path) -> None:
template_path = (templates_dir / spec.template).resolve()
templates_dir = templates_dir.resolve()
if templates_dir not in template_path.parents:
raise ValueError("Template path escapes templates directory")
template_text = template_path.read_text(encoding="utf-8")
def repl(match: re.Match[str]) -> str:
key = match.group(1)
if key in spec.variables:
return spec.variables[key]
env_key = f"DOCKER_ARCH_{key}"
if env_key in os.environ:
return os.environ[env_key]
if spec.allow_missing:
return match.group(0)
raise KeyError(f"Missing variable: {key} (set --var {key}=... or env {env_key})")
rendered = PLACEHOLDER_RE.sub(repl, template_text)
if not spec.allow_missing and PLACEHOLDER_RE.search(rendered):
remaining = sorted(set(PLACEHOLDER_RE.findall(rendered)))
raise ValueError(f"Unresolved placeholders remain: {', '.join(remaining)}")
out_path = Path(spec.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(rendered, encoding="utf-8")
if spec.chmod_x:
out_path.chmod(out_path.stat().st_mode | 0o111)
def main() -> int:
parser = argparse.ArgumentParser(description="Render a Docker Architect template into a repo file.")
parser.add_argument(
"--templates-dir",
type=Path,
default=_default_templates_dir(),
help="Templates directory (default: skill assets/templates)",
)
parser.add_argument("--template", required=True, help="Template path relative to templates dir")
parser.add_argument("--out", required=True, help="Output file path")
parser.add_argument("--var", action="append", default=[], help="Template variable KEY=VALUE (repeatable)")
parser.add_argument("--vars-file", type=Path, help="JSON file with variables (merged, overridden by --var)")
parser.add_argument("--allow-missing", action="store_true", help="Leave unresolved placeholders as-is")
parser.add_argument("--chmod-x", action="store_true", help="Mark output file executable")
args = parser.parse_args()
variables: dict[str, str] = {}
if args.vars_file:
variables.update(_load_vars_file(args.vars_file))
variables.update(_parse_vars(args.var))
spec = RenderSpec(
template=args.template,
out=args.out,
variables=variables,
allow_missing=args.allow_missing,
chmod_x=args.chmod_x,
)
render(spec, templates_dir=args.templates_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Smoke-test a docker compose environment locally.
Usage:
smoke_test_compose.sh -f docker-compose.yml [-f docker-compose.dev.yml] [--project myproj]
[--service app] [--wait-seconds 30]
Performs:
- docker compose config (validation)
- docker compose up -d --build
- optional health wait for a service if it defines a healthcheck
EOF
}
files=()
project="docker-architect-smoke"
service=""
wait_seconds="30"
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--file) files+=("$2"); shift 2 ;;
--project) project="$2"; shift 2 ;;
--service) service="$2"; shift 2 ;;
--wait-seconds) wait_seconds="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ ${#files[@]} -eq 0 ]]; then
echo "At least one -f/--file is required" >&2
usage
exit 2
fi
args=(--project-name "$project")
for f in "${files[@]}"; do
args+=(-f "$f")
done
echo "Validating compose config..."
docker compose "${args[@]}" config >/dev/null
echo "Bringing up services..."
docker compose "${args[@]}" up -d --build
cleanup() {
docker compose "${args[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
if [[ -n "$service" ]]; then
echo "Waiting for service health: $service (up to ${wait_seconds}s)"
deadline=$(( $(date +%s) + wait_seconds ))
while true; do
cid="$(docker compose "${args[@]}" ps -q "$service" | head -n1 || true)"
if [[ -n "$cid" ]]; then
health="$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || true)"
state="$(docker inspect -f '{{.State.Status}}' "$cid" 2>/dev/null || true)"
if [[ "$health" == "healthy" ]]; then
break
fi
if [[ -z "$health" && "$state" == "running" ]]; then
# No healthcheck defined.
break
fi
fi
if [[ $(date +%s) -ge $deadline ]]; then
echo "Timed out waiting for $service (state=$state health=$health)" >&2
exit 1
fi
sleep 1
done
fi
echo "OK: compose is up"
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Smoke-test a single Dockerfile build/run locally.
Usage:
smoke_test_container.sh --tag myapp:smoke [--dockerfile Dockerfile] [--context .]
[--build-check] [--pull]
[--host-port 8000 --container-port 8000 --health-path /healthz]
[--env KEY=VALUE ...]
If --health-path is set, performs an HTTP GET on http://localhost:${host-port}${health-path}.
EOF
}
tag=""
dockerfile="Dockerfile"
context="."
build_check="false"
pull="false"
host_port=""
container_port=""
health_path=""
envs=()
while [[ $# -gt 0 ]]; do
case "$1" in
--tag) tag="$2"; shift 2 ;;
--dockerfile) dockerfile="$2"; shift 2 ;;
--context) context="$2"; shift 2 ;;
--build-check) build_check="true"; shift 1 ;;
--pull) pull="true"; shift 1 ;;
--host-port) host_port="$2"; shift 2 ;;
--container-port) container_port="$2"; shift 2 ;;
--health-path) health_path="$2"; shift 2 ;;
--env) envs+=("$2"); shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ -z "$tag" ]]; then
echo "Missing --tag" >&2
usage
exit 2
fi
if [[ "$build_check" == "true" ]]; then
if docker build --help 2>/dev/null | grep -q -- '--check' ; then
echo "Running Dockerfile build checks..."
docker build --check -f "$dockerfile" "$context"
else
echo "docker build --check not supported by this Docker version; skipping" >&2
fi
fi
pull_flag=()
if [[ "$pull" == "true" ]]; then
pull_flag=(--pull)
fi
echo "Building $tag ..."
docker build "${pull_flag[@]}" -f "$dockerfile" -t "$tag" "$context"
run_args=(--rm -d)
if [[ -n "$host_port" && -n "$container_port" ]]; then
run_args+=(-p "${host_port}:${container_port}")
fi
for kv in "${envs[@]}"; do
run_args+=(-e "$kv")
done
echo "Running container ..."
cid="$(docker run "${run_args[@]}" "$tag")"
cleanup() {
docker stop "$cid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
sleep 2
if [[ -n "$health_path" && -n "$host_port" ]]; then
url="http://localhost:${host_port}${health_path}"
echo "Checking health: $url"
python3 - <<PY
import sys
import time
import urllib.error
import urllib.request
url = ${url@Q}
deadline = time.time() + 30
last_err = None
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=2) as r:
if 200 <= r.status < 400:
sys.exit(0)
except Exception as e:
last_err = e
time.sleep(1)
print(f"Health check failed: {last_err}", file=sys.stderr)
sys.exit(1)
PY
fi
echo "OK: container started"
Related skills
FAQ
Does this skill handle both dev and prod container setups?
Yes, it distinguishes dev-only fast-iteration compose from prod-like immutable images with healthchecks and least privilege.
Can it add container CI?
Yes, it includes GitHub Actions templates for build, test, scan, publish, and SBOM/provenance to GHCR.