Skip to main content
CleanStart

Knowledge Hub

Declarative Image Builder: # cleanimg-customize v0.3.0: Complete Guide

Version: 0.3.0 Last Updated: 2026-03-22 Audience: DevOps engineers, platform teams, container builders Difficulty: Intermediate

Table of Contents

  1. What cleanimg-customize Is
  2. Installation
  3. Core Concepts
  4. Workflow Overview
  5. Subcommand Reference
  6. Per-Application Examples
  7. Multi-Architecture Builds
  8. CI/CD Integration
  9. Limitations and Workarounds
  10. Troubleshooting
  11. 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:e88f932a01f2

Quick 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.0

Verify Installation

docker run --rm us-central1-docker.pkg.dev/clean-image-build/cleanimage001/cleanimg-customize:v0.3.0 --version# Output: cleanimg-customize v0.3.0

Authentication (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 login

3. 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: true

All 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.0

Multi-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.yaml

Automatic 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 packages

These 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/lock

These 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.yaml

Step 2: Review & Edit

cat spec.yaml# Edit as neededvim spec.yaml

Step 3: Build & Push

cleanimg-customize build \  --spec spec.yaml \  --tag my-registry.azurecr.io/myapp:v1.0.0 \  --push

Alternative: 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 --push

5. 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

--base-image

string

(required)

Base image URI (e.g., alpine:3.19)

--arch

amd64|arm64

amd64

Target architecture

--variant

prod|dev

prod

Image variant

--package

string

(none)

Package to install (repeatable: --package curl --package openssl)

--package-version

string

(latest)

Version for previous --package

--package-manager

apt|apk

(auto-detect)

Override package manager

--copy-file

src:dst

(none)

File to copy (repeatable)

--env

KEY=VALUE

(none)

Environment variable (repeatable)

--label

KEY=VALUE

(none)

OCI label (repeatable)

--user

string

(none)

Default user (username or username:group)

--workdir

path

/

Working directory

--entrypoint

string

(none)

Entrypoint (repeatable for array)

--cmd

string

(none)

Default command (repeatable for array)

--output

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.yaml

cleanimg-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

--spec

file

(required)

IncrementalSpec YAML file

--tag

string

(required)

Output image tag (e.g., my-registry/app:v1.0)

--push

bool

false

Push to registry after build

--pull

bool

true

Always pull base/builder images

--no-cache

bool

false

Disable Docker build cache

--builder

string

docker

Build backend (docker, buildkit, kaniko)

--platform

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 \  --push

cleanimg-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

--spec

file

(required)

IncrementalSpec YAML file

--output

file

stdout

Write Dockerfile to file instead of stdout

--format

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 --strict

cleanimg-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

--image

string

(required)

Image URI (with tag/digest)

--format

spdx|cyclonedx

spdx

SBOM format

--output

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.json

cleanimg-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 --verbose

6. 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/log

Build:

cleanimg-customize build \  --spec spec.yaml \  --tag myregistry.azurecr.io/kafka-broker:7.5.0-custom \  --push

Example 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 \  --push

Example 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 \  --push

Example 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  - /tmp

Build:

cleanimg-customize build \  --spec spec.yaml \  --tag myregistry/redis-tls:7.2 \  --push

Example 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 \  --push

7. 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 \  --push

Approach 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.0

8. 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.json

GitLab 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.json

9. 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:

  1. Create a custom base image (via Dockerfile or cleanimg-init)
  2. 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.0

Limitation 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 curl

10. 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.0

Or 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.0

Error: "Unknown package X"

Cause: Package name may not be in the default APK/APT repository.

Solution:

  1. Verify package name: apk search curl (in an Alpine container)
  2. For CleanStart, explicitly add CleanStart repos: cleanstart_repos: true apk_repositories: - clnpkgs.clnstrt.dev/main - clnpkgs.clnstrt.dev/community
  3. 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.0

Error: "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.0

Error: "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 myregistry

Build 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.jar

11. 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.