This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 27f0643105 create build script and unified docker file, fixes #6100 
(#6103)
27f0643105 is described below

commit 27f064310528c581551f35cf74368363e7c748eb
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Dec 2 16:05:22 2025 +0100

    create build script and unified docker file, fixes #6100 (#6103)
---
 .dockerignore                                      |  27 +-
 Jenkinsfile                                        |   6 +-
 docker/build-hop-images.sh                         | 581 +++++++++++++++++++++
 docker/create_hop_rest_container.sh                |   2 +-
 docker/create_hop_web_container.sh                 |   2 +-
 ...ataflowTemplate => dataflowTemplate.Dockerfile} |   0
 .../integration-tests/integration-tests-base.yaml  |   2 +-
 ...Dockerfile.unit-tests => unit-tests.Dockerfile} |   0
 docker/{Dockerfile.rest => rest.Dockerfile}        |   0
 docker/unified.Dockerfile                          | 389 ++++++++++++++
 ...Dockerfile.web-fatjar => web-fatjar.Dockerfile} |   0
 docker/{Dockerfile.web => web.Dockerfile}          |   0
 docs/hop-dev-manual/modules/ROOT/nav.adoc          |   1 +
 .../modules/ROOT/pages/docker-build.adoc           | 498 ++++++++++++++++++
 docs/hop-dev-manual/modules/ROOT/pages/index.adoc  |   1 +
 .../samples/read-samples-build-hop-run.hpl         |   2 +-
 16 files changed, 1502 insertions(+), 9 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index b404469ffe..da90c3875b 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,7 +1,7 @@
 # Ignore everything
 *
 
-# Allow files and directories
+# Allow files and directories needed for OLD Dockerfiles (pre-built artifacts)
 !/assemblies/client/target/hop-*
 !/assemblies/client/target/hop
 !/integration-tests/scripts
@@ -9,4 +9,27 @@
 !/assemblies/plugins/target
 !/docker
 !/rest
-!google-key-apache-hop-it.json
\ No newline at end of file
+!google-key-apache-hop-it.json
+
+# Allow source files needed for NEW unified Dockerfile (maven build in Docker)
+!pom.xml
+!/*/pom.xml
+!/*/*/pom.xml
+!/*/*/*/pom.xml
+!/src
+!/*/src
+!/*/*/src
+!/*/*/*/src
+!/core
+!/engine
+!/engine-beam
+!/plugins
+!/assemblies
+!/ui
+!/rap
+!/rcp
+!/lib
+!/lib-jdbc
+!/.mvn
+!mvnw
+!mvnw.cmd
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
index afef930a83..8a2fe67d4d 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -159,9 +159,9 @@ pipeline {
                     //TODO We may never create final/latest version using 
CI/CD as we need to follow manual apache release process with signing
                     sh "docker buildx create --name hop --use"
                     //Base docker image
-                    sh "docker buildx build --platform linux/amd64,linux/arm64 
. -f docker/Dockerfile.web -t ${DOCKER_REPO_WEB}:${env.POM_VERSION} -t 
${DOCKER_REPO_WEB}:Development --push"
+                    sh "docker buildx build --platform linux/amd64,linux/arm64 
. -f docker/web.Dockerfile -t ${DOCKER_REPO_WEB}:${env.POM_VERSION} -t 
${DOCKER_REPO_WEB}:Development --push"
                     //Image including fat-jar
-                    sh "docker buildx build  --build-arg 
HOP_WEB_VERSION=Development --platform linux/amd64,linux/arm64 . -f 
docker/Dockerfile.web-fatjar -t ${DOCKER_REPO_WEB}:${env.POM_VERSION}-beam -t 
${DOCKER_REPO_WEB}:Development-beam --push"
+                    sh "docker buildx build  --build-arg 
HOP_WEB_VERSION=Development --platform linux/amd64,linux/arm64 . -f 
docker/web-fatjar.Dockerfile -t ${DOCKER_REPO_WEB}:${env.POM_VERSION}-beam -t 
${DOCKER_REPO_WEB}:Development-beam --push"
                     sh "docker buildx rm hop"
                   }
             }
@@ -177,7 +177,7 @@ pipeline {
                 withDockerRegistry([ credentialsId: "dockerhub-hop", url: "" 
]) {
                     //TODO We may never create final/latest version using 
CI/CD as we need to follow manual apache release process with signing
                     sh "docker buildx create --name hop --use"
-                    sh "docker buildx build --platform linux/amd64,linux/arm64 
. -f docker/Dockerfile.dataflowTemplate -t 
${DOCKER_REPO_DATAFLOWTEMPLATE}:${env.POM_VERSION} -t 
${DOCKER_REPO_DATAFLOWTEMPLATE}:Development --push"
+                    sh "docker buildx build --platform linux/amd64,linux/arm64 
. -f docker/dataflowTemplate.Dockerfile -t 
${DOCKER_REPO_DATAFLOWTEMPLATE}:${env.POM_VERSION} -t 
${DOCKER_REPO_DATAFLOWTEMPLATE}:Development --push"
                     sh "docker buildx rm hop"
                   }
             }
