Version: 0.3.0 Last Updated: 2026-03-22 Audience: DevOps engineers, platform teams, container builders Difficulty: Intermediate
Table of Contents
- What cleanimg-customize Is
- Installation
- Core Concepts
- Workflow Overview
- Subcommand Reference
- Per-Application Examples
- Multi-Architecture Builds
- CI/CD Integration
- Limitations and Workarounds
- Troubleshooting
- What to Read Next
1. What cleanimg-customize Is
The Declarative Image Builder tool, cleanimg-customize, is a declarative Rust CLI tool that transforms an IncrementalSpec YAML file into a production-ready multi-stage Dockerfile, then optionally builds and pushes the resulting image. It bridges the gap between simple base images and application-specific customizations—no hand-written Dockerfiles required.
Instead of manually authoring multi-stage builds, you declare what you want (packages, artifacts, config files, users) in YAML, and cleanimg-customize generates a Dockerfile that: Uses a development image for package installation (APK or APT) Copies artifacts into the distroless production image Manages permissions, users, and groups Sets environment variables, labels, and resource hints Supports per-architecture overrides and custom run commands
Key Characteristics
Spec-Driven: Declarative YAML (IncrementalSpec) replaces manual Dockerfile authoring Multi-Stage Builder Pattern: Automatically uses -dev image for building, -prod image for runtime Package Manager Auto-Detection: Detects APK (Alpine/CleanStart) or APT (Debian/Ubuntu) automatically Artifact Support: Injects JARs, shared libraries, wheels, Node modules, config files Distroless-Ready: Produces minimal, shell-less production images Multi-Architecture: Supports amd64/arm64 with per-arch package and environment overrides Container-Based: Distributed as a single container image, no installation hassle Backward Compatible: v0.2.0 specs work unchanged (all new fields have #[serde(default)])
When to Use cleanimg-customize
Use cleanimg-customize when you need to: Build custom CleanStart or Alpine images with specific packages Inject application artifacts (JARs, shared libraries, Python wheels) Add TLS certificates, config files, or credentials at build time Create multi-stage images (build with dev, ship with prod) Define users, groups, and permissions declaratively Support multiple architectures (ARM64 and x86-64) from one spec Integrate image builds into CI/CD with minimal scripting
Do NOT use cleanimg-customize for: Runtime container initialization (use cleanimg-init instead) Secrets management (mount secrets at build/runtime, don't bake into images) Full OS customization (use custom base images) Frequent iterative development (use regular Dockerfiles locally, deploy with cleanimg-customize)
2. Installation
Using the Container Image
cleanimg-customize is distributed as a container image. No local installation needed.
Image: us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0
Supported Architectures: linux/amd64, linux/arm64
Image Digests (v0.3.0, deployed 2026-03-22)
Reference these exact digests for maximum reproducibility:
# Multi-arch (automatic selection):us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0sha256:f5436acd3a99 # amd64 specific:us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0-amd64sha256:7d4a65a1052e # arm64 specific:us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0-arm64sha256:e88f932a01f2Quick Setup: Create an Alias
For convenience, add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):
alias cleanimg-customize='docker run --rm \ -v "$PWD":/workspace \ -v /var/run/docker.sock:/var/run/docker.sock \ us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0'Then use as if it were installed locally:
cleanimg-customize generate --base-image alpine:3.19 --package curlcleanimg-customize build --spec spec.yaml --tag myregistry/myapp:v1.0Verify Installation
docker run --rm us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 --version# Output: cleanimg-customize v0.3.0Authentication (Private Registries)
If your registry requires authentication:
# Authenticate to Artifact Registrygcloud auth configure-docker us-central1-docker.pkg.dev # Or docker login for Docker Hub, etc.docker login3. Core Concepts
IncrementalSpec: The Declarative Schema
The IncrementalSpec YAML file is the single source of truth. It declares:
base_image: us-central1-docker.pkg.dev/my-org/images/cleanstart-prod:0.2.5arch: arm64variant: prodpackage_manager: apk # auto-detected if omitted packages: - name: curl version: "8.5.0-r0" # optional - name: openssl multistage: true # or auto-detected from presence of packages/artifacts copy_from_builder: - src: /usr/local/lib/libcustom.so dst: /usr/local/lib/ chown: root:root artifacts: - type: jar maven_coords: "com.example:auth-lib:2.1.0" destination: /opt/lib/auth-lib.jar permissions: "0644" copy_files: - source: ./config/app.yaml destination: /etc/myapp/config.yaml permissions: "0644" - source: ./certs/ca-bundle.crt destination: /etc/ssl/certs/custom-ca.crt permissions: "0644" groups: - groupname: appgroup gid: 2001 users: - username: appuser uid: 2001 gid: 2001 home: /app shell: /sbin/nologin env_vars: APP_ENV: "production" APP_LOG_LEVEL: "info" labels: maintainer: "platform-team@example.com" version: "1.2.3" resource_hints: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1000m" arch_overrides: arm64: packages: - name: openssl-libcrypto3-compat env_vars: ARCH_SPECIFIC: "arm64-value" amd64: env_vars: ARCH_SPECIFIC: "amd64-value" workdir: /appuser: appuser:appgroupentrypoint: ["/opt/bin/myapp"]cmd: ["--config", "/etc/myapp/config.yaml"] generate_sbom: trueAll new v0.3.0 fields use Rust's #[serde(default)], so v0.2.0 specs are fully valid—defaults are applied automatically.
PackageManager Auto-Detection
cleanimg-customize automatically detects the package manager based on the base image:
APK (Alpine, CleanStart): If base image contains "alpine", "cleanstart", or ends with "-prod" APT (Debian, Ubuntu): If base image contains "debian" or "ubuntu" Explicit: Override via package_manager: apt or package_manager: apk in spec
Variant: prod vs. dev
prod: Distroless, shell-less, read-only root FS. Use for final deployment images. dev: Has shell, APK/APT, debugging tools. Use for building, testing, or developer workflows.
The builder image is auto-derived by replacing -prod with -dev in the base_image tag:
base_image: us-central1-docker.pkg.dev/my-org/images/myapp-prod:1.0→ builder_image: us-central1-docker.pkg.dev/my-org/images/myapp-dev:1.0Multi-Stage Builder Pattern
When multistage: true (or auto-detected), cleanimg-customize generates:
# Stage 1: builder (dev image with tools)FROM myapp-dev:1.0 AS builderRUN apk add --no-cache curl openssl# Artifacts injected here # Stage 2: prod (distroless final image)FROM myapp-prod:1.0COPY --from=builder /usr/local/lib/ /usr/local/lib/COPY --from=builder /opt/lib/ /opt/lib/COPY config/app.yaml /etc/myapp/config.yamlAutomatic Activation when any of these are true: multistage: true in spec builder_image is set copy_from_builder is non-empty Base image is CleanStart prod + packages + APK manager
CleanStart Repository URLs
When working with CleanStart images, three package repositories are available:
apk_repositories: - clnpkgs.clnstrt.dev/main # Core packages - clnpkgs.clnstrt.dev/community # Community packages - clnpkgs.clnstrt.dev/triam # Experimental/tribal packagesThese are automatically configured when detected. Or explicitly set cleanstart_repos: true in your spec.
Writable Paths (prod images)
CleanStart prod images are read-only by default. cleanimg-customize automatically adds these writable paths:
/tmp/var/tmp/var/cache/var/log/var/run/var/lockThese become tmpfs mounts at runtime, allowing temporary file creation without modifying the image.
4. Workflow Overview
The Three-Step Build Process
Step 1: Generate (optional, helpful for newcomers)
cleanimg-customize generate \ --base-image us-central1-docker.pkg.dev/my-org/images/cleanstart-prod:0.2.5 \ --arch arm64 \ --package curl \ --package openssl \ --output spec.yamlStep 2: Review & Edit
cat spec.yaml# Edit as neededvim spec.yamlStep 3: Build & Push
cleanimg-customize build \ --spec spec.yaml \ --tag my-registry.azurecr.io/myapp:v1.0.0 \ --pushAlternative: Write Spec by Hand
For experienced users, write spec.yaml directly:
cat > spec.yaml << 'EOF'base_image: us-central1-docker.pkg.dev/my-org/images/cleanstart-prod:0.2.5packages: - name: curl - name: opensslcopy_files: - source: ./app.yaml destination: /etc/myapp/app.yamluser: appuserentrypoint: ["/opt/bin/myapp"]EOF cleanimg-customize build --spec spec.yaml --tag my-registry/myapp:v1.0.0 --push5. Subcommand Reference
cleanimg-customize generate
Generates an IncrementalSpec YAML from CLI arguments. Useful for getting started or scripting.
Syntax:
cleanimg-customize generate [OPTIONS] --output <FILE>Common Flags:
Flag | Value | Default | Description |
|---|---|---|---|
| string | (required) | Base image URI (e.g., |
| amd64|arm64 | amd64 | Target architecture |
| prod|dev | prod | Image variant |
| string | (none) | Package to install (repeatable: |
| string | (latest) | Version for previous |
| apt|apk | (auto-detect) | Override package manager |
| src:dst | (none) | File to copy (repeatable) |
| KEY=VALUE | (none) | Environment variable (repeatable) |
| KEY=VALUE | (none) | OCI label (repeatable) |
| string | (none) | Default user (username or username:group) |
| path | / | Working directory |
| string | (none) | Entrypoint (repeatable for array) |
| string | (none) | Default command (repeatable for array) |
| file | (required) | Output spec file path |
Examples:
# Basic Alpine with curlcleanimg-customize generate \ --base-image alpine:3.19 \ --package curl \ --output spec.yaml # CleanStart prod image with multiple packagescleanimg-customize generate \ --base-image us-central1-docker.pkg.dev/my-org/images/cleanstart-prod:0.2.5 \ --arch arm64 \ --package curl --package-version 8.5.0-r0 \ --package openssl \ --copy-file ./config/app.yaml:/etc/myapp/app.yaml \ --env APP_ENV=production \ --user appuser \ --output spec.yaml # Python image with custom entrypointcleanimg-customize generate \ --base-image python:3.11-alpine \ --package gcc \ --workdir /app \ --entrypoint python \ --cmd app.py \ --output spec.yamlcleanimg-customize build
Builds and optionally pushes an image from an IncrementalSpec.
Syntax:
cleanimg-customize build [OPTIONS] --spec <FILE> --tag <TAG>Common Flags:
Flag | Value | Default | Description |
|---|---|---|---|
| file | (required) | IncrementalSpec YAML file |
| string | (required) | Output image tag (e.g., |
| bool | false | Push to registry after build |
| bool | true | Always pull base/builder images |
| bool | false | Disable Docker build cache |
| string | docker | Build backend (docker, buildkit, kaniko) |
| string | (local) | Build platform (linux/amd64, linux/arm64, etc.) |
Examples:
# Build locally (Docker)cleanimg-customize build \ --spec spec.yaml \ --tag myregistry/myapp:v1.0.0 # Build and push to Azure Container Registrycleanimg-customize build \ --spec spec.yaml \ --tag myregistry.azurecr.io/myapp:v1.0.0 \ --push # Build for ARM64 specificallycleanimg-customize build \ --spec spec.yaml \ --tag myregistry/myapp:v1.0.0-arm64 \ --platform linux/arm64 \ --pushcleanimg-customize to-dockerfile
Generates a Dockerfile from an IncrementalSpec without building.
Syntax:
cleanimg-customize to-dockerfile [OPTIONS] --spec <FILE>Common Flags:
Flag | Value | Default | Description |
|---|---|---|---|
| file | (required) | IncrementalSpec YAML file |
| file | stdout | Write Dockerfile to file instead of stdout |
| string | native | Output format (native, oci) |
Examples:
# Output Dockerfile to stdoutcleanimg-customize to-dockerfile --spec spec.yaml # Save Dockerfile for reviewcleanimg-customize to-dockerfile --spec spec.yaml --output Dockerfile.generated # Integrate into CI/CD pipelinecleanimg-customize to-dockerfile --spec spec.yaml | docker build -f - -t myapp:v1.0 .cleanimg-customize validate
Validates an IncrementalSpec against the schema without building.
Syntax:
cleanimg-customize validate [OPTIONS] --spec <FILE>Examples:
# Validate spec filecleanimg-customize validate --spec spec.yaml # Validate with strict mode (rejects unknown fields)cleanimg-customize validate --spec spec.yaml --strictcleanimg-customize extract-sbom
Extracts or generates a Software Bill of Materials (SBOM) from a built image.
Syntax:
cleanimg-customize extract-sbom [OPTIONS] --image <IMAGE>Common Flags:
Flag | Value | Default | Description |
|---|---|---|---|
| string | (required) | Image URI (with tag/digest) |
| spdx|cyclonedx | spdx | SBOM format |
| file | stdout | Write SBOM to file |
Examples:
# Extract SBOM to stdoutcleanimg-customize extract-sbom --image myregistry/myapp:v1.0.0 # Generate SPDX-format SBOMcleanimg-customize extract-sbom \ --image myregistry/myapp:v1.0.0 \ --format spdx \ --output sbom.spdx.json # Generate CycloneDX SBOMcleanimg-customize extract-sbom \ --image myregistry/myapp:v1.0.0 \ --format cyclonedx \ --output sbom.cyclonedx.jsoncleanimg-customize inspect
Inspects the structure and contents of an IncrementalSpec.
Syntax:
cleanimg-customize inspect [OPTIONS] --spec <FILE>Examples:
# Show spec summarycleanimg-customize inspect --spec spec.yaml # Show detailed breakdowncleanimg-customize inspect --spec spec.yaml --verbose6. Per-Application Examples
Example 1: Kafka with Custom Authentication JAR
Requirement: Run Kafka broker with custom SASL/SSL authentication library.
spec.yaml:
base_image: confluentinc/cp-kafka:7.5.0-prodarch: arm64variant: prodpackage_manager: apt packages: - name: openssl - name: bash multistage: truebuilder_image: confluentinc/cp-kafka:7.5.0-dev artifacts: - type: jar maven_coords: "com.example:kafka-auth:1.2.0" destination: /opt/kafka/libs/kafka-auth-1.2.0.jar permissions: "0644" copy_files: - source: ./kafka-server.properties destination: /etc/kafka/kafka-server.properties permissions: "0644" - source: ./certs/broker-key.pem destination: /etc/kafka/secrets/broker-key.pem permissions: "0600" - source: ./certs/broker-cert.pem destination: /etc/kafka/secrets/broker-cert.pem permissions: "0644" groups: - groupname: kafka gid: 1001 users: - username: kafka uid: 1001 gid: 1001 home: /var/kafka shell: /bin/bash env_vars: KAFKA_HEAP_OPTS: "-Xms512M -Xmx1G" KAFKA_OPTS: "-Dcom.sun.security.auth.module=com.example.KafkaLoginModule" labels: app: kafka-broker version: "7.5.0-custom" resource_hints: requests: memory: "1Gi" cpu: "1000m" limits: memory: "2Gi" cpu: "2000m" user: kafka:kafkawritable_paths: - /var/kafka - /tmp - /var/logBuild:
cleanimg-customize build \ --spec spec.yaml \ --tag myregistry.azurecr.io/kafka-broker:7.5.0-custom \ --pushExample 2: Python App with Custom Wheels
Requirement: Python 3.11 runtime with pre-compiled wheels for NumPy and pandas.
spec.yaml:
base_image: python:3.11-alpine-prodarch: arm64variant: prod packages: - name: gcc - name: musl-dev - name: curl multistage: true artifacts: - type: wheel source: ./wheels/numpy-1.24.3-cp311-cp311-linux_aarch64.whl target_dir: /tmp/wheels - type: wheel source: ./wheels/pandas-2.0.3-cp311-cp311-linux_aarch64.whl target_dir: /tmp/wheels copy_files: - source: ./requirements.txt destination: /app/requirements.txt permissions: "0644" - source: ./app.py destination: /app/app.py permissions: "0755" env_vars: PYTHONUNBUFFERED: "1" PIP_NO_CACHE_DIR: "1" run_commands: - "pip install --no-index --find-links /tmp/wheels -r /app/requirements.txt" users: - username: pyapp uid: 2000 gid: 2000 home: /app shell: /sbin/nologin workdir: /appuser: pyappentrypoint: ["python"]cmd: ["app.py"]Build:
cleanimg-customize build \ --spec spec.yaml \ --tag myregistry/python-app:1.0.0 \ --pushExample 3: Go Binary with Config Injection
Requirement: Minimal Go binary with static TLS certificates and config.
spec.yaml:
base_image: gcr.io/distroless/base-debian12:nonroot-prodarch: amd64variant: prod multistage: truebuilder_image: golang:1.21-alpine artifacts: - type: file source: ./bin/myapp destination: /usr/local/bin/myapp permissions: "0755" - type: file source: ./certs/ca-bundle.crt destination: /etc/ssl/certs/ca-bundle.crt permissions: "0644" copy_files: - source: ./config/config.toml destination: /etc/myapp/config.toml permissions: "0644" env_vars: CONFIG_PATH: /etc/myapp/config.toml LOG_LEVEL: info users: - username: appuser uid: 65532 gid: 65532 user: appuserentrypoint: ["/usr/local/bin/myapp"]cmd: ["--config=/etc/myapp/config.toml"]Build:
cleanimg-customize build \ --spec spec.yaml \ --tag myregistry/go-app:latest \ --pushExample 4: Redis with TLS Support
Requirement: Redis with TLS certificates and custom configuration.
spec.yaml:
base_image: redis:7.2-alpine-prodarch: arm64variant: prod packages: - name: openssl copy_files: - source: ./redis.conf destination: /etc/redis/redis.conf permissions: "0644" - source: ./certs/redis.crt destination: /etc/redis/certs/redis.crt permissions: "0644" - source: ./certs/redis.key destination: /etc/redis/certs/redis.key permissions: "0600" - source: ./certs/ca.crt destination: /etc/redis/certs/ca.crt permissions: "0644" groups: - groupname: redis gid: 999 users: - username: redis uid: 999 gid: 999 home: /var/lib/redis shell: /sbin/nologin env_vars: REDIS_REPLICATION_SYNC_TIMEOUT: "60000" REDIS_TCP_KEEPALIVE: "300" labels: service: redis tier: data user: redis:redisentrypoint: ["redis-server"]cmd: ["/etc/redis/redis.conf"] writable_paths: - /var/lib/redis - /tmpBuild:
cleanimg-customize build \ --spec spec.yaml \ --tag myregistry/redis-tls:7.2 \ --pushExample 5: Node.js App with npm Modules
Requirement: Node.js runtime with pre-installed npm modules.
spec.yaml:
base_image: node:20-alpine-prodarch: arm64variant: prod packages: - name: curl - name: openssl multistage: true artifacts: - type: node_module source: ./node_modules/@myorg/logging target_dir: /app/node_modules/@myorg/ copy_files: - source: ./package.json destination: /app/package.json permissions: "0644" - source: ./package-lock.json destination: /app/package-lock.json permissions: "0644" - source: ./src/index.js destination: /app/src/index.js permissions: "0644" - source: ./.env.production destination: /app/.env.production permissions: "0644" env_vars: NODE_ENV: "production" NODE_OPTIONS: "--max-old-space-size=512" users: - username: nodeapp uid: 1000 gid: 1000 home: /app shell: /sbin/nologin workdir: /appuser: nodeappentrypoint: ["node"]cmd: ["src/index.js"]Build:
cleanimg-customize build \ --spec spec.yaml \ --tag myregistry/node-app:v2.0.0 \ --push7. Multi-Architecture Builds
Using arch_overrides
Support multiple architectures from a single spec using arch_overrides:
spec.yaml:
base_image: us-central1-docker.pkg.dev/my-org/images/myapp-prod:1.0arch: amd64 # Default packages: - name: curl - name: openssl arch_overrides: arm64: packages: - name: openssl-libcrypto3-compat # ARM64 variant env_vars: ARCH: "arm64" SIMD_ENABLED: "true" amd64: env_vars: ARCH: "amd64" SIMD_ENABLED: "true" env_vars: APP_ENV: "production"Building for Multiple Platforms
Approach 1: Sequential builds
# Build for amd64cleanimg-customize build \ --spec spec.yaml \ --platform linux/amd64 \ --tag myregistry/myapp:v1.0.0-amd64 \ --push # Build for arm64sed -i 's/arch: amd64/arch: arm64/' spec.yamlcleanimg-customize build \ --spec spec.yaml \ --platform linux/arm64 \ --tag myregistry/myapp:v1.0.0-arm64 \ --pushApproach 2: Using Docker Buildx (multi-arch in one step)
# Create buildx builderdocker buildx create --name mybuilder --use # Generate Dockerfilecleanimg-customize to-dockerfile --spec spec.yaml > Dockerfile.generated # Build and push multi-arch imagedocker buildx build \ --platform linux/amd64,linux/arm64 \ --tag myregistry/myapp:v1.0.0 \ --push \ -f Dockerfile.generated . # Create and push manifest (optional, for manual control)docker manifest create myregistry/myapp:v1.0.0 \ myregistry/myapp:v1.0.0-amd64 \ myregistry/myapp:v1.0.0-arm64docker manifest push myregistry/myapp:v1.0.08. CI/CD Integration
Cloud Build (Google Cloud)
cloudbuild.yaml:
steps: # Step 1: Validate spec - name: "us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0" args: ["validate", "--spec", "spec.yaml"] # Step 2: Generate Dockerfile - name: "us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0" args: ["to-dockerfile", "--spec", "spec.yaml", "--output", "Dockerfile.generated"] # Step 3: Build image - name: "gcr.io/cloud-builders/docker" args: ["build", "-f", "Dockerfile.generated", "-t", "gcr.io/$PROJECT_ID/$_SERVICE_NAME:$SHORT_SHA", "."] # Step 4: Push to GCR - name: "gcr.io/cloud-builders/docker" args: ["push", "gcr.io/$PROJECT_ID/$_SERVICE_NAME:$SHORT_SHA"] # Step 5: Extract SBOM - name: "us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0" args: ["extract-sbom", "--image", "gcr.io/$PROJECT_ID/$_SERVICE_NAME:$SHORT_SHA", "--format", "spdx", "--output", "sbom.spdx.json"] images: - "gcr.io/$PROJECT_ID/$_SERVICE_NAME:$SHORT_SHA" - "gcr.io/$PROJECT_ID/$_SERVICE_NAME:latest" substitutions: _SERVICE_NAME: "myapp"GitHub Actions
.github/workflows/build.yaml:
name: Build and Push Image on: push: branches: [main] paths: - "spec.yaml" - ".github/workflows/build.yaml" env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Validate spec run: | docker run --rm \ -v "${{ github.workspace }}":/workspace \ us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 \ validate --spec /workspace/spec.yaml - name: Generate Dockerfile run: | docker run --rm \ -v "${{ github.workspace }}":/workspace \ us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 \ to-dockerfile --spec /workspace/spec.yaml --output /workspace/Dockerfile.generated - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile.generated push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Extract SBOM run: | docker run --rm \ -v "${{ github.workspace }}":/workspace \ us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 \ extract-sbom \ --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \ --format spdx \ --output /workspace/sbom.spdx.json - name: Upload SBOM uses: actions/upload-artifact@v3 with: name: sbom path: sbom.spdx.jsonGitLab CI
.gitlab-ci.yml:
image: us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 stages: - validate - build - push - sbom variables: REGISTRY: registry.gitlab.com IMAGE_NAME: $CI_REGISTRY_IMAGE DOCKER_HOST: tcp://docker:2375 services: - docker:dind validate: stage: validate script: - cleanimg-customize validate --spec spec.yaml generate_dockerfile: stage: build script: - cleanimg-customize to-dockerfile --spec spec.yaml --output Dockerfile.generated artifacts: paths: - Dockerfile.generated build_and_push: stage: push image: docker:latest services: - docker:dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -f Dockerfile.generated -t $IMAGE_NAME:$CI_COMMIT_SHA . - docker push $IMAGE_NAME:$CI_COMMIT_SHA - docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest - docker push $IMAGE_NAME:latest extract_sbom: stage: sbom script: - cleanimg-customize extract-sbom --image $IMAGE_NAME:$CI_COMMIT_SHA --format spdx --output sbom.spdx.json artifacts: paths: - sbom.spdx.json9. Limitations and Workarounds
Limitation 1: No Removal of Base Image Files
Problem: cleanimg-customize can only add/override files, not remove them.
Workaround: If you need to exclude files from the base image:
- Create a custom base image (via Dockerfile or cleanimg-init)
- Use that as your base_image in the spec
Limitation 2: No Dynamic Environment Variable Substitution
Problem: Environment variables are baked into the image at build time.
Workaround: For runtime configuration, mount config files or use cleanimg-init for container startup:
docker run -e APP_CONFIG=/custom/config.yaml -v /my/config.yaml:/custom/config.yaml myimage:v1.0Limitation 3: File Size Limitations
Problem: Very large artifact files (>1GB) may timeout or cause resource issues.
Workaround: Split large artifacts across multiple copy_files entries with compression Consider mounting volumes instead of baking into the image Use multi-stage builds to keep the final image smaller
Limitation 4: No Privilege Escalation
Problem: All run_commands execute with the builder image's default user (usually root).
Workaround: Ensure compilation/installation happens in the builder stage; the prod image retains the built artifacts with correct ownership.
Limitation 5: Limited Package Version Pinning with APK
Problem: APK versioning uses complex syntax (e.g., curl-8.5.0-r0). Version matching may fail silently.
Workaround: Test your spec in a development environment first:
cleanimg-customize build --spec spec.yaml --tag test:latestdocker run test:latest apk list | grep curl10. Troubleshooting
Error: "Cannot find builder image"
Cause: Auto-derived builder image doesn't exist (e.g., -dev tag not found).
Solution:
# Instead of relying on auto-detection:builder_image: my-registry.azurecr.io/myapp-dev:1.0Or manually push the dev image:
docker tag my-registry.azurecr.io/myapp-prod:1.0 my-registry.azurecr.io/myapp-dev:1.0docker push my-registry.azurecr.io/myapp-dev:1.0Error: "Unknown package X"
Cause: Package name may not be in the default APK/APT repository.
Solution:
- Verify package name:
apk search curl(in an Alpine container) - For CleanStart, explicitly add CleanStart repos: cleanstart_repos: true apk_repositories: - clnpkgs.clnstrt.dev/main - clnpkgs.clnstrt.dev/community
- Try specifying a version that exists in your repos
Error: "Docker socket not found"
Cause: When running cleanimg-customize in a container without Docker socket mounted.
Solution:
# Mount Docker socketdocker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$PWD":/workspace \ us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 \ build --spec /workspace/spec.yaml --tag myimage:v1.0Error: "Spec validation failed: unknown field 'xyz'"
Cause: Using a newer spec format with an older version of cleanimg-customize.
Solution: Update to latest version:
docker pull us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0Error: "Image push failed: access denied"
Cause: Not authenticated to the registry.
Solution:
# For Artifact Registrygcloud auth configure-docker us-central1-docker.pkg.dev # For Docker Hubdocker login # For Azure Container Registryaz acr login --name myregistryBuild Fails with "insufficient disk space"
Cause: Docker doesn't have enough space to build the image.
Solution:
# Clean up old images/layersdocker system prune -a # Check available spacedocker system df # Increase Docker's disk allocation (Docker Desktop settings, or systemd-docker service)Artifact Downloaded Multiple Times
Cause: cleanimg-customize downloads artifacts for each build without caching.
Workaround: Pre-download artifacts and reference locally:
artifacts: - type: jar source: ./pre-downloaded/auth-lib.jar # Local file instead of Maven coords destination: /opt/lib/auth-lib.jar11. What to Read Next
cleanimg-init Guide: Container startup and initialization, environment injection CleanStart Image Variants: Understanding -prod vs. -dev images Multi-Stage Build Best Practices: Advanced Dockerfile patterns Artifact Management: Maven, pip, npm artifact strategies Security and Hardening: Non-root users, read-only FS, minimal attack surface SBOM and Software Provenance: Understanding and verifying SBOMs Registry Integration: Pushing to various container registries
Questions? File an issue or contact the platform team at platform-team@example.com.
