Files
foxhunt/scripts/build_docker_images.sh
jgrusewski 267240530d perf(ci): enable Kaniko layer caching + Docker Hub auth on all builds
- Add --cache=true --cache-repo to all 12 Kaniko builds
- Cache Docker layers in Scaleway CR (rg.fr-par.scw.cloud/foxhunt-ci/cache)
- Add Docker Hub auth to devcontainer + infra-runner prepare jobs
- First build populates cache; subsequent builds skip base image pulls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:20:44 +01:00

538 lines
15 KiB
Bash
Executable File

#!/bin/bash
# =============================================================================
# PRODUCTION DOCKER BUILD SCRIPT - AUTOMATIC VERSIONING
# =============================================================================
# Purpose: Build and push Docker images with automatic versioning and validation
# Features:
# - Automatic version tagging (git commit, timestamp, latest)
# - BuildKit optimization with cache mounts
# - Binary validation in built image
# - Image size reporting
# - Build time measurement
# - Multi-platform support (future)
# - Error handling with exit codes
#
# Usage:
# ./scripts/build_docker_images.sh # Build and push with all defaults
# ./scripts/build_docker_images.sh --no-push # Build only, skip push
# ./scripts/build_docker_images.sh --dry-run # Show what would be built
# ./scripts/build_docker_images.sh --dockerfile Dockerfile.runpod # Specify Dockerfile
# ./scripts/build_docker_images.sh --platform linux/amd64 # Specific platform
# ./scripts/build_docker_images.sh --skip-validation # Skip binary validation
#
# Exit codes:
# 0 - Success
# 1 - Build failed
# 2 - Validation failed
# 3 - Push failed
# 4 - Invalid arguments
# =============================================================================
set -euo pipefail
# =============================================================================
# COLOR DEFINITIONS
# =============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# =============================================================================
# CONFIGURATION
# =============================================================================
DOCKER_REGISTRY="jgrusewski"
IMAGE_NAME="foxhunt-hyperopt"
DEFAULT_DOCKERFILE="Dockerfile.foxhunt-build"
EXPECTED_BINARIES=(
"hyperopt_baseline_rl"
"hyperopt_baseline_supervised"
)
# =============================================================================
# SCRIPT VARIABLES
# =============================================================================
DOCKERFILE="${DEFAULT_DOCKERFILE}"
DO_PUSH=true
DRY_RUN=false
PLATFORM=""
SKIP_VALIDATION=false
BUILD_START_TIME=""
BUILD_END_TIME=""
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
# Print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_header() {
echo -e "\n${CYAN}==============================================================================${NC}"
echo -e "${CYAN}$1${NC}"
echo -e "${CYAN}==============================================================================${NC}\n"
}
# Calculate time difference
time_diff() {
local start=$1
local end=$2
local diff=$((end - start))
echo "${diff}s"
}
# Format bytes to human-readable
format_bytes() {
local bytes=$1
if [ "$bytes" -lt 1024 ]; then
echo "${bytes}B"
elif [ "$bytes" -lt 1048576 ]; then
echo "$((bytes / 1024))KB"
elif [ "$bytes" -lt 1073741824 ]; then
echo "$((bytes / 1048576))MB"
else
echo "$((bytes / 1073741824))GB"
fi
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Show usage
usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS]
Build and push Docker images with automatic versioning.
OPTIONS:
--dockerfile FILE Dockerfile to build (default: ${DEFAULT_DOCKERFILE})
--no-push Build only, skip pushing to registry
--dry-run Show what would be built without building
--platform PLATFORM Build for specific platform (e.g., linux/amd64)
--skip-validation Skip binary validation after build
-h, --help Show this help message
EXAMPLES:
$(basename "$0") # Build and push with all defaults
$(basename "$0") --no-push # Build only, no push
$(basename "$0") --dry-run # Show build plan
$(basename "$0") --dockerfile Dockerfile.runpod # Specific Dockerfile
$(basename "$0") --platform linux/amd64 # Specific platform
EXIT CODES:
0 - Success
1 - Build failed
2 - Validation failed
3 - Push failed
4 - Invalid arguments
EOF
}
# =============================================================================
# ARGUMENT PARSING
# =============================================================================
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--dockerfile)
DOCKERFILE="$2"
shift 2
;;
--no-push)
DO_PUSH=false
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--platform)
PLATFORM="$2"
shift 2
;;
--skip-validation)
SKIP_VALIDATION=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
print_error "Unknown option: $1"
usage
exit 4
;;
esac
done
}
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
# Check prerequisites
check_prerequisites() {
print_header "CHECKING PREREQUISITES"
# Check Docker
if ! command_exists docker; then
print_error "Docker is not installed"
exit 4
fi
print_success "Docker: $(docker --version)"
# Check git
if ! command_exists git; then
print_error "Git is not installed"
exit 4
fi
print_success "Git: $(git --version)"
# Check if in git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not in a git repository"
exit 4
fi
print_success "Git repository detected"
# Check if Dockerfile exists
if [ ! -f "$DOCKERFILE" ]; then
print_error "Dockerfile not found: $DOCKERFILE"
exit 4
fi
print_success "Dockerfile: $DOCKERFILE"
# Check Docker daemon
if ! docker info > /dev/null 2>&1; then
print_error "Docker daemon is not running"
exit 4
fi
print_success "Docker daemon running"
# Check BuildKit support
if ! docker buildx version > /dev/null 2>&1; then
print_warning "BuildKit not available, using legacy builder"
else
print_success "BuildKit available: $(docker buildx version | head -n1)"
fi
}
# Get version tags
get_version_tags() {
print_header "GENERATING VERSION TAGS"
# Git commit hash (short)
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
print_info "Git commit: $GIT_COMMIT"
# Timestamp (YYYYMMDD_HHMMSS UTC)
TIMESTAMP=$(date -u +"%Y%m%d_%H%M%S")
print_info "Timestamp: $TIMESTAMP"
# Check if working directory is clean
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
print_warning "Working directory has uncommitted changes"
GIT_COMMIT="${GIT_COMMIT}-dirty"
fi
# Generate full image tags
TAG_LATEST="${DOCKER_REGISTRY}/${IMAGE_NAME}:latest"
TAG_COMMIT="${DOCKER_REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT}"
TAG_TIMESTAMP="${DOCKER_REGISTRY}/${IMAGE_NAME}:${TIMESTAMP}"
print_success "Tags generated:"
echo " - ${TAG_LATEST}"
echo " - ${TAG_COMMIT}"
echo " - ${TAG_TIMESTAMP}"
}
# Validate binaries in built image (EMBEDDED BINARIES ARCHITECTURE)
validate_binaries() {
local image_tag=$1
print_header "VALIDATING IMAGE"
# Check that all expected binaries are embedded in /usr/local/bin/
print_info "Checking embedded binaries in /usr/local/bin/..."
local missing_binaries=()
for binary in "${EXPECTED_BINARIES[@]}"; do
# Override entrypoint to bypass self-termination wrapper during validation
if ! docker run --rm --entrypoint="" "$image_tag" test -f "/usr/local/bin/$binary"; then
print_error "Binary not found: /usr/local/bin/$binary"
missing_binaries+=("$binary")
else
print_success "Binary exists: /usr/local/bin/$binary"
fi
done
if [ ${#missing_binaries[@]} -gt 0 ]; then
print_error "Missing binaries: ${missing_binaries[*]}"
return 1
fi
# Check CUDA libraries
print_info "Checking CUDA libraries..."
if ! docker run --rm --entrypoint="" "$image_tag" test -d /usr/local/cuda; then
print_error "CUDA directory not found in image"
return 1
fi
print_success "CUDA runtime present: /usr/local/cuda"
# Check cuDNN libraries
print_info "Checking cuDNN libraries..."
if ! docker run --rm --entrypoint="" "$image_tag" sh -c 'ldconfig -p | grep -q libcudnn'; then
print_warning "cuDNN library not found (may be OK if using base image)"
else
print_success "cuDNN library present"
fi
# Check GLIBC version (should be 2.35 for Ubuntu 22.04)
print_info "Checking GLIBC version..."
local glibc_version
glibc_version=$(docker run --rm --entrypoint="" "$image_tag" ldd --version | head -n1 | grep -oP '\d+\.\d+' | head -n1)
print_success "GLIBC version: $glibc_version"
print_success "Image validation complete"
return 0
}
# Get image size
get_image_size() {
local image_tag=$1
print_header "IMAGE SIZE REPORT"
# Get image size in bytes
local size_bytes
size_bytes=$(docker image inspect "$image_tag" --format='{{.Size}}' 2>/dev/null || echo "0")
# Get human-readable size
local size_human
size_human=$(docker image inspect "$image_tag" --format='{{.Size}}' 2>/dev/null | \
awk '{ sum=$1; hum[1024**3]="GB"; hum[1024**2]="MB"; hum[1024]="KB";
for (x=1024**3; x>=1024; x/=1024) {
if (sum>=x) { printf "%.2f %s\n", sum/x, hum[x]; break }
} }')
if [ -z "$size_human" ]; then
size_human=$(format_bytes "$size_bytes")
fi
print_info "Image size: ${size_human}"
# Show layer sizes
print_info "Layer breakdown:"
docker history --human=true --no-trunc "$image_tag" | head -n 10
}
# =============================================================================
# BUILD FUNCTIONS
# =============================================================================
# Build Docker image
build_image() {
print_header "BUILDING DOCKER IMAGE"
BUILD_START_TIME=$(date +%s)
# Prepare build command
local build_cmd="docker build"
local build_args=()
# Enable BuildKit if available
if command_exists docker-buildx || [ -n "${DOCKER_BUILDKIT:-}" ]; then
export DOCKER_BUILDKIT=1
print_info "BuildKit enabled"
fi
# Add build arguments
build_args+=("--build-arg" "GIT_COMMIT=${GIT_COMMIT}")
build_args+=("--build-arg" "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')")
build_args+=("--build-arg" "VERSION=${GIT_COMMIT}")
# Add platform if specified
if [ -n "$PLATFORM" ]; then
build_args+=("--platform" "$PLATFORM")
print_info "Building for platform: $PLATFORM"
fi
# Add tags
build_args+=("-t" "$TAG_LATEST")
build_args+=("-t" "$TAG_COMMIT")
build_args+=("-t" "$TAG_TIMESTAMP")
# Add Dockerfile
build_args+=("-f" "$DOCKERFILE")
# Add context (current directory)
build_args+=(".")
# Show build command
print_info "Build command:"
echo " DOCKER_BUILDKIT=1 ${build_cmd} ${build_args[*]}"
echo ""
# Execute build
if [ "$DRY_RUN" = true ]; then
print_warning "DRY RUN: Would execute build command above"
return 0
fi
print_info "Starting build..."
if ${build_cmd} "${build_args[@]}"; then
BUILD_END_TIME=$(date +%s)
local build_time
build_time=$(time_diff "$BUILD_START_TIME" "$BUILD_END_TIME")
print_success "Build completed in ${build_time}"
return 0
else
print_error "Build failed"
return 1
fi
}
# Push images to registry
push_images() {
print_header "PUSHING IMAGES TO REGISTRY"
if [ "$DRY_RUN" = true ]; then
print_warning "DRY RUN: Would push the following tags:"
echo " - ${TAG_LATEST}"
echo " - ${TAG_COMMIT}"
echo " - ${TAG_TIMESTAMP}"
return 0
fi
if [ "$DO_PUSH" = false ]; then
print_warning "Skipping push (--no-push flag)"
return 0
fi
# Check Docker Hub login
if ! docker info 2>/dev/null | grep -q "Username: ${DOCKER_REGISTRY}"; then
print_warning "Not logged in to Docker Hub as ${DOCKER_REGISTRY}"
print_info "Run: docker login"
print_warning "Skipping push"
return 0
fi
# Push all tags
local tags=("$TAG_LATEST" "$TAG_COMMIT" "$TAG_TIMESTAMP")
for tag in "${tags[@]}"; do
print_info "Pushing: $tag"
if docker push "$tag"; then
print_success "Pushed: $tag"
else
print_error "Failed to push: $tag"
return 1
fi
done
print_success "All images pushed successfully"
return 0
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
main() {
print_header "FOXHUNT DOCKER BUILD SCRIPT"
# Parse command line arguments
parse_args "$@"
# Check prerequisites
check_prerequisites
# Generate version tags
get_version_tags
# Show dry-run notice
if [ "$DRY_RUN" = true ]; then
print_warning "DRY RUN MODE - No actual changes will be made"
echo ""
fi
# Build image
if ! build_image; then
print_error "Build failed"
exit 1
fi
# Skip validation and reporting in dry-run mode
if [ "$DRY_RUN" = true ]; then
print_success "Dry-run completed successfully"
exit 0
fi
# Validate binaries (unless skipped)
if [ "$SKIP_VALIDATION" = false ]; then
if ! validate_binaries "$TAG_LATEST"; then
print_error "Validation failed"
exit 2
fi
else
print_warning "Skipping validation (--skip-validation flag)"
fi
# Report image size
get_image_size "$TAG_LATEST"
# Push images
if ! push_images; then
print_error "Push failed"
exit 3
fi
# Final summary
print_header "BUILD SUMMARY"
print_success "Build completed successfully"
echo ""
echo "Image tags:"
echo " - ${TAG_LATEST}"
echo " - ${TAG_COMMIT}"
echo " - ${TAG_TIMESTAMP}"
echo ""
if [ "$DO_PUSH" = true ] && docker info 2>/dev/null | grep -q "Username: ${DOCKER_REGISTRY}"; then
echo "Images pushed to Docker Hub: https://hub.docker.com/r/${DOCKER_REGISTRY}/${IMAGE_NAME}"
else
echo "Images built locally (not pushed)"
fi
if [ -n "$BUILD_START_TIME" ] && [ -n "$BUILD_END_TIME" ]; then
local total_time
total_time=$(time_diff "$BUILD_START_TIME" "$BUILD_END_TIME")
echo "Total build time: ${total_time}"
fi
echo ""
print_success "Done!"
exit 0
}
# Run main function
main "$@"