diff --git a/docker/build-hop-images.sh b/docker/build-hop-images.sh
new file mode 100755
index 0000000000..a46403107a
--- /dev/null
+++ b/docker/build-hop-images.sh
@@ -0,0 +1,581 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+################################################################################
+# Apache Hop Unified Docker Build Script
+#
+# This script builds all Apache Hop Docker images using a multi-stage build
+# approach. It supports building from local source or from a GitHub tag.
+#
+# Usage:
+#   ./build-hop-images.sh [OPTIONS]
+#
+# Options:
+#   -s, --source <local|github>   Build from local source or GitHub (default: 
local)
+#   -t, --tag <tag>               Git tag/branch to build from (default: main)
+#   -r, --repo <url>              GitHub repository URL (default: 
https://github.com/apache/hop.git)
+#   -i, --images <list>           Comma-separated list of images to build 
(default: all)
+#                                 Options: see ALL_STAGES variable, or 'all'
+#   -v, --version <version>       Version string for image tagging 
(auto-detected if not provided)
+#   -p, --push                    Push images to registry after build
+#   -x, --registry <registry>     Docker registry prefix (default: none/local, 
e.g., apache, myregistry.io)
+#   --platforms <platforms>       Build platforms (default: current platform, 
e.g., linux/amd64,linux/arm64 for multi)
+#   --maven-threads <threads>     Maven build threads (default: 1C, e.g., 2C, 
4, 8)
+#   --progress <mode>             Docker build output mode: auto (default), 
plain (verbose), tty (compact)
+#   --builder <type>              Builder type: full (Maven, default) or fast 
(pre-built artifacts)
+#   --no-cache                    Build without using cache
+#   -h, --help                    Show this help message
+#
+# Examples:
+#   # Build all images from local source
+#   ./build-hop-images.sh
+#
+#   # Build from GitHub tag 2.9.0
+#   ./build-hop-images.sh --source github --tag 2.9.0
+#
+#   # Fast build using pre-built artifacts (for local dev)
+#   ./build-hop-images.sh --builder fast --images web
+#
+#   # Build web image with beam variant (includes fat jar for Dataflow)
+#   ./build-hop-images.sh --images web-beam
+#
+#   # Build specific variants
+#   ./build-hop-images.sh --images client-minimal,web-beam
+#
+#   # Build for multiple platforms and push to Docker Hub
+#   ./build-hop-images.sh --platforms linux/amd64,linux/arm64 --push 
--registry apache
+#
+################################################################################
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Available image stages (add new stages here)
+# Format: "baseImage" or "baseImage-variant"
+ALL_STAGES=("client" "web" "web-beam" "dataflow")
+
+# Detect current platform architecture
+detect_platform() {
+    local arch=$(uname -m)
+    case "$arch" in
+        x86_64|amd64)
+            echo "linux/amd64"
+            ;;
+        aarch64|arm64)
+            echo "linux/arm64"
+            ;;
+        *)
+            echo "linux/amd64"  # fallback to amd64
+            ;;
+    esac
+}
+
+# Default values
+SOURCE_TYPE="local"
+GIT_TAG="main"
+GIT_REPO="https://github.com/apache/hop.git";
+IMAGES="all"
+VERSION=""
+PUSH=false
+REGISTRY=""  # Empty = local registry (no prefix)
+PLATFORMS="$(detect_platform)"  # Default to current platform only
+USE_CACHE="true"
+MAVEN_THREADS="1C"  # Maven thread count (1C, 2C, or specific number like 4)
+BUILD_PROGRESS="auto"  # Docker build progress output (auto, plain, tty)
+BUILDER_TYPE="full"  # Builder type: full (Maven build) or fast (pre-built 
artifacts)
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+
+# Stage names follow pattern: <base>-<variant> or just <base>
+# Examples:
+#   client         → hop
+#   client-minimal → hop
+#   web            → hop-web
+#   web-beam       → hop-web
+get_image_name() {
+    local stage_name="$1"
+    
+    # Check if stage has a variant suffix
+    case "$stage_name" in
+        client*)   echo "hop" ;;
+        web*)      echo "hop-web" ;;
+
+        dataflow*) echo "hop-dataflow-template" ;;
+        *)         echo "" ;;
+    esac
+}
+
+# Extract variant suffix from stage name
+# Examples:
+#   client         → ""
+#   client-minimal → "minimal"
+#   web-beam       → "beam"
+get_variant_suffix() {
+    local stage_name="$1"
+    
+    # Check if there's a dash in the name
+    if [[ "$stage_name" == *"-"* ]]; then
+        # Extract everything after the first dash
+        echo "${stage_name#*-}"
+    else
+        echo ""
+    fi
+}
+
+# Function to print colored messages
+print_info() {
+    echo -e "${BLUE}ℹ${NC} $1"
+}
+
+print_success() {
+    echo -e "${GREEN}✓${NC} $1"
+}
+
+print_warning() {
+    echo -e "${YELLOW}⚠${NC} $1"
+}
+
+print_error() {
+    echo -e "${RED}✗${NC} $1"
+}
+
+print_header() {
+    echo -e 
"\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
+    echo -e "${BLUE}  $1${NC}"
+    echo -e 
"${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
+}
+
+# Function to show help
+show_help() {
+    sed -n '/^# 
Usage:/,/^################################################################################$/p'
 "$0" | grep -v "^#####" | sed 's/^# //'
+    exit 0
+}
+
+# Function to detect version from pom.xml
+detect_version() {
+    if [ -f "$PROJECT_ROOT/pom.xml" ]; then
+        # Get the project version (not parent version)
+        # Remove parent block, then get first version tag
+        VERSION=$(sed -n '/<parent>/,/<\/parent>/d; 
s/.*<version>\(.*\)<\/version>.*/\1/p' "$PROJECT_ROOT/pom.xml" | head -1 | 
xargs)
+        
+        if [ -z "$VERSION" ]; then
+            # Fallback to maven command if available
+            if command -v mvn &> /dev/null; then
+                VERSION=$(cd "$PROJECT_ROOT" && mvn help:evaluate 
-Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "")
+            fi
+        fi
+        
+        if [ -z "$VERSION" ]; then
+            VERSION="latest"
+            print_warning "Could not detect version, using: $VERSION"
+        else
+            print_info "Detected version from pom.xml: $VERSION"
+        fi
+    else
+        VERSION="latest"
+        print_warning "Could not detect version, using: $VERSION"
+    fi
+}
+
+# Function to validate prerequisites
+check_prerequisites() {
+    print_header "Checking Prerequisites"
+    
+    # Check Docker
+    if ! command -v docker &> /dev/null; then
+        print_error "Docker is not installed or not in PATH"
+        exit 1
+    fi
+    print_success "Docker: $(docker --version)"
+    
+    # Check if Docker daemon is running
+    if ! docker info &> /dev/null; then
+        print_error "Docker daemon is not running"
+        exit 1
+    fi
+    print_success "Docker daemon is running"
+    
+    # Check buildx for multi-platform builds
+    if echo "$PLATFORMS" | grep -q ","; then
+        if ! docker buildx version &> /dev/null; then
+            print_error "Docker buildx is required for multi-platform builds"
+            exit 1
+        fi
+        print_success "Docker buildx is available"
+    fi
+}
+
+# Function to load environment file if exists
+load_env_file() {
+    local env_file="$SCRIPT_DIR/build.env"
+    if [[ -f "$env_file" ]]; then
+        print_info "Loading configuration from build.env"
+        # shellcheck disable=SC1090
+        source "$env_file"
+    fi
+}
+
+# Function to parse command line arguments
+parse_args() {
+    while [[ $# -gt 0 ]]; do
+        case $1 in
+            -s|--source)
+                SOURCE_TYPE="$2"
+                shift 2
+                ;;
+            -t|--tag)
+                GIT_TAG="$2"
+                shift 2
+                ;;
+            -r|--repo)
+                GIT_REPO="$2"
+                shift 2
+                ;;
+            -i|--images)
+                IMAGES="$2"
+                shift 2
+                ;;
+            -v|--version)
+                VERSION="$2"
+                shift 2
+                ;;
+            -p|--push)
+                PUSH=true
+                shift
+                ;;
+            -x|--registry)
+                REGISTRY="$2"
+                shift 2
+                ;;
+            --platforms)
+                PLATFORMS="$2"
+                shift 2
+                ;;
+            --maven-threads)
+                MAVEN_THREADS="$2"
+                shift 2
+                ;;
+            --progress)
+                BUILD_PROGRESS="$2"
+                shift 2
+                ;;
+            --builder)
+                BUILDER_TYPE="$2"
+                shift 2
+                ;;
+            --no-cache)
+                USE_CACHE="false"
+                shift
+                ;;
+            -h|--help)
+                show_help
+                ;;
+            *)
+                print_error "Unknown option: $1"
+                echo "Use --help for usage information"
+                exit 1
+                ;;
+        esac
+    done
+}
+
+# Function to validate arguments
+validate_args() {
+    # Validate source type
+    if [[ "$SOURCE_TYPE" != "local" && "$SOURCE_TYPE" != "github" ]]; then
+        print_error "Invalid source type: $SOURCE_TYPE (must be 'local' or 
'github')"
+        exit 1
+    fi
+    
+    # If building from local, check if we're in the right directory
+    if [[ "$SOURCE_TYPE" == "local" && ! -f "$PROJECT_ROOT/pom.xml" ]]; then
+        print_error "pom.xml not found in $PROJECT_ROOT"
+        print_error "Make sure you're running this script from the Hop project 
directory"
+        exit 1
+    fi
+}
+
+# Function to build an image
+build_image() {
+    local stage_name=$1
+    local image_name=$(get_image_name "$stage_name")
+    local variant_suffix=$(get_variant_suffix "$stage_name")
+    
+    if [ -z "$image_name" ]; then
+        print_warning "Unknown stage name: $stage_name (skipping)"
+        return 1
+    fi
+    
+    # Build full image name with optional registry prefix
+    local full_image_name
+    if [ -n "$REGISTRY" ]; then
+        full_image_name="$REGISTRY/$image_name"
+    else
+        full_image_name="$image_name"
+    fi
+    
+    # Display what we're building
+    if [ -n "$variant_suffix" ]; then
+        print_header "Building $image_name (variant: $variant_suffix)"
+    else
+        print_header "Building $image_name"
+    fi
+    
+    # Detect CPU cores for Maven threading
+    local cpu_cores
+    if [[ "$OSTYPE" == "darwin"* ]]; then
+        cpu_cores=$(sysctl -n hw.ncpu 2>/dev/null || echo "4")
+    else
+        cpu_cores=$(nproc 2>/dev/null || echo "4")
+    fi
+    local maven_threads="${MAVEN_THREADS:-1C}"
+    
+    # Determine the primary version tag (include variant suffix if applicable)
+    local version_tag="$VERSION"
+    if [ -n "$variant_suffix" ]; then
+        version_tag="${VERSION}-${variant_suffix}"
+    fi
+    
+    # Build arguments
+    local build_args=(
+        "--build-arg" "HOP_BUILD_FROM_SOURCE=$SOURCE_TYPE"
+        "--build-arg" "TARGET_IMAGE=$stage_name"
+        "--build-arg" "MAVEN_THREADS=$maven_threads"
+        "--build-arg" "BUILDER_TYPE=$BUILDER_TYPE"
+        "--progress" "$BUILD_PROGRESS"
+        "--file" "$SCRIPT_DIR/unified.Dockerfile"
+        "--target" "$stage_name"
+        "--tag" "$full_image_name:$version_tag"
+    )
+    
+    # Add git arguments if building from GitHub
+    if [[ "$SOURCE_TYPE" == "github" ]]; then
+        build_args+=(
+            "--build-arg" "HOP_GIT_REPO=$GIT_REPO"
+            "--build-arg" "HOP_GIT_TAG=$GIT_TAG"
+        )
+    fi
+    
+    # Add cache flag
+    if [[ "$USE_CACHE" == "false" ]]; then
+        build_args+=("--no-cache")
+    fi
+    
+    # Add additional tags based on version and variant
+    # Tag format examples:
+    #   Standard: hop-web:2.9.0, hop-web:Development, hop-web:latest
+    #   Variant:  hop-web:2.9.0-beam, hop-web:Development-beam, 
hop-web:beam-latest
+    if [[ "$VERSION" =~ SNAPSHOT ]]; then
+        # SNAPSHOT versions get "Development" tag
+        if [ -n "$variant_suffix" ]; then
+            build_args+=("--tag" 
"$full_image_name:Development-${variant_suffix}")
+        else
+            build_args+=("--tag" "$full_image_name:Development")
+        fi
+    else
+        # Release versions get "latest" tag
+        if [ -n "$variant_suffix" ]; then
+            build_args+=("--tag" "$full_image_name:${variant_suffix}-latest")
+        else
+            build_args+=("--tag" "$full_image_name:latest")
+        fi
+    fi
+    
+    # Build command (primary_tag already set as version_tag above)
+    if echo "$PLATFORMS" | grep -q ","; then
+        # Multi-platform build requires buildx
+        build_args+=(
+            "--platform" "$PLATFORMS"
+        )
+        
+        if [[ "$PUSH" == "true" ]]; then
+            build_args+=("--push")
+        else
+            build_args+=("--load")
+        fi
+        
+        print_info "Building multi-platform image: $PLATFORMS"
+        # Use default buildx builder (no need to create/destroy)
+        docker buildx build "${build_args[@]}" "$PROJECT_ROOT"
+    else
+        # Single platform build (faster, smaller, can use regular docker build)
+        print_info "Building for platform: $PLATFORMS"
+        
+        # Use buildx only if pushing, otherwise use regular docker build 
(faster)
+        if [[ "$PUSH" == "true" ]]; then
+            build_args+=(
+                "--platform" "$PLATFORMS"
+                "--push"
+            )
+            
+            # Use default buildx builder
+            docker buildx build "${build_args[@]}" "$PROJECT_ROOT"
+        else
+            # Regular docker build for single platform (no buildx overhead)
+            build_args+=(
+                "--platform" "$PLATFORMS"
+            )
+            docker build "${build_args[@]}" "$PROJECT_ROOT"
+        fi
+    fi
+    
+    print_success "Successfully built $full_image_name:$version_tag"
+}
+
+# Function to build all requested images
+build_images() {
+    print_header "Build Configuration"
+    echo "Source:        $SOURCE_TYPE"
+    if [[ "$SOURCE_TYPE" == "github" ]]; then
+        echo "Repository:    $GIT_REPO"
+        echo "Tag:           $GIT_TAG"
+    fi
+    echo "Images:        $IMAGES"
+    echo "Version:       $VERSION"
+    if [ -n "$REGISTRY" ]; then
+        echo "Registry:      $REGISTRY"
+    else
+        echo "Registry:      local (no prefix)"
+    fi
+    echo "Platforms:     $PLATFORMS"
+    echo "Builder type:  $BUILDER_TYPE"
+    if [[ "$BUILDER_TYPE" == "full" ]]; then
+        echo "Maven threads: $MAVEN_THREADS"
+    fi
+    echo "Push:          $PUSH"
+    echo "Cache:         $USE_CACHE"
+    echo ""
+    
+    # Determine which images to build
+    local images_to_build=()
+    if [[ "$IMAGES" == "all" ]]; then
+        # Build all images including variants
+        images_to_build=("${ALL_STAGES[@]}")
+    else
+        IFS=',' read -ra images_to_build <<< "$IMAGES"
+    fi
+    
+    # Build each image
+    for image in "${images_to_build[@]}"; do
+        image=$(echo "$image" | xargs) # trim whitespace
+        build_image "$image"
+    done
+}
+
+# Function to print summary
+print_summary() {
+    print_header "Build Summary"
+    
+    local images_to_check=()
+    if [[ "$IMAGES" == "all" ]]; then
+        # Check all images including variants
+        images_to_check=("${ALL_STAGES[@]}")
+    else
+        IFS=',' read -ra images_to_check <<< "$IMAGES"
+    fi
+    
+    echo "Built images:"
+    for image in "${images_to_check[@]}"; do
+        image=$(echo "$image" | xargs)
+        local image_name=$(get_image_name "$image")
+        local variant_suffix=$(get_variant_suffix "$image")
+        
+        if [[ -n "$image_name" ]]; then
+            # Build the full image name with registry
+            local full_image_name
+            if [ -n "$REGISTRY" ]; then
+                full_image_name="$REGISTRY/$image_name"
+            else
+                full_image_name="$image_name"
+            fi
+            
+            # Primary version tag (with variant suffix if applicable)
+            local version_tag="$VERSION"
+            if [ -n "$variant_suffix" ]; then
+                version_tag="${VERSION}-${variant_suffix}"
+            fi
+            echo "  - $full_image_name:$version_tag"
+            
+            # Additional alias tag (Development or latest)
+            if [[ "$VERSION" =~ SNAPSHOT ]]; then
+                if [ -n "$variant_suffix" ]; then
+                    echo "  - $full_image_name:Development-${variant_suffix}"
+                else
+                    echo "  - $full_image_name:Development"
+                fi
+            else
+                if [ -n "$variant_suffix" ]; then
+                    echo "  - $full_image_name:${variant_suffix}-latest"
+                else
+                    echo "  - $full_image_name:latest"
+                fi
+            fi
+        fi
+    done
+    
+    if [[ "$PUSH" == "true" ]]; then
+        print_success "Images pushed to registry"
+    else
+        print_info "Images available locally (not pushed)"
+    fi
+    
+    echo ""
+    print_success "Build completed successfully!"
+}
+
+################################################################################
+# Main execution
+################################################################################
+
+main() {
+    print_header "Apache Hop Docker Build System"
+    
+    # Load environment file if exists (before parsing args so args can 
override)
+    load_env_file
+    
+    # Parse command line arguments
+    parse_args "$@"
+    
+    # Check prerequisites
+    check_prerequisites
+    
+    # Validate arguments
+    validate_args
+    
+    # Detect version if not provided
+    if [[ -z "$VERSION" ]]; then
+        detect_version
+    fi
+    
+    # Build images
+    build_images
+    
+    # Print summary
+    print_summary
+}
+
+# Run main function
+main "$@"
+
diff --git a/docker/create_hop_rest_container.sh 
b/docker/create_hop_rest_container.sh
index 604f8e6c46..b7e41b394b 100755
--- a/docker/create_hop_rest_container.sh
+++ b/docker/create_hop_rest_container.sh
@@ -43,7 +43,7 @@ cp ../plugins/misc/projects/target/hop-plugins*.jar 
../assemblies/plugins/dist/t
 
 # Build the docker image.
 #
