Docker Development Skill
Production-grade patterns for building, testing, and deploying Docker container images.
When to Use
This skill activates when working with:
- •Dockerfile - Building custom container images
- •docker-compose.yml / compose.yml - Multi-container orchestration
- •docker-bake.hcl - BuildKit bake files for multi-platform builds
- •.dockerignore - Build context optimization
- •CI/CD pipelines - Testing and publishing container images
Core Principles
- •Minimal images - Use Alpine/distroless, multi-stage builds
- •Security first - Non-root users, no secrets in layers
- •Testable - Images must be verifiable in CI
- •Reproducible - Pin versions, use checksums
Dockerfile Best Practices
Multi-Stage Builds
# Build stage - has build tools FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # Runtime stage - minimal FROM node:20-alpine RUN addgroup -g 1001 app && adduser -u 1001 -G app -D app USER app COPY --from=builder /app/node_modules ./node_modules COPY --chown=app:app . . CMD ["node", "server.js"]
Layer Optimization
# Bad - each RUN creates a layer, leaves cache
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Good - single layer, cleanup included
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Build Arguments vs Environment
# Build-time only (not in final image)
ARG BUILD_VERSION
RUN echo "Building version ${BUILD_VERSION}"
# Runtime configuration
ENV APP_PORT=8080
EXPOSE $APP_PORT
Docker Bake (BuildKit)
For multi-platform builds, use docker-bake.hcl:
group "default" {
targets = ["app"]
}
target "app" {
dockerfile = "Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["myapp:latest"]
cache-from = ["type=gha"]
cache-to = ["type=gha,mode=max"]
}
target "app-dev" {
inherits = ["app"]
target = "development"
tags = ["myapp:dev"]
}
Build with: docker buildx bake
Testing Docker Images in CI
Critical patterns learned from production failures:
1. Bypass Entrypoint for Direct Testing
When images have entrypoint scripts, they execute before any test command:
# WRONG - entrypoint runs, interferes with test - run: docker run --rm myimage php -v # CORRECT - bypass entrypoint for direct command - run: docker run --rm --entrypoint php myimage -v - run: docker run --rm --entrypoint node myimage --version
2. Mock DNS for Upstream Servers
nginx/haproxy configs with upstream servers fail nginx -t in isolation:
# WRONG - fails with "host not found in upstream" - run: docker run --rm nginx-image nginx -t # CORRECT - provide fake DNS resolution - run: docker run --rm --add-host backend:127.0.0.1 nginx-image nginx -t
3. Docker Compose Validation
When compose.yml uses required variables (${VAR:?error}), create .env first:
- name: Create test environment
run: |
cp .env.example .env
sed -i 's/CHANGE_ME_PASSWORD/test_ci_password/g' .env
- name: Validate compose syntax
run: docker compose config > /dev/null
4. Secret Scanning Exclusions
Documentation legitimately references placeholder passwords:
- name: Check for leaked secrets
run: |
EXCLUDE=".env.example|README|QUICKSTART|docs/"
if git ls-files | xargs grep -l "CHANGE_ME" | grep -vE "$EXCLUDE"; then
echo "Found secrets in tracked files"
exit 1
fi
For comprehensive CI testing patterns, see references/ci-testing.md.
Docker Compose Patterns
Health Checks and Dependencies
services:
app:
depends_on:
database:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s # Grace period for slow startups
Network Isolation
networks:
frontend:
name: ${PROJECT}_frontend
backend:
name: ${PROJECT}_backend
internal: true # No external access
services:
nginx:
networks: [frontend, backend] # Bridge
app:
networks: [frontend, backend] # Needs internet + internal
database:
networks: [backend] # Internal only
Optional Services with Profiles
services:
mailpit:
profiles: [dev] # Only starts with --profile dev
debug-tools:
profiles: [debug]
CI/CD Workflow Pattern
name: Docker Build
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
load: true # Load for testing
tags: myimage:test
cache-from: type=gha
cache-to: type=gha,mode=max
# Test the built image
- name: Verify image
run: |
docker run --rm --entrypoint /bin/sh myimage:test -c "echo 'Image works'"
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/${{ github.repository }}:latest
Security Scanning
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myimage:test
format: 'table'
exit-code: '1' # Fail on HIGH/CRITICAL
severity: 'CRITICAL,HIGH'
.dockerignore Best Practices
Always create .dockerignore to optimize build context:
# Version control .git .gitignore # Dependencies (rebuild in container) node_modules vendor __pycache__ # IDE and editor files .idea .vscode *.swp # Test and documentation tests docs *.md !README.md # CI/CD .github .gitlab-ci.yml # Local environment .env .env.* !.env.example docker-compose.override.yml
References
- •
references/ci-testing.md- Comprehensive CI testing patterns for Docker images