diff --git a/README.md b/README.md index 74ddab3..4abcf8a 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ docker compose config > /dev/null Extended documentation in the skill `references/` directories: -- `docker-development/references/ci-testing.md` - Comprehensive CI testing patterns +- `docker-development/references/ci-testing.md` - Retro-born CI testing gotchas - `docker-via-wsl/references/diagnosis-and-fix.md` - Diagnose and fix a wrong bind mount caused by driving Docker from a Windows shell ## Contributing diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a3864c6..47bce19 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,7 +15,7 @@ Main entry point loaded by agent frameworks. Contains: ### Reference Docs (`skills/docker-development/references/`) -- **ci-testing.md** — comprehensive CI testing patterns for container images +- **ci-testing.md** — retro-born CI testing gotchas for container images - **dind-testing-patterns.md** — Docker-in-Docker testing strategies ### Checkpoints (`skills/docker-development/checkpoints.yaml`) diff --git a/skills/docker-development/SKILL.md b/skills/docker-development/SKILL.md index 04f441f..796dae6 100644 --- a/skills/docker-development/SKILL.md +++ b/skills/docker-development/SKILL.md @@ -32,13 +32,13 @@ Patterns for building, testing, and deploying Docker containers. ### Multi-Stage Build (Node.js) ```dockerfile -FROM node:20-alpine AS builder +FROM node:24-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . -FROM node:20-alpine +FROM node:24-alpine RUN addgroup -g 1001 app && adduser -u 1001 -G app -D app USER app COPY --from=builder /app . @@ -50,7 +50,7 @@ CMD ["node", "server.js"] ### Multi-Stage Build (Go -- scratch/distroless) ```dockerfile -FROM golang:1.22-alpine AS builder +FROM golang:1.26-alpine AS builder WORKDIR /app COPY go.* ./ RUN go mod download diff --git a/skills/docker-development/references/ci-testing.md b/skills/docker-development/references/ci-testing.md index fa86877..9074067 100644 --- a/skills/docker-development/references/ci-testing.md +++ b/skills/docker-development/references/ci-testing.md @@ -1,157 +1,10 @@ # CI Testing Patterns for Docker Images -Comprehensive patterns for testing Docker images in CI/CD pipelines. +Deeper CI/CD gotchas beyond the basics. For entrypoint bypass, DNS mocking, +compose validation, and secret-scan exclusions, see SKILL.md's Quick +Reference § CI Testing Gotchas. -## The Challenge - -Docker images often have: -- **Entrypoint scripts** that run before any command -- **Service dependencies** (databases, caches) that don't exist in CI -- **Network configurations** referencing other containers -- **Required environment variables** that fail validation - -These cause CI failures that don't occur in local development. - -## Pattern 1: Entrypoint Bypass - -### Problem - -```dockerfile -# Dockerfile -ENTRYPOINT ["/entrypoint.sh"] -CMD ["php-fpm"] -``` - -```yaml -# CI - FAILS -- run: docker run --rm myimage php -v -# Output: entrypoint.sh runs, starts services, php -v never executes properly -``` - -### Solution - -```yaml -# Override entrypoint for direct command execution -- run: docker run --rm --entrypoint php myimage -v -- run: docker run --rm --entrypoint php myimage -m # List modules -- run: docker run --rm --entrypoint node myimage --version -- run: docker run --rm --entrypoint /bin/sh myimage -c "cat /etc/os-release" -``` - -### When to Use - -- Verifying installed software versions -- Checking available extensions/modules -- Testing configuration files -- Running diagnostic commands - -## Pattern 2: DNS Mocking for Upstream Services - -### Problem - -```nginx -# nginx.conf -upstream backend { - server app:9000; -} -``` - -```yaml -# CI - FAILS -- run: docker run --rm nginx-image nginx -t -# Error: host not found in upstream "app:9000" -``` - -### Solution - -```yaml -# Provide fake DNS resolution for upstream hosts -- run: | - docker run --rm \ - --add-host app:127.0.0.1 \ - --add-host database:127.0.0.1 \ - --add-host cache:127.0.0.1 \ - nginx-image nginx -t -``` - -### Multiple Upstreams - -```yaml -- name: Test nginx config - run: | - docker run --rm \ - --add-host php-fpm:127.0.0.1 \ - --add-host mailpit:127.0.0.1 \ - --add-host redis:127.0.0.1 \ - myapp-nginx nginx -t -``` - -## Pattern 3: Docker Compose Validation - -### Problem - -```yaml -# compose.yml -services: - app: - environment: - - DB_PASSWORD=${DB_PASSWORD:?Required} -``` - -```yaml -# CI - FAILS -- run: docker compose config -# Error: required variable DB_PASSWORD is missing -``` - -### Solution - -```yaml -- name: Create test environment - run: | - cp .env.example .env - # Replace all CHANGE_ME placeholders - sed -i 's/CHANGE_ME_[A-Z_]*/test_password/g' .env - -- name: Validate compose syntax - run: docker compose config > /dev/null -``` - -### Alternative: Inline Variables - -```yaml -- name: Validate compose - env: - DB_PASSWORD: test - REDIS_PASSWORD: test - run: docker compose config > /dev/null -``` - -## Pattern 4: Health Check Verification - -### Test Health Check Command - -```yaml -- name: Build image - run: docker build -t myapp:test . - -- name: Test health check - run: | - # Start container - docker run -d --name test-container myapp:test - - # Wait for health - timeout 60 bash -c 'until docker inspect test-container --format="{{.State.Health.Status}}" | grep -q healthy; do sleep 2; done' - - # Verify - docker inspect test-container --format="{{.State.Health.Status}}" - -- name: Cleanup - if: always() - run: docker rm -f test-container -``` - -### Worker/Sidecar Services That Reuse an App Image +## Pattern 1: Worker/Sidecar Services That Reuse an App Image A compose service that reuses the app image (e.g. a queue worker on the php-fpm+nginx web image) **inherits the image's baked-in `HEALTHCHECK`**. @@ -191,93 +44,9 @@ Prefer letting a worker exit on its own limits (`exec` the daemon as PID 1, restart policy with backoff) over in-container `while true ... || true` loops that mask fatal errors from orchestration. -## Pattern 5: Secret Scanning with Exclusions +## Pattern 2: GitLab CI — image entrypoint must be a shell (or be overridden) -### Problem - -Documentation references placeholder passwords: - -```markdown - -Set `DB_PASSWORD=CHANGE_ME` in your .env file -``` - -```yaml -# CI - FALSE POSITIVE -- run: git ls-files | xargs grep -l "CHANGE_ME" && exit 1 -# Fails on README.md, QUICKSTART.md, etc. -``` - -### Solution - -```yaml -- name: Check for leaked secrets - run: | - # Files that legitimately reference placeholders - EXCLUDE_PATTERN=".env.example|README|QUICKSTART|docs/|Makefile|\.github/" - - # Find files with secrets, excluding legitimate references - FOUND=$(git ls-files | xargs grep -l "CHANGE_ME" | grep -vE "$EXCLUDE_PATTERN" || true) - - if [ -n "$FOUND" ]; then - echo "Found secrets in:" - echo "$FOUND" - exit 1 - fi - echo "No leaked secrets detected" -``` - -## Pattern 6: Multi-Platform Build Testing - -```yaml -- name: Set up QEMU - uses: docker/setup-qemu-action@v3 - -- name: Set up Buildx - uses: docker/setup-buildx-action@v3 - -- name: Build multi-platform - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - load: false # Can't load multi-platform - cache-from: type=gha - cache-to: type=gha,mode=max -``` - -## Pattern 7: Integration Testing with Service Containers - -```yaml -jobs: - test: - services: - database: - image: mariadb:11 - env: - MYSQL_ROOT_PASSWORD: test - MYSQL_DATABASE: testdb - options: >- - --health-cmd="healthcheck.sh --connect --innodb_initialized" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - - steps: - - name: Build app image - run: docker build -t myapp:test . - - - name: Run integration tests - run: | - docker run --rm \ - --network ${{ job.container.network }} \ - -e DB_HOST=database \ - -e DB_PASSWORD=test \ - myapp:test npm test -``` - -## Pattern 8: GitLab CI — image entrypoint must be a shell (or be overridden) - -Unlike a test-time `--entrypoint` bypass (Pattern 1), GitLab **runs every job's `script:` via `sh -c`**. If the image used as a job `image:` has a non-shell `ENTRYPOINT ["mytool"]`, the runner effectively runs `mytool sh -c '…'` → **`No such command 'sh'`**, and the job fails before the script runs. +Unlike a test-time `--entrypoint` bypass, GitLab **runs every job's `script:` via `sh -c`**. If the image used as a job `image:` has a non-shell `ENTRYPOINT ["mytool"]`, the runner effectively runs `mytool sh -c '…'` → **`No such command 'sh'`**, and the job fails before the script runs. ```yaml job: @@ -290,7 +59,7 @@ job: A CLI image also meant for `docker run mytool …` can keep `ENTRYPOINT ["mytool"]`, but **document** that GitLab consumers must set `entrypoint: [""]`. If the image is *primarily* a CI image, prefer no tool entrypoint (use `CMD`). -## Pattern 9: Restricted runner egress — bundle external assets at build time +## Pattern 3: Restricted runner egress — bundle external assets at build time CI runners (especially internal/self-hosted) often have **no outbound internet**. An image that fetches something at *runtime* (`page.add_script_tag(url="https://cdn…/axe.min.js")`, `curl https://…` in the entrypoint, a remote `pip`/`npm` install) works locally but fails in CI. @@ -312,7 +81,7 @@ ENV AXE_PATH=/opt/axe-core/axe.min.js …and have the app prefer the local file (CDN as a dev-only fallback). -## Pattern 10: Test the *built image*, not just the editable dev install +## Pattern 4: Test the *built image*, not just the editable dev install A non-editable install in the image (`pip install .`, `npm install `) does not behave like the editable/dev checkout your tests ran against. Classic failure: **data files resolved by walking from `__file__`** (`Path(__file__).resolve().parents[2]/"data"/…`) don't exist under `site-packages`, so the tool can't find its catalog/config inside the container even though `pytest` was green. @@ -325,7 +94,7 @@ A non-editable install in the image (`pip install .`, `npm install `) d - run: docker run --rm app:test render fixture.json /tmp/out # real command, end-to-end ``` -## Pattern 11: Bake targets must inherit `docker-metadata-action` +## Pattern 5: Bake targets must inherit `docker-metadata-action` ### Problem @@ -380,62 +149,6 @@ docker buildx bake --print docker buildx bake -f docker-bake.hcl -f /tmp/metadata-bake.json --print ``` -## Complete CI Workflow Example - -```yaml -name: Docker CI - -on: [push, pull_request] - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup test environment - run: | - cp .env.example .env - sed -i 's/CHANGE_ME_[A-Z_]*/ci_test_value/g' .env - - - name: Validate compose - run: docker compose config > /dev/null - - build-and-test: - runs-on: ubuntu-latest - needs: validate - steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-buildx-action@v3 - - - name: Build images - run: docker compose build - - - name: Test PHP version - run: docker run --rm --entrypoint php myapp:latest -v | grep "8.4" - - - name: Test nginx config - run: docker run --rm --add-host app:127.0.0.1 myapp-nginx nginx -t - - - name: Start stack - run: | - cp .env.example .env - sed -i 's/CHANGE_ME_[A-Z_]*/ci_test/g' .env - docker compose up -d - - - name: Wait for healthy - run: | - timeout 120 bash -c 'until docker compose ps | grep -q healthy; do sleep 5; done' - - - name: Test endpoint - run: curl -f http://localhost/ || exit 1 - - - name: Cleanup - if: always() - run: docker compose down -v -``` - ## Local boot-test pitfalls When smoke/boot-testing an image by hand (not in the CI matrix):