-docker build ../ -f Dockerfile.rest -t hop-rest # --progress=plain --no-cache
+docker build ../ -f rest.Dockerfile -t hop-rest # --progress=plain --no-cache
 
 # Cleanup
 #
diff --git a/docker/create_hop_web_container.sh 
b/docker/create_hop_web_container.sh
index b95283ce57..cd31341188 100755
--- a/docker/create_hop_web_container.sh
+++ b/docker/create_hop_web_container.sh
@@ -42,7 +42,7 @@ cp ../rap/target/hop-*SNAPSHOT.jar 
../assemblies/web/target/webapp/WEB-INF/lib/
 cp ../plugins/engines/beam/target/hop-plugins*.jar 
../assemblies/plugins/dist/target/plugins/engines/beam/
 
 #build docker image
-docker build ../ -f Dockerfile.web -t hop-web
+docker build ../ -f web.Dockerfile -t hop-web
 
 #cleanup
 rm -rf ../assemblies/web/target/webapp
diff --git a/docker/Dockerfile.dataflowTemplate 
b/docker/dataflowTemplate.Dockerfile
similarity index 100%
rename from docker/Dockerfile.dataflowTemplate
rename to docker/dataflowTemplate.Dockerfile
diff --git a/docker/integration-tests/integration-tests-base.yaml 
b/docker/integration-tests/integration-tests-base.yaml
index b0483b7aa3..51d1a1028a 100644
--- a/docker/integration-tests/integration-tests-base.yaml
+++ b/docker/integration-tests/integration-tests-base.yaml
@@ -20,7 +20,7 @@ services:
     image: hop-base-image
     build:
       context: ../../.
