| name | Docker Build and Test Workflow |
| description | Use this skill when building, testing, or working with Docker images in the agentic-container repository. Covers when to rebuild vs reuse images, how to leverage layer caching, and efficient iteration patterns. |
| allowed-tools | Bash, Read, Grep, Glob |
Docker Build and Test Workflow
This skill provides guidance for efficiently building, testing, and working with Docker images in this repository.
Core Principles
- Scripts rebuild by design - The testing scripts prioritize reliability over speed by rebuilding images
- Docker layer caching is your friend - Rebuilds are fast when only small changes occur
- Avoid unnecessary script invocations - Reuse existing images when possible
- Manual cache management - Only delete images as a last resort
Understanding Script Behavior
What the Scripts Actually Do
scripts/build-local.sh [target]
- Builds specified target (standard, dev, or stage)
- Relies on Docker layer caching for speed
- Securely handles GitHub token via
gh auth - Always performs a build, but uses cache when available
scripts/test-dockerfile.sh [target|dockerfile] [--cleanup]
- For base targets (standard/dev): Always rebuilds via
build-local.sh - For cookbooks: Smart about base (
ensure_base_image()checks timestamps), but always rebuilds cookbook - Runs comprehensive goss validation tests
- Keeps test images by default (use
--cleanupto remove)
scripts/shell.sh [target|cookbook] [tag]
- Always rebuilds the specified target or cookbook
- Launches interactive shell in the container
- Auto-cleans container on exit (but keeps image)
The One Smart Cache Check
The ensure_base_image() function (used for cookbook testing):
- Checks if
agentic-container:latestexists - Compares Dockerfile modification time vs image creation time
- Only rebuilds base if Dockerfile is newer or image missing
- This is the ONLY automatic cache optimization in the scripts
Efficient Workflow Patterns
Pattern 1: Initial Build + Iterative Testing
# Step 1: Initial build and test (will rebuild)
./scripts/test-dockerfile.sh standard
# Step 2: Iterate on goss tests WITHOUT rebuilding
# Edit goss/standard.yaml, then:
docker run --rm --user root \
-v "$PWD/goss/base-common.yaml:/tmp/goss-base-common.yaml:ro" \
-v "$PWD/goss/standard.yaml:/tmp/goss-base.yaml:ro" \
test-standard:latest \
bash -c 'goss -g /tmp/goss-base-common.yaml -g /tmp/goss-base.yaml validate --format documentation'
# Step 3: Final test with script (rebuilds to verify)
./scripts/test-dockerfile.sh standard --cleanup
Pattern 2: Check Before Rebuilding
# Check what images exist
docker images | grep -E '(agentic|test-)'
# If test-standard:latest exists, use it directly
docker run --rm test-standard:latest python --version
# Only rebuild if you changed the Dockerfile
./scripts/build-local.sh standard agentic-container:latest
Pattern 3: Cookbook Development
# Initial build and test (smart about base, rebuilds cookbook)
./scripts/test-dockerfile.sh docs/cookbooks/python-cli/Dockerfile
# The script built: test-dockerfile-TIMESTAMP image
# Find it:
docker images | grep test-dockerfile
# Debug with existing image (no rebuild)
docker run --rm -it test-dockerfile-1234567890 bash
# When done iterating, run final test with cleanup
./scripts/test-dockerfile.sh docs/cookbooks/python-cli/Dockerfile --cleanup
Pattern 4: Quick Validation
# Instead of shell.sh (which rebuilds), use existing image:
docker run --rm -it test-standard:latest bash
# Or run a quick command:
docker run --rm test-standard:latest mise list
Pattern 5: Prototyping Dockerfile Commands
When adding complex commands to Dockerfile, test them first in existing image:
# Step 1: Test interactively to develop the command
docker run --rm -it --user root test-dev:latest bash
# Inside container, try commands until they work:
# $ mkdir -p /some/path
# $ ln -sf source target
# $ command --version
# $ exit
# Step 2: Test as one-liner (how Dockerfile RUN works)
docker run --rm --user root test-dev:latest bash -c '
mkdir -p /some/path && \
ln -sf source target && \
command --version
'
# Step 3: If successful, add to Dockerfile
RUN mkdir -p /some/path \
&& ln -sf source target
# Step 4: Rebuild and verify changes persisted
./scripts/test-dockerfile.sh dev
docker run --rm test-dev:latest command --version
Why This Matters:
- Catches path issues, permission problems, and syntax errors before build
- Validates that commands work in the actual environment
- Much faster than rebuild cycles for iteration
- Helps understand what files/directories already exist
Common Use Cases:
- Creating symlinks (test paths are correct)
- Setting up configuration files
- Verifying package installations work
- Testing permission requirements
Pattern 6: Debugging Multi-Stage Builds
When packages or files are missing from final image but work in build stage:
# Step 1: Build the intermediate stage directly
./scripts/build-local.sh npm-globals-stage test-npm-globals:latest
# Step 2: Inspect what the stage actually contains
docker run --rm test-npm-globals:latest bash -c 'ls -la /path/to/expected/files'
# Step 3: Check for symlinks (Docker COPY follows them!)
docker run --rm test-npm-globals:latest bash -c 'ls -la /path/to/bin/'
# Step 4: Compare to final image
docker run --rm test-dev:latest bash -c 'ls -la /path/to/bin/'
# Step 5: Identify what's different
# - Symlinks become regular files when COPY'd
# - Permissions may change
# - Files might be in different locations
Common Multi-Stage Issues:
- Docker COPY follows symlinks - Converts them to regular files, breaking relative paths
- Files exist in stage but not in final - Check COPY commands copy the right paths
- Permissions change between stages - RUN commands in final stage may run as different user
- ARG not passed to stage - Re-declare ARG in each stage that needs it
Solution Patterns:
- For symlinks: Recreate them in final stage with RUN command
- For missing files: Verify COPY source path includes the files
- For permissions: Run chmod in final stage after COPY
When Rebuilds Are Required
You MUST Rebuild When:
- Dockerfile content changed (RUN, COPY, ADD, etc.)
- ARG versions updated (NODE_VERSION, PYTHON_VERSION, etc.)
- Base image dependencies changed
- Files copied into image (COPY/ADD) have changed
Layer Cache Makes These Fast When:
- Only bottom layers changed (Dockerfile ordered well)
- System packages unchanged (apt-get install cached)
- Language installations unchanged (mise installations cached)
- Only scripts or configs changed
You DON'T Need to Rebuild When:
- Only goss test files changed (mount them at test time)
- Documentation changed (*.md files)
- CI configuration changed (.github/workflows/*.yml)
- Comments in Dockerfile changed
Layer Cache Optimization
Understanding Image vs Layer Cache
CRITICAL: Deleting an image DOES NOT clear layer cache!
# This removes the image tag but layers persist:
docker rmi test-dev:latest
# Build will still show CACHED for unchanged layers:
./scripts/test-dockerfile.sh dev
# To actually clear layer cache:
docker builder prune -f
# Nuclear option (clears everything including unused images):
docker system prune -f && docker builder prune -f
Why This Matters:
- Docker stores layers separately from image tags
- Multiple images can share the same layers
- Removing an image only removes the tag, not the layers
- Layer cache persists across branches and image deletions
Understanding Layer Invalidation
Docker rebuilds from the first changed layer onward. Order matters:
# ✅ GOOD: Infrequently changing items first
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl
ARG NODE_VERSION=24.8.0
RUN mise use -g node@${NODE_VERSION}
COPY scripts/ /usr/local/bin/ # Changes frequently
# ❌ BAD: Frequently changing items first
FROM ubuntu:24.04
COPY scripts/ /usr/local/bin/ # Changes frequently, invalidates all below
RUN apt-get update && apt-get install -y curl
Checking Cache Effectiveness
# Watch build output for "CACHED" vs "RUN" steps
./scripts/build-local.sh standard agentic-container:test | grep -E '(CACHED|RUN|COPY)'
# If you see mostly CACHED, layer cache is working well
# If you see mostly RUN, something early in Dockerfile changed
When Layer Cache Causes Problems
Symptom: Build shows "CACHED" but you changed the Dockerfile Cause: Layer hash collision or cache from different branch/state
Solutions:
- Make a small change to force cache bust - Add/modify a comment in the RUN command
- Clear builder cache -
docker builder prune -f(fast, selective) - Use --no-cache - Only as last resort (slowest, rebuilds everything)
Example Cache Bust:
# Before (keeps showing CACHED even after adding commands):
RUN mise use -g node@${NODE_VERSION} \
&& your-new-commands-here
# After (change comment to bust cache):
# v2: Added symlink creation
RUN mise use -g node@${NODE_VERSION} \
&& your-new-commands-here
Verifying Changes Persisted:
# Check if your changes made it into the image:
docker history test-dev:latest --no-trunc | grep "your-command"
# Or inspect the actual files:
docker run --rm test-dev:latest ls -la /path/to/your/files
Working with Test Images
Test Image Naming Conventions
- Base targets in local mode:
test-standard:latest,test-dev:latest - Cookbooks in local mode:
test-dockerfile-TIMESTAMP(timestamped) - CI mode: Pre-built images with specific tags
Retaining vs Cleaning Up
# Default: Keep test images for inspection
./scripts/test-dockerfile.sh standard
docker run --rm -it test-standard:latest bash # Debug it
# Cleanup when done
docker rmi test-standard:latest
# Or use --cleanup flag (auto-removes after tests pass)
./scripts/test-dockerfile.sh standard --cleanup
Troubleshooting
RUN Command Not Working
Symptom: Added commands to Dockerfile but they don't seem to execute or changes don't persist
Debugging Steps:
# 1. Check if command is in image history
docker history test-dev:latest --no-trunc | grep "your-command"
# 2. If found, check if changes actually exist in image
docker run --rm test-dev:latest ls -la /path/to/expected/files
# 3. If not found or layer shows CACHED, try cache bust
# Edit Dockerfile: add/change a comment in the RUN command
# 4. Test the command manually first (Pattern 5)
docker run --rm --user root test-dev:latest bash -c 'your-command'
Common Causes:
- Layer cache hiding your changes (try cache bust)
- Command fails silently in
&&chain (test commands individually) - Wrong user (check if RUN runs as root but final image is non-root)
- Path issues (prototype in existing image first)
Files Missing in Final Image
Symptom: Files exist in build stage but not in final multi-stage image
Debugging Steps:
# 1. Build and inspect the intermediate stage
./scripts/build-local.sh your-stage test-stage:latest
docker run --rm test-stage:latest ls -la /expected/path
# 2. Compare to final image
docker run --rm test-dev:latest ls -la /expected/path
# 3. Check if symlinks are involved
docker run --rm test-stage:latest bash -c 'ls -la /path/ | grep "^l"'
Common Causes:
- Docker COPY follows symlinks (recreate them in final stage)
- COPY path doesn't include the files you expect
- Files in different location than you think
- ARG not declared in the stage where COPY happens
Build Shows CACHED But You Changed Dockerfile
Symptom: Modified Dockerfile but build output shows "CACHED" for your layer
Solutions:
# Option 1: Force cache bust with small change
# Add or modify a comment in the RUN command
# Option 2: Clear builder cache
docker builder prune -f
# Option 3: Verify your change is actually different
git diff Dockerfile # Did the change actually save?
Why This Happens:
- Docker layer cache persists beyond image deletion
- Layer hash collision (rare but possible)
- Changes don't affect layer hash (comment-only changes)
Build is Slow
- Check if layer cache is working: Look for "CACHED" in build output
- Identify what changed: Compare to last build
- Consider if rebuild is necessary: Can you test with existing image?
- Last resort: This is normal for FROM scratch builds or major changes
Tests Failing After Dockerfile Changes
- Test with existing image first: Isolate if it's a Dockerfile vs test issue
- Check goss test validity: Can tests pass with old image?
- Rebuild and test:
./scripts/test-dockerfile.sh
Image Doesn't Exist
# Check what you have
docker images | grep -E '(agentic|test-)'
# Build what you need
./scripts/test-dockerfile.sh dev # Builds test-dev:latest
Cache Seems Corrupted
Symptoms:
- Build errors with "snapshot does not exist"
- Inconsistent build results
- Cache-related build failures
Only as last resort:
# Nuclear option: clear all cache and rebuild
docker system prune -f && docker builder prune -f
# Rebuild from scratch (will take several minutes)
./scripts/test-dockerfile.sh dev
Common Commands Reference
Building
# Build base standard target
./scripts/build-local.sh standard agentic-container:latest
# Build dev target
./scripts/build-local.sh dev agentic-container:dev
# Build specific stage
./scripts/build-local.sh python-stage python-only:latest
Testing
# Test base target (rebuilds)
./scripts/test-dockerfile.sh standard
# Test with cleanup
./scripts/test-dockerfile.sh standard --cleanup
# Test cookbook
./scripts/test-dockerfile.sh docs/cookbooks/python-cli/Dockerfile
# CI mode (use pre-built image)
./scripts/test-dockerfile.sh standard test-standard:latest
Interactive Debugging
# Using shell.sh (rebuilds)
./scripts/shell.sh standard
# Using existing image (no rebuild)
docker run --rm -it test-standard:latest bash
# Run a command in existing image
docker run --rm test-standard:latest python --version
Image Management
# List all images
docker images | grep -E '(agentic|test-)'
# Remove test images
docker rmi $(docker images -q 'test-*')
# Check image age
docker image inspect agentic-container:latest --format '{{.Created}}'
# Check Dockerfile age
ls -l Dockerfile
Debugging
# Inspect layer history
docker history test-dev:latest --no-trunc | grep "your-command"
# Test command manually as root
docker run --rm --user root test-dev:latest bash -c 'your-command'
# Interactive debugging session
docker run --rm -it --user root test-dev:latest bash
# Check if file exists in image
docker run --rm test-dev:latest ls -la /path/to/file
# Check for symlinks
docker run --rm test-dev:latest bash -c 'ls -la /path/ | grep "^l"'
# Compare files between stages
docker run --rm test-stage:latest ls -la /path
docker run --rm test-dev:latest ls -la /path
Cache Management
# Clear builder cache (recommended)
docker builder prune -f
# Clear all Docker cache (nuclear option)
docker system prune -f && docker builder prune -f
# Check builder cache size
docker system df
# Remove specific image (doesn't clear layers!)
docker rmi test-dev:latest
Decision Tree
Need to work with Docker image?
├─ Developing new Dockerfile commands?
│ ├─ Step 1: Prototype in existing image (Pattern 5)
│ ├─ Step 2: Add to Dockerfile
│ └─ Step 3: Test with ./scripts/test-dockerfile.sh
│
├─ RUN command not working as expected?
│ ├─ Check: docker history IMAGE | grep "command"
│ ├─ Test manually: docker run --user root IMAGE bash -c 'command'
│ └─ If CACHED: Add comment to bust cache
│
├─ Files missing in final image?
│ ├─ Build intermediate stage: ./scripts/build-local.sh STAGE
│ ├─ Inspect: docker run STAGE-IMAGE ls -la /path
│ └─ Check for symlinks: ls -la | grep "^l"
│
├─ Build shows CACHED but you changed Dockerfile?
│ ├─ Try: Modify a comment in the RUN command
│ ├─ Or: docker builder prune -f
│ └─ Verify: docker history IMAGE --no-trunc
│
├─ Making Dockerfile changes?
│ ├─ Yes → Run test-dockerfile.sh (will rebuild with cache)
│ └─ No ↓
│
├─ Making goss test changes only?
│ ├─ Yes → Use existing image with docker run + volume mounts
│ └─ No ↓
│
├─ Need interactive shell?
│ ├─ Image exists? → docker run -it IMAGE bash
│ └─ No image → ./scripts/test-dockerfile.sh TARGET
│
├─ Testing if something works?
│ ├─ Image exists? → docker run IMAGE your-test-command
│ └─ No image → ./scripts/test-dockerfile.sh TARGET
│
└─ Just want to build?
└─ ./scripts/test-dockerfile.sh TARGET (preferred, includes tests)
└─ Or: ./scripts/build-local.sh TARGET TAG (build only)
Key Takeaways
- First run is always slower - Subsequent runs use layer cache
- Scripts prioritize correctness - They rebuild to ensure clean state
- Reuse images between script runs - Docker's layer cache + manual reuse
- Don't fight the scripts - They're designed to be reliable, not fast
- Layer cache is automatic - You get it for free with Docker
- Manual cache management is rare - Only needed when truly broken
- Test iterations don't need rebuilds - Mount goss files and test directly
When to Use This Skill
Invoke this skill when:
- User asks to build or test Docker images
- User asks how to iterate on Dockerfiles efficiently
- User complains about slow builds
- User asks about Docker cache
- User asks which script to use (build-local.sh vs test-dockerfile.sh vs shell.sh)
- User wants to test changes without rebuilding
- User needs to debug Docker images
- RUN command not working - Guide to test commands manually before adding to Dockerfile
- Files missing in final image - Debug multi-stage builds by inspecting intermediate stages
- Build shows CACHED but Dockerfile changed - Explain image vs layer cache difference
- Symlinks or special files not working - Docker COPY follows symlinks behavior
- Permission errors in containers - Test with --user root to debug
- Prototyping new features - Use existing images to test before modifying Dockerfile
- Multi-stage build debugging - Build and inspect intermediate stages