-      dockerfile: docker/integration-tests/Dockerfile.unit-tests
+      dockerfile: docker/integration-tests/unit-tests.Dockerfile
       args:
         - JENKINS_USER=jenkins
         - JENKINS_UID=1000
diff --git a/docker/integration-tests/Dockerfile.unit-tests 
b/docker/integration-tests/unit-tests.Dockerfile
similarity index 100%
rename from docker/integration-tests/Dockerfile.unit-tests
rename to docker/integration-tests/unit-tests.Dockerfile
diff --git a/docker/Dockerfile.rest b/docker/rest.Dockerfile
similarity index 100%
rename from docker/Dockerfile.rest
rename to docker/rest.Dockerfile
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
new file mode 100644
index 0000000000..d828fae106
--- /dev/null
+++ b/docker/unified.Dockerfile
@@ -0,0 +1,389 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+################################################################################
+# Unified Multi-Stage Dockerfile for Apache Hop
+# 
+# This Dockerfile can build all Hop container images using multi-stage builds:
+# - hop (client/server)
+# - hop-web
+# - hop-rest
+# - hop-dataflow-template
+#
+# Build arguments:
+#   HOP_BUILD_FROM_SOURCE: If "github", clones from GitHub; else uses local 
source
+#   HOP_GIT_REPO: GitHub repository URL
+#   HOP_GIT_TAG: Git tag/branch to build from
+#   HOP_VERSION: Version string for labeling
+#   TARGET_IMAGE: Which image to build (client, web, rest, dataflow)
+#   BUILDER_TYPE: Builder flavor (full, fast)
+################################################################################
+
+# Global build arguments (must be declared before any FROM to use in FROM 
statements)
+ARG BUILDER_TYPE=full
+
+################################################################################
+# Stage 1: Source preparation
+################################################################################
+FROM alpine:latest AS source-github
+ARG HOP_GIT_REPO=https://github.com/apache/hop.git
+ARG HOP_GIT_TAG=main
+
+RUN apk add --no-cache git
+WORKDIR /build
+RUN git clone --depth 1 --branch ${HOP_GIT_TAG} ${HOP_GIT_REPO} hop
+
+################################################################################
+# Stage 2a: Full Maven Builder (slower, complete build)
+################################################################################
+FROM maven:3.9-eclipse-temurin-17 AS builder-full
+ARG HOP_BUILD_FROM_SOURCE=local
+ARG MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
+ARG MAVEN_THREADS="1C"
+
+# Copy source - either from git clone or local context
+# For GitHub builds, copy from the git clone stage
+COPY --from=source-github /build/hop /tmp/github-hop
+
+# For local builds, copy directly to /build first (more efficient)
+COPY . /build
+
+# If building from GitHub, replace /build with GitHub source
+RUN if [ "${HOP_BUILD_FROM_SOURCE}" = "github" ]; then \
+        rm -rf /build/* /build/.* 2>/dev/null || true; \
+        cp -a /tmp/github-hop/. /build/; \
+    fi
+
+WORKDIR /build
+
+# Verify pom.xml exists
+RUN if [ ! -f "pom.xml" ]; then \
+        echo "ERROR: pom.xml not found in /build"; \
+        echo "Contents of /build:"; \
+        ls -la /build/ || true; \
+        exit 1; \
+    fi
+
+# Build with Maven - produces artifacts in target/ folders
+RUN mvn clean install -T ${MAVEN_THREADS} -B -C -e -V \
+    -Djacoco.skip=true \
+    -Drat.skip=true \
+    -Dcheckstyle.skip=true \
+    -Dspotless.skip=true \
+    -DskipTests=true \
+    -Daether.syncContext.named.time=300 \
+    --file pom.xml
+
+
+
+################################################################################
+# Stage 2b: Fast Builder (uses pre-built artifacts, for local dev)
+################################################################################
+# This builder skips Maven and just copies pre-built artifacts
+# Use this when you've already run "mvn clean install" locally
+# Expects the project to be built with artifacts in target/ folders
+FROM alpine:latest AS builder-fast
+
+WORKDIR /build
+
+# Copy pre-built artifacts from context (must exist!)
+COPY ./assemblies/client/target/hop-client-*.zip 
/build/assemblies/client/target/
+COPY ./assemblies/web/target/hop.war /build/assemblies/web/target/
+COPY ./assemblies/plugins/target/hop-assemblies-*.zip 
/build/assemblies/plugins/target/
+COPY ./rest/target/hop-rest*.war /build/rest/target/
+COPY ./docker/resources/ /build/docker/resources/
+
+# builder-fast produces the same artifacts as builder-full:
+# - /build/assemblies/client/target/hop-client-*.zip
+# - /build/assemblies/web/target/hop.war
+# - /build/assemblies/plugins/target/hop-assemblies-*.zip
+# - /build/rest/target/hop-rest*.war
+# - /build/docker/resources/*
+#
+# These will be extracted and prepared in Stage 3
+
+################################################################################
+# Stage 2c: Builder Selector
+################################################################################
+# This stage selects which builder to use based on BUILDER_TYPE build arg
+# Default is "full" for complete Maven build
+# NOTE: BUILDER_TYPE is declared globally at the top of this file
+FROM builder-${BUILDER_TYPE} AS builder-selected
+
+################################################################################
+# Stage 3: Common Preparation Stage (ALL preparation logic in ONE place!)
+################################################################################
+# This stage:
+# 1. Extracts/unzips artifacts from Stage 2
+# 2. Generates fat jar
+# 3. Prepares optimized directory structures for final images
+#
+# ANY new builder flavor just needs to produce the same artifacts as Stage 2,
+# and this stage will handle everything else!
+################################################################################
+FROM alpine:latest AS builder
+
+# Install tools needed for preparation
+RUN apk add --no-cache unzip bash openjdk17-jre
+
+WORKDIR /build
+
+# Copy artifacts from selected builder
+COPY --from=builder-selected /build/ /build/
+
+# Step 1: Unzip and extract all assemblies
+RUN echo "=== Extracting assemblies ===" && \
+    # Unzip client assembly
+    if [ -f assemblies/client/target/hop-client-*.zip ]; then \
+        cd assemblies/client/target && \
+        unzip -q hop-client-*.zip && \
+        cd /build; \
+    else \
+        echo "ERROR: Client assembly not found" && exit 1; \
+    fi && \
+    # Unzip web assembly
+    if [ -f assemblies/web/target/hop.war ]; then \
+        cd assemblies/web/target && \
+        unzip -q hop.war -d webapp && \
+        cd /build; \
+    else \
+        echo "WARNING: Web assembly not found, skipping"; \
+    fi && \
+    # Unzip plugins assembly
+    if [ -d assemblies/plugins/target ] && ls 
assemblies/plugins/target/hop-assemblies-*.zip 1> /dev/null 2>&1; then \
+        cd assemblies/plugins/target && \
+        unzip -q hop-assemblies-*.zip && \
+        cd /build; \
+    else \
+        echo "WARNING: Plugins assembly not found, will use built plugins 
directly"; \
+    fi
+
+# Step 2: Generate fat jar for dataflow template
+RUN if [ -f /build/assemblies/client/target/hop/hop-conf.sh ]; then \
+        /build/assemblies/client/target/hop/hop-conf.sh \
+        --generate-fat-jar=/tmp/hop-fatjar.jar; \
+    else \
+        echo "ERROR: hop-conf.sh not found" && exit 1; \
+    fi
+
+# Step 3: Prepare optimized directory structures for final images
+RUN mkdir -p /build/hop-web-prepared/webapps/ROOT && \
+    cp -r /build/assemblies/web/target/webapp/* 
/build/hop-web-prepared/webapps/ROOT/ && \
+    cp -r /build/assemblies/client/target/hop/config 
/build/hop-web-prepared/webapps/ROOT/ && \
+    cp -r /build/assemblies/client/target/hop/plugins /build/hop-web-prepared/ 
&& \
+    cp -r /build/assemblies/client/target/hop/lib/jdbc/ 
/build/hop-web-prepared/jdbc-drivers && \
+    cp -r /build/assemblies/client/target/hop/lib/beam/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
+    cp -r /build/assemblies/client/target/hop/lib/core/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
+    rm /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/hop-ui-rcp* && \
+    mkdir -p /build/hop-web-prepared/bin && \
+    cp -r /build/docker/resources/run-web.sh 
/build/hop-web-prepared/bin/run-web.sh
+
+# Make scripts executable
+RUN chmod +x /build/hop-web-prepared/webapps/ROOT/*.sh \
+    && chmod +x /build/hop-web-prepared/bin/run-web.sh
+
+    # Fix hop-config.json
+RUN sed -i 's/config\/projects/${HOP_CONFIG_FOLDER}\/projects/g' 
/build/hop-web-prepared/webapps/ROOT/config/hop-config.json
+
+# Set the correct classpath for hop scripts
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-run.sh
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-conf.sh
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-search.sh
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-encrypt.sh
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-import.sh
+RUN sed -i 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' 
/build/hop-web-prepared/webapps/ROOT/hop-search.sh
+
+
+# Prepare Hop Client directory structure
+RUN mkdir -p /build/hop-client-prepared && \
+    # Copy entire hop installation
+    cp -r /build/assemblies/client/target/hop/* /build/hop-client-prepared/ && 
\
+    # Copy run scripts
+    cp /build/docker/resources/run.sh /build/hop-client-prepared/run.sh && \
+    cp /build/docker/resources/load-and-execute.sh 
/build/hop-client-prepared/load-and-execute.sh && \
+    chmod +x /build/hop-client-prepared/run.sh 
/build/hop-client-prepared/load-and-execute.sh
+
+# Prepare Hop REST directory structure
+RUN mkdir -p /build/hop-rest-prepared/plugins && \
+    mkdir -p /build/hop-rest-prepared/webapps && \
+    mkdir -p /build/hop-rest-prepared/lib/swt/linux/x86_64 && \
+    mkdir -p /build/hop-rest-prepared/bin && \
+    # Copy plugins
+    cp -r /build/assemblies/plugins/target/plugins/* 
/build/hop-rest-prepared/plugins/ && \
+    # Copy REST war
+    cp /build/rest/target/hop-rest*.war 
/build/hop-rest-prepared/webapps/hop.war && \
+    # Copy run script
+    cp /build/docker/resources/run-rest.sh 
/build/hop-rest-prepared/bin/run-rest.sh && \
+    chmod +x /build/hop-rest-prepared/bin/run-rest.sh
+
+################################################################################
+# Stage 4a: Hop Client/Server Image (Standard)
+################################################################################
+FROM alpine:latest AS client
+
+# Build arguments
+ARG HOP_UID=501
+ARG HOP_GID=501
+
+# Environment variables
+ENV DEPLOYMENT_PATH=/opt/hop
+ENV VOLUME_MOUNT_POINT=/files
+ENV HOP_LOG_LEVEL=Basic
+ENV HOP_FILE_PATH=
+ENV HOP_LOG_PATH=${DEPLOYMENT_PATH}/hop.err.log
+ENV HOP_SHARED_JDBC_FOLDERS=
+ENV HOP_PROJECT_NAME=
+ENV HOP_PROJECT_DIRECTORY=
+ENV HOP_PROJECT_FOLDER=
+ENV HOP_PROJECT_CONFIG_FILE_NAME=project-config.json
+ENV HOP_ENVIRONMENT_NAME=
+ENV HOP_ENVIRONMENT_CONFIG_FILE_NAME_PATHS=
+ENV HOP_RUN_CONFIG=
+ENV HOP_RUN_PARAMETERS=
+ENV HOP_START_ACTION=
+ENV HOP_RUN_METADATA_EXPORT=
+ENV HOP_SYSTEM_PROPERTIES=
+ENV HOP_OPTIONS=-XX:+AggressiveHeap
+ENV HOP_CUSTOM_ENTRYPOINT_EXTENSION_SHELL_FILE_PATH=
+ENV HOP_SERVER_USER=cluster
+ENV HOP_SERVER_PASSWORD=cluster
+ENV HOP_SERVER_HOSTNAME=0.0.0.0
+ENV HOP_SERVER_PORT=8080
+ENV HOP_SERVER_SHUTDOWNPORT=8079
+ENV HOP_SERVER_METADATA_FOLDER=
+ENV HOP_SERVER_KEYSTORE=
+ENV HOP_SERVER_KEYSTORE_PASSWORD=
+ENV HOP_SERVER_KEY_PASSWORD=
+ENV HOP_SERVER_MAX_LOG_LINES=
+ENV HOP_SERVER_MAX_LOG_TIMEOUT=
+ENV HOP_SERVER_MAX_OBJECT_TIMEOUT=
+ENV HOP_CONFIG_OPTIONS=
+
+# Install required packages
+RUN addgroup -g ${HOP_GID} -S hop \
+    && adduser -u ${HOP_UID} -S -D -G hop hop \
+    && chmod 777 -R /tmp && chmod o+t -R /tmp \
+    && apk update \
+    && apk --no-cache add bash curl fontconfig msttcorefonts-installer 
openjdk17-jre procps \
+    && update-ms-fonts \
+    && fc-cache -f \
+    && rm -rf /var/cache/apk/* \
+    && mkdir ${DEPLOYMENT_PATH} \
+    && mkdir ${VOLUME_MOUNT_POINT} \
+    && chown hop:hop ${DEPLOYMENT_PATH} \
+    && chown hop:hop ${VOLUME_MOUNT_POINT}
+
+# Copy pre-assembled Hop Client structure from builder (SINGLE layer!)
+COPY --from=builder --chown=hop:hop /build/hop-client-prepared/ 
${DEPLOYMENT_PATH}/
+
+# Expose ports
+EXPOSE 8080 8079
+
+# Configure
+VOLUME ["/files"]
+USER hop
+ENV PATH=${PATH}:${DEPLOYMENT_PATH}/hop
+WORKDIR /home/hop
+
+CMD bash -c "$DEPLOYMENT_PATH/run.sh"
+
+################################################################################
+# Stage 4b: Hop Web Image
+################################################################################
+FROM tomcat:10-jdk17 AS web
+
+# Build arguments
+ARG HOP_UID=501
+ARG HOP_GID=501
+
+# Environment variables
+ENV DEPLOYMENT_PATH=/usr/local/tomcat/webapps/ROOT
+ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
+ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
+ENV HOP_LOG_LEVEL="Basic"
+ENV HOP_OPTIONS="-XX:+AggressiveHeap 
-Dorg.eclipse.rap.rwt.resourceLocation=/tmp/rwt-resources"
+ENV HOP_PASSWORD_ENCODER_PLUGIN="Hop"
+ENV HOP_PLUGIN_BASE_FOLDERS=${CATALINA_HOME}/plugins
+ENV HOP_SHARED_JDBC_FOLDERS="${CATALINA_HOME}/jdbc-drivers"
+ENV HOP_WEB_THEME="light"
+ENV HOP_GUI_ZOOM_FACTOR=1.0
+ENV HOP_PROJECT_FOLDER=
+ENV HOP_PROJECT_CONFIG_FILE_NAME=project-config.json
+ENV HOP_ENVIRONMENT_NAME=environment1
+ENV HOP_ENVIRONMENT_CONFIG_FILE_NAME_PATHS=
+
+# Set TOMCAT start variables
+ENV CATALINA_OPTS='${HOP_OPTIONS} \
+  -DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+  -DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
+  -DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
+  -DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
+  -DHOP_PASSWORD_ENCODER_PLUGIN="${HOP_PASSWORD_ENCODER_PLUGIN}" \
+  -DHOP_PLUGIN_BASE_FOLDERS="${HOP_PLUGIN_BASE_FOLDERS}" \
+  -DHOP_SHARED_JDBC_FOLDERS="${HOP_SHARED_JDBC_FOLDERS}" \
+  -DHOP_WEB_THEME="${HOP_WEB_THEME}" \
+  -DHOP_GUI_ZOOM_FACTOR="${HOP_GUI_ZOOM_FACTOR}"'
+
+# Create Hop user
+RUN groupadd -r hop -g ${HOP_GID} \
+    && useradd -d /home/hop -u ${HOP_UID} -m -s /bin/bash -g hop hop \
+    && rm -rf webapps/* \
+    && mkdir "${CATALINA_HOME}"/webapps/ROOT \
+    && mkdir "${HOP_AUDIT_FOLDER}" \
+    && chown -R hop:hop /usr/local/tomcat
+
+# Copy resources (matching original Dockerfile.web layer structure)
+COPY --from=builder --chown=hop /build/hop-web-prepared/ "${CATALINA_HOME}"
+
+USER hop
+
+CMD bash -c "$CATALINA_HOME/bin/run-web.sh"
+
+
+################################################################################
+# Stage 4c: Hop Dataflow Template Image
+################################################################################
+FROM gcr.io/dataflow-templates-base/java17-template-launcher-base AS dataflow
+
+ARG WORKDIR=/dataflow/template
+RUN mkdir -p ${WORKDIR}
+WORKDIR ${WORKDIR}
+
+# Copy fat jar from builder
+COPY --from=builder /tmp/hop-fatjar.jar ./
+
+ENV 
FLEX_TEMPLATE_JAVA_MAIN_CLASS="org.apache.hop.beam.run.MainDataflowTemplate"
+ENV FLEX_TEMPLATE_JAVA_CLASSPATH="${WORKDIR}/*"
+
+ENTRYPOINT ["/opt/google/dataflow/java_template_launcher"]
+
+################################################################################
+# Image Variants/Flavors
+################################################################################
+# These stages extend base stages to create variants with additional features
+# Stage names follow pattern: <base>-<variant> (e.g., web-beam, client-minimal)
+################################################################################
+
+################################################################################
+# Stage 4e: Hop Web with Beam (includes fat jar for Dataflow)
+################################################################################
+FROM web AS web-beam
+LABEL variant="beam"
+
+# Copy fat jar from builder into web image for Dataflow integration
+COPY --from=builder /tmp/hop-fatjar.jar /root/hop-fatjar.jar
\ No newline at end of file
diff --git a/docker/Dockerfile.web-fatjar b/docker/web-fatjar.Dockerfile
similarity index 100%
rename from docker/Dockerfile.web-fatjar
rename to docker/web-fatjar.Dockerfile
diff --git a/docker/Dockerfile.web b/docker/web.Dockerfile
similarity index 100%
rename from docker/Dockerfile.web
rename to docker/web.Dockerfile
diff --git a/docs/hop-dev-manual/modules/ROOT/nav.adoc 
b/docs/hop-dev-manual/modules/ROOT/nav.adoc
index 5c4a869f9c..c81487ca81 100644
--- a/docs/hop-dev-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/nav.adoc
@@ -19,6 +19,7 @@ under the License.
 * xref:porting-kettle-plugins.adoc[Porting Kettle plugins]
 * xref:metadata-plugins.adoc[Metadata plugins]
 * xref:setup-dev-environment.adoc[Setting up your development environment]
+* xref:docker-build.adoc[Docker Build Script]
 * xref:integration-testing.adoc[Integration testing]
 * xref:internationalisation.adoc[Internationalisation (i18n)]
 * xref:plugin-development.adoc[Plugins Development]
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/docker-build.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/docker-build.adoc
new file mode 100644
index 0000000000..5ebcdf9b46
--- /dev/null
+++ b/docs/hop-dev-manual/modules/ROOT/pages/docker-build.adoc
@@ -0,0 +1,498 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+:description: The Apache Hop Docker Build Script provides a unified way to 
build all Hop container images using multi-stage Docker builds.
+
+[[docker-build]]
+= Docker Build Script
+
+The Apache Hop project provides a unified build script (`build-hop-images.sh`) 
that can build all Apache Hop Docker images using a multi-stage build approach. 
This script supports building from local source or from a GitHub tag, with 
options for multi-platform builds and registry pushing.
+
+== Overview
+
+The build script creates the following Docker images:
+
+* **hop** (client) - The main Apache Hop client/server image based on Alpine 
Linux
+* **hop-web** - Apache Hop Web interface running on Tomcat
+* **hop-web-beam** - Hop Web variant with Apache Beam fat jar for Google Cloud 
Dataflow integration
+* **hop-dataflow-template** - Google Cloud Dataflow Flex Template image
+
+== Build Architecture
+
+The build process uses a unified multi-stage Dockerfile that shares common 
build stages across all images, reducing build time and ensuring consistency.
+
+[mermaid]
+....
+flowchart TB
+    subgraph Source["Stage 1: Source Preparation"]
+        SG[source-github<br/>Clone from GitHub]
+        SL[Local Source<br/>From build context]
+    end
+
+    subgraph Builder["Stage 2: Builder"]
+        BF[builder-full<br/>Maven build from source]
+        BFast[builder-fast<br/>Pre-built artifacts]
+    end
+
+    subgraph Prep["Stage 3: Preparation"]
+        BP[builder<br/>Extract & prepare artifacts<br/>Generate fat jar]
+    end
+
+    subgraph Final["Stage 4: Final Images"]
+        IC["client<br/>Alpine + JRE<br/>&lt;registry&gt;/hop:&lt;version&gt;"]
+        IW["web<br/>Tomcat<br/>&lt;registry&gt;/hop-web:&lt;version&gt;"]
+        IWB["web-beam<br/>Tomcat + fat 
jar<br/>&lt;registry&gt;/hop-web:&lt;version&gt;-beam"]
+        ID["dataflow<br/>GCP Dataflow 
base<br/>&lt;registry&gt;/hop-dataflow-template:&lt;version&gt;"]
+    end
+
+    SG --> BF
+    SL --> BFast
+    BF --> BP
+    BFast --> BP
+    BP --> IC
+    BP --> IW
+    BP --> ID
+    IW --> IWB
+
+    style SG fill:#e1f5fe
+    style SL fill:#e1f5fe
+    style BF fill:#fff3e0
+    style BFast fill:#fff3e0
+    style BP fill:#f3e5f5
+    style IC fill:#e8f5e9
+    style IW fill:#e8f5e9
+    style IWB fill:#c8e6c9
+    style ID fill:#e8f5e9
+....
+
+== Prerequisites
+
+Before using the build script, ensure you have:
+
+* **Docker** - Version 20.10 or higher
+* **Docker Buildx** - Required only for multi-platform builds (usually 
included with Docker Desktop)
+* **Maven** - Only if building locally before using `--builder fast`
+
+== Command Line Arguments
+
+The build script supports the following arguments:
+
+[cols="2,1,3,2", options="header"]
+|===
+|Argument |Short |Description |Default
+
+|`--source <type>`
+|`-s`
+|Build from `local` source or `github`
+|`local`
+
+|`--tag <tag>`
+|`-t`
+|Git tag or branch to build from (when using `--source github`)
+|`main`
+
+|`--repo <url>`
+|`-r`
+|GitHub repository URL
+|`https://github.com/apache/hop.git`
+
+|`--images <list>`
+|`-i`
+|Comma-separated list of images to build, or `all`
+|`all`
+
+|`--version <version>`
+|`-v`
+|Version string for image tagging
+|Auto-detected from pom.xml
+
+|`--push`
+|`-p`
+|Push images to registry after build
+|`false`
+
+|`--registry <registry>`
+|`-x`
+|Docker registry prefix (e.g., `apache`, `myregistry.io/myorg`)
+|None (local only)
+
+|`--platforms <platforms>`
+|
+|Build platforms (e.g., `linux/amd64,linux/arm64`)
+|Current system platform
+
+|`--maven-threads <threads>`
+|
+|Maven build parallelism (e.g., `1C`, `2C`, `4`)
+|`1C` (1 thread per CPU core)
+
+|`--progress <mode>`
+|
+|Docker build output: `auto`, `plain` (verbose), `tty` (compact)
+|`auto`
+
+|`--builder <type>`
+|
+|Builder type: `full` (Maven build) or `fast` (pre-built artifacts)
+|`full`
+
+|`--no-cache`
+|
+|Build without using Docker cache
+|Cache enabled
+
+|`--help`
+|`-h`
+|Show help message
+|
+|===
+
+== Available Image Stages
+
+The following image stages can be specified with the `--images` argument:
+
+[cols="1,2,3", options="header"]
+|===
+|Stage |Image Name |Description
+
+|`client`
+|`hop`
+|Main Hop client/server image. Based on Alpine Linux with OpenJDK 17. Can run 
pipelines, workflows, and Hop Server.
+
+|`web`
+|`hop-web`
+|Hop Web interface running on Apache Tomcat 10. Provides browser-based access 
to the Hop GUI.
+
+|`web-beam`
+|`hop-web` (variant)
+|Hop Web with Apache Beam fat jar included. Use for Google Cloud Dataflow 
integration. Tagged with `-beam` suffix.
+
+|`dataflow`
+|`hop-dataflow-template`
+|Google Cloud Dataflow Flex Template image. Contains only the fat jar for 
running Hop pipelines on Dataflow.
+|===
+
+== Builder Types
+
+=== Full Builder (Default)
+
+The `full` builder performs a complete Maven build from source:
+
+[source,bash]
+----
+./build-hop-images.sh --builder full
+----
+
+* Clones source code
+* Runs full Maven build with all dependencies
+* Slower but ensures everything is built from scratch
+* Required for CI/CD and release builds
+
+=== Fast Builder
+
+The `fast` builder uses pre-built artifacts, skipping Maven:
+
+[source,bash]
+----
+# First, build with Maven locally
+mvn clean install -DskipTests
+
+# Then build Docker images using pre-built artifacts (only Hop-web in this 
example)
+./build-hop-images.sh --builder fast --images web
+----
+
+* Requires `mvn clean install` to be run first
+* Much faster for local development iteration
+* Copies artifacts directly from `target/` folders
+
+== Image Tagging
+
+Images are automatically tagged based on the version:
+
+* **SNAPSHOT versions** (e.g., `2.17.0-SNAPSHOT`):
+** Primary tag: `hop-web:2.17.0-SNAPSHOT`
+** Alias tag: `hop-web:Development`
+
+* **Release versions** (e.g., `2.17.0`):
+** Primary tag: `hop-web:2.17.0`
+** Alias tag: `hop-web:latest`
+
+* **Variant images** get suffix added:
+** Primary tag: `hop-web:2.17.0-SNAPSHOT-beam`
+** Alias tag: `hop-web:Development-beam`
+
+== Examples
+
+=== Basic Usage
+
+Build all images from local source:
+
+[source,bash]
+----
+./build-hop-images.sh
+----
+
+=== Build Specific Images
+
+Build only the web and client images:
+
+[source,bash]
+----
+./build-hop-images.sh --images client,web
+----
+
+=== Build from GitHub Release
+
+Build version 2.9.0 from GitHub:
+
+[source,bash]
+----
+./build-hop-images.sh --source github --tag 2.9.0
+----
+
+=== Fast Development Build
+
+For quick iteration during development:
+
+[source,bash]
+----
+# Build project with Maven first
+mvn clean install -DskipTests
+
+# Fast Docker build (skips Maven, uses pre-built artifacts)
+./build-hop-images.sh --builder fast --images web
+----
+
+=== Multi-Platform Build with Push
+
+Build for AMD64 and ARM64, then push to Docker Hub:
+
+[source,bash]
+----
+./build-hop-images.sh \
+    --platforms linux/amd64,linux/arm64 \
+    --push \
+    --registry apache \
+    --version 2.9.0
+----
+
+=== Build Web with Beam Variant
+
+Build the web image with Apache Beam fat jar for Dataflow:
+
+[source,bash]
+----
+./build-hop-images.sh --images web-beam --registry myregistry
+----
+
+This creates:
+
+* `myregistry/hop-web:2.17.0-SNAPSHOT-beam`
+* `myregistry/hop-web:Development-beam`
+
+=== Verbose Build Output
+
+See detailed build progress:
+
+[source,bash]
+----
+./build-hop-images.sh --progress plain --images client
+----
+
+=== Build Without Cache
+
+Force a fresh build without using cached layers:
+
+[source,bash]
+----
+./build-hop-images.sh --no-cache --images web
+----
+
+== Configuration File
+
+You can create a `build.env` file in the `docker/` directory to set default 
values:
+
+[source,bash]
+----
+# docker/build.env
+REGISTRY=myregistry.io/myorg
+PLATFORMS=linux/amd64,linux/arm64
+PUSH=true
+MAVEN_THREADS=2C
+----
+
+Command line arguments override values from `build.env`.
+
+== Troubleshooting
+
+=== Build Fails with "pom.xml not found"
+
+Ensure you're running the script from the repository root or the `docker/` 
directory:
+
+[source,bash]
+----
+cd /path/to/hop
+./docker/build-hop-images.sh
+----
+
+=== Multi-Platform Build Fails
+
+Multi-platform builds require Docker Buildx:
+
+[source,bash]
+----
+# Check if buildx is available
+docker buildx version
+
+# Create a builder if needed
+docker buildx create --use --name hop-builder
+----
+
+=== Out of Memory During Maven Build
+
+Increase Maven memory by setting threads lower:
+
+[source,bash]
+----
+./build-hop-images.sh --maven-threads 1
+----
+
+=== Images Not Appearing Locally After Multi-Platform Build
+
+Multi-platform builds require `--push` or won't load locally. For local 
testing, use single platform:
+
+[source,bash]
+----
+# Single platform (loads locally)
+./build-hop-images.sh --platforms linux/amd64
+
+# Multi-platform requires push
+./build-hop-images.sh --platforms linux/amd64,linux/arm64 --push --registry 
myregistry
+----
+
+== Adding a New Image Variant
+
+You can extend the build system by adding new image variants. Variants are 
images that extend a base image with additional features. For example, 
`web-beam` extends the `web` image with the Apache Beam fat jar.
+
+This section demonstrates how to add a hypothetical `client-debug` variant 
that includes additional debugging tools.
+
+=== Step 1: Add the Stage to unified.Dockerfile
+
+Add a new stage at the end of `docker/unified.Dockerfile` that extends the 
base image:
+
+[source,dockerfile]
+----
+################################################################################
+# Stage: Hop Client with Debug Tools
+################################################################################
+FROM client AS client-debug
+LABEL variant="debug"
+
+# Switch to root to install packages
+USER root
+
+# Install debugging tools
+RUN apk add --no-cache \
+    strace \
+    htop \
+    vim \
+    curl \
+    netcat-openbsd
+
+# Add debug-specific environment variables
+ENV HOP_OPTIONS="-XX:+AggressiveHeap 
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
+
+# Expose debug port
+EXPOSE 5005
+
+# Switch back to hop user
+USER hop
+----
+
+Key points:
+
+* **Stage name format**: Use `<base>-<variant>` naming (e.g., `client-debug`)
+* **FROM clause**: Extend the base image (`FROM client AS client-debug`)
+* **LABEL**: Add `variant="debug"` label for identification
+* **Inheritance**: The variant inherits everything from the base image
+
+=== Step 2: Register the Variant in build-hop-images.sh
+
+Add the new variant to the `ALL_STAGES` array at the top of 
`docker/build-hop-images.sh`:
+
+[source,bash]
+----
+# Available image stages (add new stages here)
+# Format: "baseImage" or "baseImage-variant"
+ALL_STAGES=("client" "client-debug" "web" "web-beam" "dataflow")
+----
+
+The build script automatically handles:
+
+* **Image naming**: `client-debug` → image name `hop` (from base `client`)
+* **Tag suffix**: Version tag gets `-debug` suffix (e.g., 
`hop:2.17.0-SNAPSHOT-debug`)
+* **Alias tags**: Development/latest tags also get suffix (e.g., 
`hop:Development-debug`)
+
+=== Step 3: Build and Test the Variant
+
+Build only the new variant:
+
+[source,bash]
+----
+./build-hop-images.sh --images client-debug --registry myregistry
+----
+
+This produces:
+
+* `myregistry/hop:2.17.0-SNAPSHOT-debug`
+* `myregistry/hop:Development-debug`
+
+Build all images including the new variant:
+
+[source,bash]
+----
+./build-hop-images.sh --images all
+----
+
+=== How Variant Detection Works
+
+The build script uses these functions to handle variants:
+
+[source,bash]
+----
+# Extracts base image name from stage name
+get_image_name() {
+    local stage_name="$1"
+    case "$stage_name" in
+        client*)   echo "hop" ;;
+        web*)      echo "hop-web" ;;
+        dataflow*) echo "hop-dataflow-template" ;;
+        *)         echo "" ;;
+    esac
+}
+
+# Extracts variant suffix (everything after first hyphen)
+get_variant_suffix() {
+    local stage_name="$1"
+    if [[ "$stage_name" == *"-"* ]]; then
+        echo "${stage_name#*-}"  # Returns "debug" from "client-debug"
+    else
+        echo ""
+    fi
+}
+----
+
+For a new base image type (not a variant of existing), you would also need to 
update the `get_image_name()` function.
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/index.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
index f4ef9727d1..4afeabf2cd 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
@@ -20,6 +20,7 @@ under the License.
 * xref:metadata-plugins.adoc[Metadata plugins]
 * xref:porting-kettle-plugins.adoc[Porting Kettle plugins]
 * xref:setup-dev-environment.adoc[Setting up your development environment]
+* xref:docker-build.adoc[Docker Build Script]
 * xref:integration-testing.adoc[Integration Testing]
 * xref:plugin-development.adoc[Plugin Development]
 * xref:plugin-samples.adoc[Plugin Samples]
diff --git a/integration-tests/samples/read-samples-build-hop-run.hpl 
b/integration-tests/samples/read-samples-build-hop-run.hpl
index ca8be57ac2..88f53e4a8b 100644
--- a/integration-tests/samples/read-samples-build-hop-run.hpl
+++ b/integration-tests/samples/read-samples-build-hop-run.hpl
@@ -640,7 +640,7 @@ var hop_run_cmd = './hop-run.sh -j samples -r ' + 
run_config + ' -f ' + filename
         <item>Is intended to fail.</item>
       </line>
       <line>
-        <item>sample_pipeline_resolver</item>
+        <item>sample_pipeline_resolver.hpl</item>
         <item>Is used by another transform</item>
       </line>
     </data>

Reply via email to