diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..55da80f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,42 @@
+# Build directories
+.build
+.build-deps
+build/
+.venv/
+.vcpkg/
+vcpkg_installed/
+
+# Git
+.git/
+.gitignore
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Docs
+*.md
+
+# CI
+.github/
+
+# Logs
+*.log
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Temporary
+*.tmp
+tmp/
+
+# Python
+__pycache__/
+*.pyc
+
+# Docker
+docker-compose*.yml
+
diff --git a/.github/CI_SETUP.md b/.github/CI_SETUP.md
new file mode 100644
index 0000000..75bf95d
--- /dev/null
+++ b/.github/CI_SETUP.md
@@ -0,0 +1,295 @@
+# CI/CD Setup Complete ✅
+
+GitHub Actions workflow has been configured for automated Docker multi-arch builds.
+
+## What was created
+
+### 1. GitHub Actions Workflow
+**File:** `.github/workflows/docker-build.yml`
+
+Multi-stage workflow with:
+- **setup_matrix** - determines build configuration
+- **build_dependencies** - builds vcpkg dependencies (only when needed)
+- **build** - parallel native builds on ARM64/AMD64 runners
+- **create_manifest** - creates unified multi-arch Docker manifest
+
+### 2. Matrix Action
+**File:** `.github/actions/docker-matrix/action.yml`
+
+Generates build matrix dynamically based on selected architectures.
+
+### 3. Documentation
+**Files:**
+- `.github/workflows/README.md` - Complete CI/CD guide
+- `README.md` - Updated with CI/CD section
+
+## Triggers
+
+| Event | Action |
+|-------|--------|
+| Push to `ci/docker` branch | Auto-build + push to Docker Hub |
+| Push git tag | Auto-build + push with tag (+ latest) |
+| Pull request | Build only (validation) |
+| Manual (UI) | Full control via GitHub Actions UI |
+
+## Manual Workflow Parameters
+
+Run workflow manually via GitHub UI with these options:
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `build_amd64` | boolean | `true` | Build for AMD64 |
+| `build_arm64` | boolean | `true` | Build for ARM64 |
+| `build_dependencies` | boolean | `false` | Rebuild vcpkg dependencies |
+| `push_to_registry` | boolean | `false` | Push to Docker Hub |
+| `docker_push_tag` | string | `""` | Custom tag (e.g., `v1.0.0`) |
+| `docker_push_latest` | boolean | `false` | Also push as `latest` |
+| `docker_deps_tag` | string | `"latest"` | Dependencies tag |
+
+## Required Secrets
+
+Set these in GitHub repository settings (**Settings** → **Secrets** → **Actions**):
+
+| Secret | Description |
+|--------|-------------|
+| `DOCKER_USERNAME` | Docker Hub username (from example CI: already set) |
+| `DOCKER_TOKEN` | Docker Hub access token (from example CI: already set) |
+
+## Runner Configuration
+
+The workflow uses **free GitHub-hosted runners** with native ARM64 support:
+
+| Architecture | Runner Label | Type | Build Speed |
+|--------------|--------------|------|-------------|
+| AMD64 | `ubuntu-24.04` | GitHub-hosted (free, native) | ~20-30 min |
+| ARM64 | `ubuntu-24.04-arm` | GitHub-hosted (free, native) | ~20-30 min |
+
+**Benefits:**
+- ✅ **100% Free** - No cost for public repositories
+- ✅ **Native builds** - Full speed on both architectures (no QEMU)
+- ✅ **No setup** - Works out of the box
+- ✅ **Parallel builds** - ARM64 and AMD64 build simultaneously
+- ✅ **Multi-arch support** - One tag works on both architectures
+- ✅ **Fast** - Native compilation is 2-3x faster than QEMU emulation
+
+**Note:** This is the recommended setup for all public repositories. For private repositories, ARM64 minutes are billed, but AMD64 is still free.
+
+## Example Usage
+
+### Scenario 1: Test build locally (no push)
+1. Go to **Actions** → **Docker Build** → **Run workflow**
+2. Settings:
+ - Build linux/amd64: ✅
+ - Build linux/arm64: ✅
+ - Push to Docker Hub: ❌
+
+### Scenario 2: Release v1.0.0
+1. Create git tag:
+ ```bash
+ git tag v1.0.0
+ git push origin v1.0.0
+ ```
+2. Workflow automatically:
+ - Builds ARM64 + AMD64
+ - Pushes images:
+ - `qdrvm/qlean-mini:608f5cc` (commit)
+ - `qdrvm/qlean-mini:v1.0.0` (tag)
+ - `qdrvm/qlean-mini:latest`
+
+### Scenario 3: Rebuild dependencies (vcpkg.json changed)
+1. Go to **Actions** → **Docker Build** → **Run workflow**
+2. Settings:
+ - Build dependencies image: ✅
+ - Push to Docker Hub: ✅
+ - Dependencies image tag: `latest`
+3. This updates `qdrvm/qlean-mini-dependencies:latest`
+
+### Scenario 4: Staging deployment
+1. Commit changes to `ci/docker` branch
+2. Push: `git push origin ci/docker`
+3. Workflow automatically builds and pushes:
+ - `qdrvm/qlean-mini:608f5cc`
+
+Or manually with custom tag:
+1. Go to **Actions** → **Docker Build** → **Run workflow**
+2. Settings:
+ - Push to Docker Hub: ✅
+ - Push additional custom tag: `staging`
+3. Result: `qdrvm/qlean-mini:608f5cc` + `qdrvm/qlean-mini:staging`
+
+## Workflow Process
+
+**Automatic on push to `ci/docker`:**
+
+```
+┌─────────────────┐
+│ Push to branch │
+└────────┬────────┘
+ │
+ ▼
+┌─────────────────────────────┐
+│ setup_matrix │
+│ - Detect vcpkg.json changes │
+│ - Generate build matrix │
+└────────┬────────────────────┘
+ │
+ ├─────────────────────────────┐
+ │ │
+ ▼ ▼
+┌────────────────────┐ ┌────────────────────┐
+│ build (ARM64) │ │ build (AMD64) │
+│ - Pull deps │ │ - Pull deps │
+│ - Build builder │ │ - Build builder │
+│ - Build runtime │ │ - Build runtime │
+│ - Push -arm64 │ │ - Push -amd64 │
+└────────┬───────────┘ └────────┬───────────┘
+ │ │
+ └───────────┬───────────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ create_manifest │
+ │ - Create multi-arch │
+ │ - Push unified tags │
+ └─────────────────────┘
+```
+
+**If vcpkg.json changed:**
+
+```
+┌─────────────────┐
+│ Detect changes │
+└────────┬────────┘
+ │
+ ├─────────────────────────────┐
+ │ │
+ ▼ ▼
+┌────────────────────┐ ┌────────────────────┐
+│ build_dependencies │ │ build_dependencies │
+│ (ARM64) │ │ (AMD64) │
+│ - Build deps │ │ - Build deps │
+│ - Push -arm64 │ │ - Push -amd64 │
+└────────┬───────────┘ └────────┬───────────┘
+ │ │
+ └───────────┬───────────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ deps manifest │
+ │ - Create multi-arch │
+ └─────────────────────┘
+ │
+ ▼
+ (continue with build stage)
+```
+
+## Image Tags
+
+**Always pushed (commit hash):**
+- `qdrvm/qlean-mini:608f5cc`
+- `qdrvm/qlean-mini-builder:608f5cc`
+
+**Optional (custom tag):**
+- `qdrvm/qlean-mini:v1.0.0` (if tag created)
+- `qdrvm/qlean-mini:staging` (if manually specified)
+
+**Optional (latest):**
+- `qdrvm/qlean-mini:latest` (for releases)
+
+**Dependencies:**
+- `qdrvm/qlean-mini-dependencies:latest` (default)
+- `qdrvm/qlean-mini-dependencies:v1` (custom)
+
+## Migrating to Production
+
+Currently configured for `ci/docker` branch (testing).
+
+**To enable for `master`:**
+
+1. Edit `.github/workflows/docker-build.yml`
+2. Change:
+ ```yaml
+ on:
+ push:
+ branches:
+ - ci/docker # Change to: master
+ ```
+3. Update PR trigger:
+ ```yaml
+ pull_request:
+ branches:
+ - ci/docker # Change to: master
+ ```
+4. Commit and push to master
+5. Delete `ci/docker` branch after validation
+
+## Verification
+
+After first workflow run, verify:
+
+```bash
+# Check multi-arch manifest
+docker manifest inspect qdrvm/qlean-mini:608f5cc
+
+# Output should show:
+# - linux/arm64
+# - linux/amd64
+
+# Pull and run
+docker pull qdrvm/qlean-mini:608f5cc
+docker run --rm qdrvm/qlean-mini:608f5cc --help
+```
+
+## Benefits vs Manual Builds
+
+| Feature | Manual | CI/CD |
+|---------|--------|-------|
+| Build time | ~50 min (single arch) | ~20-30 min (parallel native) |
+| Consistency | Depends on dev | Always reproducible |
+| Tag management | Manual | Automatic |
+| Multi-arch | Complex setup | Built-in |
+| Dependency caching | Manual | Automatic detection |
+| Team collaboration | Requires coordination | Automatic |
+| Cost | Local resources | 100% free (public repos) |
+
+## Troubleshooting
+
+**Q: Workflow fails with "Dependencies image not found"**
+
+A: Run workflow with "Build dependencies image" = ✅ first time
+
+**Q: ARM64 runner not available**
+
+A: Check runner status in **Settings** → **Actions** → **Runners**
+
+**Q: Secrets not found**
+
+A: Verify `DOCKER_USERNAME` and `DOCKER_TOKEN` in repository secrets
+
+**Q: Build timeout**
+
+A: Default is 180 min. Increase in workflow if needed:
+```yaml
+timeout-minutes: 240
+```
+
+## Next Steps
+
+1. ✅ **Verify secrets** - Check Docker Hub credentials are set
+2. ✅ **Test workflow** - Run manual build via UI
+3. ✅ **Push to ci/docker** - Test automatic build
+4. ✅ **Create test tag** - Verify tag workflow
+5. ✅ **Migrate to master** - After validation
+
+## Support
+
+- **Workflow docs:** `.github/workflows/README.md`
+- **Docker build docs:** `DOCKER_BUILD.md`
+- **Example CI:** `.ci/example-ci/` (reference)
+
+---
+
+**Status:** ✅ CI/CD infrastructure ready for use
+
+**Next action:** Test workflow by pushing to `ci/docker` branch or running manually via GitHub UI
+
diff --git a/.github/actions/docker-matrix/action.yml b/.github/actions/docker-matrix/action.yml
new file mode 100644
index 0000000..55b6cde
--- /dev/null
+++ b/.github/actions/docker-matrix/action.yml
@@ -0,0 +1,55 @@
+name: 'Setup Docker Build Matrix'
+description: 'Dynamically configure the build matrix for Docker multi-arch builds'
+
+inputs:
+ build_amd64:
+ description: 'Build for AMD64 architecture'
+ required: false
+ default: 'true'
+ build_arm64:
+ description: 'Build for ARM64 architecture'
+ required: false
+ default: 'true'
+ amd64_runner:
+ description: 'Runner label for AMD64 architecture'
+ required: false
+ default: 'ubuntu-24.04'
+ arm64_runner:
+ description: 'Runner label for ARM64 architecture'
+ required: false
+ default: 'ubuntu-24.04-arm'
+
+outputs:
+ matrix:
+ description: 'The generated build matrix'
+ value: ${{ steps.set-matrix.outputs.matrix }}
+
+runs:
+ using: 'composite'
+ steps:
+ - id: set-matrix
+ shell: bash
+ run: |
+ # Initialize empty array for matrix
+ MATRIX_ITEMS=()
+
+ # Process AMD64
+ if [[ "${{ inputs.build_amd64 }}" == "true" ]]; then
+ MATRIX_ITEMS+=('{"platform":"linux/amd64","arch_suffix":"amd64","runs_on":"${{ inputs.amd64_runner }}"}')
+ fi
+
+ # Process ARM64
+ if [[ "${{ inputs.build_arm64 }}" == "true" ]]; then
+ MATRIX_ITEMS+=('{"platform":"linux/arm64","arch_suffix":"arm64","runs_on":"${{ inputs.arm64_runner }}"}')
+ fi
+
+ # Format the matrix JSON
+ if [[ ${#MATRIX_ITEMS[@]} -eq 0 ]]; then
+ echo "No matrix items selected! Defaulting to AMD64"
+ MATRIX_ITEMS+=('{"platform":"linux/amd64","arch_suffix":"amd64","runs_on":"${{ inputs.amd64_runner }}"}')
+ fi
+
+ MATRIX_JSON="{\"include\":[$(IFS=,; echo "${MATRIX_ITEMS[*]}")]}"
+ echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
+ echo "Generated matrix: $MATRIX_JSON"
+
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
new file mode 100644
index 0000000..ea4ea1a
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,296 @@
+# GitHub Actions CI/CD
+
+This directory contains GitHub Actions workflows for automated Docker multi-arch builds.
+
+## Workflows
+
+### `docker-build.yml` - Docker Multi-arch Build
+
+Builds Docker images for multiple CPU architectures (ARM64, AMD64) and pushes them to Docker Hub.
+
+**Triggers:**
+
+1. **Manual (workflow_dispatch)** - Full control via GitHub UI
+2. **Push to `ci/docker` branch** - Auto-build and push
+3. **Git tags** - Auto-build and push with tag
+4. **Pull requests** - Build only (no push)
+
+**Architecture:**
+
+The workflow consists of 4 stages:
+
+1. **setup_matrix** - Determines what to build based on inputs/triggers
+2. **build_dependencies** - Builds dependencies image (only when needed)
+3. **build** - Builds builder and runtime images on native runners (ARM64 + AMD64)
+4. **create_manifest** - Creates unified multi-arch manifest
+
+**Key Features:**
+
+- ✅ Native builds on ARM64 and AMD64 runners (fast, no emulation)
+- ✅ Parallel builds on different architectures
+- ✅ Smart dependency caching (rebuild only when `vcpkg.json` changes)
+- ✅ Multi-arch manifest (one tag works on both architectures)
+- ✅ Flexible tagging (commit hash + optional custom tag + optional latest)
+- ✅ Auto-push on branch/tag pushes
+
+## Manual Build via GitHub UI
+
+1. Go to **Actions** → **Docker Build** → **Run workflow**
+2. Select parameters:
+ - **Build linux/amd64** - Build for AMD64 (default: yes)
+ - **Build linux/arm64** - Build for ARM64 (default: yes)
+ - **Build dependencies image** - Rebuild vcpkg dependencies (default: no)
+ - **Push to Docker Hub** - Push images to registry (default: no)
+ - **Push additional custom tag** - Optional tag (e.g., `v1.0.0`, `staging`)
+ - **Push 'latest' tag** - Also tag as `latest` (default: no)
+ - **Dependencies image tag** - Which deps tag to use/create (default: `latest`)
+
+3. Click **Run workflow**
+
+**Example scenarios:**
+
+```yaml
+# Scenario 1: Build both architectures, don't push (testing)
+build_amd64: true
+build_arm64: true
+push_to_registry: false
+
+# Scenario 2: Build and push with version tag
+build_amd64: true
+build_arm64: true
+push_to_registry: true
+docker_push_tag: "v1.0.0"
+docker_push_latest: true # Also tag as 'latest'
+
+# Scenario 3: Rebuild dependencies (vcpkg.json changed)
+build_dependencies: true
+push_to_registry: true
+docker_deps_tag: "latest"
+
+# Scenario 4: Build only ARM64 for testing
+build_amd64: false
+build_arm64: true
+push_to_registry: false
+```
+
+## Automatic Builds
+
+**Push to `ci/docker` branch:**
+```bash
+git push origin ci/docker
+```
+- Builds both ARM64 and AMD64
+- Automatically pushes to Docker Hub
+- Tags: `qlean-mini:608f5cc` (commit hash)
+- If `vcpkg.json` changed → rebuilds dependencies
+
+**Create git tag:**
+```bash
+git tag v1.0.0
+git push origin v1.0.0
+```
+- Same as branch push
+- Tags: `qlean-mini:608f5cc` + `qlean-mini:v1.0.0` + `qlean-mini:latest`
+
+**Pull request:**
+```bash
+git push origin feature-branch
+# Create PR to ci/docker
+```
+- Builds images but doesn't push
+- Validates that the build works
+
+## Runners
+
+The workflow uses **GitHub-hosted free runners** with native ARM64 support:
+
+- **AMD64**: `ubuntu-24.04` (native x64)
+- **ARM64**: `ubuntu-24.04-arm` (native ARM64)
+
+These are configured in `.github/actions/docker-matrix/action.yml`.
+
+**Benefits:**
+- ✅ **Free** - Both AMD64 and ARM64 runners are free for public repositories
+- ✅ **Native builds** - No QEMU emulation, full speed on both architectures
+- ✅ **Parallel** - Builds run simultaneously on separate runners
+- ✅ **Fast** - Each architecture builds in ~20-30 minutes natively
+
+**Note:** For private repositories, consider costs or use self-hosted runners.
+
+## Secrets
+
+Required secrets in GitHub repository settings:
+
+- `DOCKER_USERNAME` - Docker Hub username
+- `DOCKER_TOKEN` - Docker Hub access token
+
+**Setting up secrets:**
+
+1. Go to **Settings** → **Secrets and variables** → **Actions**
+2. Click **New repository secret**
+3. Add both secrets
+
+## Dependencies Image
+
+The dependencies image (`qlean-mini-dependencies:latest`) contains:
+- vcpkg packages
+- Python venv
+- Rust toolchain
+- System dependencies
+
+**When to rebuild:**
+
+- ❌ **Don't rebuild** for normal code changes
+- ✅ **Rebuild** when `vcpkg.json` changes
+- ✅ **Rebuild** when system dependencies change (`.ci/scripts/init.sh`)
+
+The workflow automatically detects `vcpkg.json` changes on push events.
+
+For manual builds, check **Build dependencies image** in the UI.
+
+## Image Naming
+
+**Dependencies:**
+```
+qdrvm/qlean-mini-dependencies:latest
+qdrvm/qlean-mini-dependencies:v1 (custom tag)
+```
+
+**Builder:**
+```
+qdrvm/qlean-mini-builder:608f5cc (commit hash, always)
+qdrvm/qlean-mini-builder:v1.0.0 (custom tag, optional)
+```
+
+**Runtime:**
+```
+qdrvm/qlean-mini:608f5cc (commit hash, always)
+qdrvm/qlean-mini:v1.0.0 (custom tag, optional)
+qdrvm/qlean-mini:latest (latest tag, optional)
+```
+
+## Workflow Process
+
+**1. Setup Matrix**
+```yaml
+outputs:
+ matrix: {"include":[{"platform":"linux/amd64","arch_suffix":"amd64","runs_on":"actions-runner-controller"},{"platform":"linux/arm64","arch_suffix":"arm64","runs_on":["self-hosted","qdrvm-arm64"]}]}
+ should_build_deps: false
+ should_push: true
+```
+
+**2. Build Dependencies (if needed)**
+```bash
+# Job 1: ARM64 runner
+DOCKER_PLATFORM=linux/arm64 make docker_build_dependencies
+make docker_push_platform_dependencies # Push with -arm64 suffix
+
+# Job 2: AMD64 runner (parallel)
+DOCKER_PLATFORM=linux/amd64 make docker_build_dependencies
+make docker_push_platform_dependencies # Push with -amd64 suffix
+```
+
+**3. Build Application**
+```bash
+# Job 1: ARM64 runner
+make docker_pull_dependencies # Pull deps from registry
+DOCKER_PLATFORM=linux/arm64 make docker_build_builder
+DOCKER_PLATFORM=linux/arm64 make docker_build_runtime
+make docker_push_platform # Push with -arm64 suffix
+
+# Job 2: AMD64 runner (parallel)
+make docker_pull_dependencies
+DOCKER_PLATFORM=linux/amd64 make docker_build_builder
+DOCKER_PLATFORM=linux/amd64 make docker_build_runtime
+make docker_push_platform # Push with -amd64 suffix
+```
+
+**4. Create Manifest**
+```bash
+# On any runner (no build, no pull)
+make docker_manifest_dependencies # If deps were built
+make docker_manifest_create # Builder + runtime
+```
+
+**Result:** Single tag works on both architectures:
+```bash
+docker pull qdrvm/qlean-mini:608f5cc
+# Docker automatically pulls correct architecture (ARM64 or AMD64)
+```
+
+## Troubleshooting
+
+**Error: "Dependencies image not found in registry"**
+
+Solution:
+```bash
+# Option 1: Run workflow with "Build dependencies image" = true
+# Option 2: Build and push manually
+make docker_build_dependencies DOCKER_PLATFORM=linux/arm64
+make docker_push_platform_dependencies
+
+make docker_build_dependencies DOCKER_PLATFORM=linux/amd64
+make docker_push_platform_dependencies
+
+make docker_manifest_dependencies
+```
+
+**Error: "Runner not found"**
+
+The workflow should work on all GitHub repositories with free ubuntu-24.04 runners. If you see this error, check that GitHub Actions are enabled for your repository.
+
+**Build timeout**
+
+Default timeout is 180 minutes (3 hours). If builds take longer, increase in workflow:
+```yaml
+timeout-minutes: 240 # 4 hours
+```
+
+## Local Development
+
+The workflow uses standard Makefile commands, so you can test locally:
+
+```bash
+# Build locally
+export DOCKER_PLATFORM=linux/arm64
+make docker_build_all
+
+# Push to test
+export DOCKER_REGISTRY=qdrvm
+export DOCKER_PUSH_TAG=true
+export DOCKER_IMAGE_TAG=test-build
+make docker_push
+```
+
+## CI/CD Best Practices
+
+1. **Use commit tags** - Always pushed, provides full traceability
+2. **Use custom tags for releases** - `v1.0.0`, `staging`, etc.
+3. **Use `latest` sparingly** - Only for stable releases
+4. **Rebuild dependencies rarely** - Only when `vcpkg.json` changes
+5. **Test PRs before merging** - Auto-builds verify changes work
+
+## Migration Plan
+
+Current configuration builds on `ci/docker` branch for testing.
+
+**After validation:**
+
+1. Update workflow triggers to `master`:
+ ```yaml
+ on:
+ push:
+ branches:
+ - master # Change from ci/docker
+ ```
+
+2. Update PR target:
+ ```yaml
+ pull_request:
+ branches:
+ - master
+ ```
+
+3. Push workflows to master
+4. Delete `ci/docker` branch
+
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..565b816
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,312 @@
+#
+# Qlean-mini Docker Multi-arch Build
+#
+# This workflow builds Docker images for multiple architectures (ARM64, AMD64)
+# and creates a unified multi-arch manifest for easy deployment.
+#
+
+name: "Docker Build"
+
+on:
+ push:
+ branches:
+ - ci/docker # TODO: Add master after testing
+ tags:
+ - '*'
+ pull_request:
+ branches:
+ - ci/docker # TODO: Add master after testing
+
+ workflow_dispatch:
+ inputs:
+ build_amd64:
+ description: "Build linux/amd64"
+ type: boolean
+ required: false
+ default: true
+ build_arm64:
+ description: "Build linux/arm64"
+ type: boolean
+ required: false
+ default: true
+ build_dependencies:
+ description: "Build dependencies image (only when vcpkg.json changes)"
+ type: boolean
+ required: false
+ default: false
+ push_to_registry:
+ description: "Push images to Docker Hub"
+ type: boolean
+ required: false
+ default: false
+ docker_push_tag:
+ description: "Push additional custom tag (e.g., v1.0.0, staging)"
+ type: string
+ required: false
+ default: ""
+ docker_push_latest:
+ description: "Push 'latest' tag"
+ type: boolean
+ required: false
+ default: false
+ docker_deps_tag:
+ description: "Dependencies image tag to use/create"
+ type: string
+ required: false
+ default: "latest"
+
+env:
+ DOCKER_REGISTRY: qdrvm
+ DOCKER_IMAGE_NAME: qlean-mini
+ DOCKER_DEPS_TAG: ${{ inputs.docker_deps_tag || 'latest' }}
+ DOCKER_PUSH_TAG: ${{ inputs.docker_push_tag != '' && 'true' || 'false' }}
+ DOCKER_IMAGE_TAG: ${{ inputs.docker_push_tag || 'localBuild' }}
+ DOCKER_PUSH_LATEST: ${{ inputs.push_to_registry && inputs.docker_push_latest && 'true' || 'false' }}
+ GIT_COMMIT: ${{ github.sha }}
+
+ # Auto-push configuration based on event type
+ AUTO_PUSH: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/ci/docker' || startsWith(github.ref, 'refs/tags/')) }}
+ IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
+
+jobs:
+ setup_matrix:
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.matrix.outputs.matrix }}
+ should_build_deps: ${{ steps.config.outputs.should_build_deps }}
+ should_push: ${{ steps.config.outputs.should_push }}
+ steps:
+ - name: "Checkout repository"
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: "Determine build configuration"
+ id: config
+ run: |
+ # Determine if we should build dependencies
+ BUILD_DEPS="false"
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ BUILD_DEPS="${{ inputs.build_dependencies }}"
+ fi
+
+ # For push events, check if vcpkg.json changed
+ if [[ "${{ github.event_name }}" == "push" ]] && [[ -n "${{ github.event.before }}" ]]; then
+ if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "vcpkg.json"; then
+ echo "vcpkg.json changed, will build dependencies"
+ BUILD_DEPS="true"
+ fi
+ fi
+
+ echo "should_build_deps=$BUILD_DEPS" >> $GITHUB_OUTPUT
+
+ # Determine if we should push
+ SHOULD_PUSH="${{ env.AUTO_PUSH }}"
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ SHOULD_PUSH="${{ inputs.push_to_registry }}"
+ fi
+ echo "should_push=$SHOULD_PUSH" >> $GITHUB_OUTPUT
+
+ echo "Configuration:"
+ echo " Build dependencies: $BUILD_DEPS"
+ echo " Push to registry: $SHOULD_PUSH"
+ echo " Event: ${{ github.event_name }}"
+
+ - name: "Generate build matrix"
+ id: matrix
+ uses: ./.github/actions/docker-matrix
+ with:
+ build_amd64: ${{ inputs.build_amd64 || 'true' }}
+ build_arm64: ${{ inputs.build_arm64 || 'true' }}
+ amd64_runner: ubuntu-24.04
+ arm64_runner: ubuntu-24.04-arm
+
+ build_dependencies:
+ needs: setup_matrix
+ if: needs.setup_matrix.outputs.should_build_deps == 'true'
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(needs.setup_matrix.outputs.matrix) }}
+
+ runs-on: ${{ matrix.runs_on }}
+ timeout-minutes: 180
+
+ env:
+ DOCKER_PLATFORM: ${{ matrix.platform }}
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: "Set up Docker Buildx"
+ uses: docker/setup-buildx-action@v3
+
+ - name: "Build dependencies image"
+ run: |
+ echo "Building dependencies for ${{ matrix.platform }}"
+ make docker_build_dependencies
+
+ - name: "Login to Docker Hub"
+ if: needs.setup_matrix.outputs.should_push == 'true'
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: "Push platform-specific dependencies"
+ if: needs.setup_matrix.outputs.should_push == 'true'
+ run: |
+ make docker_push_platform_dependencies
+
+ build:
+ needs: [setup_matrix, build_dependencies]
+ if: always() && needs.setup_matrix.result == 'success' && (needs.build_dependencies.result == 'success' || needs.build_dependencies.result == 'skipped')
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(needs.setup_matrix.outputs.matrix) }}
+
+ runs-on: ${{ matrix.runs_on }}
+ timeout-minutes: 180
+
+ env:
+ DOCKER_PLATFORM: ${{ matrix.platform }}
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: "Set up Docker Buildx"
+ uses: docker/setup-buildx-action@v3
+
+ - name: "Login to Docker Hub (for pulling dependencies)"
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: "Pull dependencies image"
+ run: |
+ if docker manifest inspect ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-dependencies:${DOCKER_DEPS_TAG} > /dev/null 2>&1; then
+ echo "Pulling dependencies from registry..."
+ make docker_pull_dependencies
+ else
+ echo "ERROR: Dependencies image not found in registry!"
+ echo "Image: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-dependencies:${DOCKER_DEPS_TAG}"
+ echo ""
+ echo "To build dependencies, either:"
+ echo " 1. Run workflow with 'Build dependencies image' = true"
+ echo " 2. Build and push manually: make docker_build_dependencies && make docker_push_platform_dependencies"
+ exit 1
+ fi
+
+ - name: "Build builder and runtime images"
+ run: |
+ echo "Building for ${{ matrix.platform }}"
+ make docker_build_builder
+ make docker_build_runtime
+
+ - name: "Verify runtime image"
+ run: |
+ make docker_verify
+
+ - name: "Login to Docker Hub (for pushing)"
+ if: needs.setup_matrix.outputs.should_push == 'true'
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: "Push platform-specific images"
+ if: needs.setup_matrix.outputs.should_push == 'true'
+ run: |
+ make docker_push_platform
+
+ create_manifest:
+ needs: [setup_matrix, build]
+ if: needs.setup_matrix.outputs.should_push == 'true' && needs.build.result == 'success'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: "Checkout repository"
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: "Login to Docker Hub"
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: "Create multi-arch manifests"
+ run: |
+ echo "Creating multi-arch manifests..."
+
+ # Only create dependencies manifest if we built it
+ if [[ "${{ needs.setup_matrix.outputs.should_build_deps }}" == "true" ]]; then
+ echo "Creating dependencies manifest..."
+ make docker_manifest_dependencies
+ fi
+
+ echo "Creating builder and runtime manifests..."
+ make docker_manifest_create
+
+ - name: "Verify manifests"
+ run: |
+ echo "=== Verifying multi-arch manifests ==="
+
+ SHORT_COMMIT=$(git rev-parse --short HEAD)
+
+ echo ""
+ echo "Builder manifest (${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-builder:${SHORT_COMMIT}):"
+ docker manifest inspect ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-builder:${SHORT_COMMIT} | grep -A 3 "Platform"
+
+ echo ""
+ echo "Runtime manifest (${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${SHORT_COMMIT}):"
+ docker manifest inspect ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${SHORT_COMMIT} | grep -A 3 "Platform"
+
+ if [[ "${{ needs.setup_matrix.outputs.should_build_deps }}" == "true" ]]; then
+ echo ""
+ echo "Dependencies manifest (${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-dependencies:${DOCKER_DEPS_TAG}):"
+ docker manifest inspect ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-dependencies:${DOCKER_DEPS_TAG} | grep -A 3 "Platform"
+ fi
+
+ echo ""
+ echo "✅ All manifests created successfully!"
+
+ - name: "Display final image tags"
+ run: |
+ SHORT_COMMIT=$(git rev-parse --short HEAD)
+
+ echo "=== Successfully pushed Docker images ==="
+ echo ""
+ echo "🐳 Runtime image:"
+ echo " ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${SHORT_COMMIT}"
+
+ if [[ "${DOCKER_PUSH_TAG}" == "true" ]]; then
+ echo " ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
+ fi
+
+ if [[ "${DOCKER_PUSH_LATEST}" == "true" ]]; then
+ echo " ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:latest"
+ fi
+
+ echo ""
+ echo "🔧 Builder image:"
+ echo " ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-builder:${SHORT_COMMIT}"
+
+ if [[ "${{ needs.setup_matrix.outputs.should_build_deps }}" == "true" ]]; then
+ echo ""
+ echo "📦 Dependencies image:"
+ echo " ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-dependencies:${DOCKER_DEPS_TAG}"
+ fi
+
+ echo ""
+ echo "Pull with: docker pull ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${SHORT_COMMIT}"
+ echo "Run with: docker run --rm ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${SHORT_COMMIT} --help"
+
diff --git a/DOCKER_BUILD.md b/DOCKER_BUILD.md
new file mode 100644
index 0000000..9f04332
--- /dev/null
+++ b/DOCKER_BUILD.md
@@ -0,0 +1,665 @@
+# Docker Build - Three-Stage Strategy
+
+## Overview
+
+Three-stage build separates dependencies from project code for faster development:
+
+```
+Stage 1: Dependencies (vcpkg libs) → ~20 min, rebuild rarely
+Stage 2: Builder (project code) → ~3-5 min, rebuild often
+Stage 3: Runtime (minimal image) → ~1 min, automatic
+```
+
+## Quick Start
+
+### First Time (Full Build)
+
+```bash
+make docker_build_all
+# Builds: dependencies → builder → runtime (~25 min)
+```
+
+### Daily Development (Fast Rebuild)
+
+```bash
+# After code changes
+make docker_build
+# Builds: builder → runtime using cached dependencies (~4 min)
+```
+
+### CI/CD Optimized
+
+```bash
+make docker_build_ci
+# Pull deps from registry + build code (~4 min)
+```
+
+## All Commands
+
+### Build
+
+```bash
+make docker_build_dependencies # Stage 1: vcpkg libs (~20 min)
+make docker_build_builder # Stage 2: project code (~3-5 min)
+make docker_build_runtime # Stage 3: final image (~1 min)
+make docker_build # Fast: builder + runtime (~4 min)
+make docker_build_all # Full: all 3 stages (~25 min)
+make docker_build_ci # CI/CD optimized (~4 min)
+```
+
+**Platform selection:**
+```bash
+# ARM64 (default)
+make docker_build_all
+
+# AMD64 / x86_64
+make docker_build_all DOCKER_PLATFORM=linux/amd64
+
+# Build for specific architecture
+DOCKER_PLATFORM=linux/amd64 make docker_build_dependencies
+DOCKER_PLATFORM=linux/amd64 make docker_build
+
+# Multi-arch: see "Multi-platform Builds" section for CI/CD workflow
+```
+
+### Push to Registry
+
+**Dependencies (push once per version):**
+```bash
+# Push dependencies with default tag (latest)
+make docker_push_dependencies # Push: qlean-mini-dependencies:latest
+
+# Push dependencies with custom tag
+DOCKER_DEPS_TAG=v2 make docker_build_dependencies
+make docker_push_dependencies # Push: qlean-mini-dependencies:v2
+```
+
+**Builder & Runtime (push per commit):**
+```bash
+# Local dev (default) - only commit tag
+make docker_push # Push: qlean-mini:608f5cc
+
+# Push with custom tag (e.g., version)
+DOCKER_PUSH_TAG=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push # Push: commit + v1.0.0
+
+# Push with latest tag
+DOCKER_PUSH_LATEST=true make docker_push # Push: commit + latest
+
+# Production release - push all 3 tags
+DOCKER_PUSH_TAG=true DOCKER_PUSH_LATEST=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push
+# Push: qlean-mini:608f5cc + qlean-mini:v1.0.0 + qlean-mini:latest
+
+# Staging environment
+DOCKER_PUSH_TAG=true DOCKER_IMAGE_TAG=staging make docker_push # Push: commit + staging
+
+# Individual stages
+make docker_push_builder # Push builder only
+make docker_push_runtime # Push runtime only
+```
+
+### Pull from Registry
+
+```bash
+# Pull dependencies with default tag (latest)
+make docker_pull_dependencies # Pull: qlean-mini-dependencies:latest
+
+# Pull dependencies with custom tag
+DOCKER_DEPS_TAG=v2 make docker_pull_dependencies # Pull: qlean-mini-dependencies:v2
+```
+
+### Run & Verify
+
+```bash
+make docker_run # Run with --help
+make docker_run ARGS='--version'
+make docker_verify # Test runtime image
+
+# Run on specific platform
+make docker_run DOCKER_PLATFORM=linux/amd64 ARGS='--version'
+make docker_verify DOCKER_PLATFORM=linux/amd64
+```
+
+### Clean
+
+```bash
+make docker_clean # Remove builder + runtime (keep dependencies)
+make docker_clean_all # Remove all images (including dependencies)
+make docker_inspect # Show image info
+```
+
+## Images
+
+- `qlean-mini-dependencies:latest` (~18 GB) - vcpkg libraries, build tools
+- `qlean-mini-builder:latest` (~19 GB) - compiled project code
+- `qlean-mini:latest` (~240 MB) - **optimized** runtime image for production
+
+**Image tagging:**
+
+**Dependencies** (single version, shared across all commits):
+- Tag: `qlean-mini-dependencies:latest` (default, configurable via `DOCKER_DEPS_TAG`)
+- Example: `DOCKER_DEPS_TAG=v1 make docker_build_dependencies`
+- Changes only when `vcpkg.json`, `vcpkg-configuration.json`, or system deps change
+- Always uses the same tag across all code commits
+
+**Builder & Runtime** (per-commit versioning):
+- Every build creates **2 local tags**:
+ - **Commit tag**: `qlean-mini:608f5cc` (always, based on git commit)
+ - **Additional tag**: `qlean-mini:localBuild` (default, configurable via `DOCKER_IMAGE_TAG`)
+
+Push behavior for Builder & Runtime (**up to 3 tags** can be pushed):
+
+| Tag Type | Variable | Always Pushed? | Example |
+|----------|----------|----------------|---------|
+| **Commit** | `GIT_COMMIT` | ✅ Yes | `qlean-mini:608f5cc` |
+| **Custom** | `DOCKER_IMAGE_TAG` | Only if `DOCKER_PUSH_TAG=true` | `qlean-mini:v1.0.0` |
+| **Latest** | (hardcoded) | Only if `DOCKER_PUSH_LATEST=true` | `qlean-mini:latest` |
+
+**Examples:**
+
+| Scenario | Command | Local Tags | Pushed Tags |
+|----------|---------|------------|-------------|
+| Local dev (default) | `make docker_build` | Builder/Runtime: `608f5cc`, `localBuild`
Deps: `latest` | None (manual push) |
+| Push to registry | `make docker_push` | Builder/Runtime: `608f5cc`, `localBuild`
Deps: `latest` | `608f5cc` only |
+| Master branch | `DOCKER_PUSH_LATEST=true` | Builder/Runtime: `608f5cc`, `localBuild`
Deps: `latest` | `608f5cc`, `latest` |
+| Production release | `DOCKER_PUSH_TAG=true`
`DOCKER_PUSH_LATEST=true`
`DOCKER_IMAGE_TAG=v1.0.0` | Builder/Runtime: `608f5cc`, `v1.0.0`
Deps: `latest` | `608f5cc`, `v1.0.0`, `latest` |
+| Staging | `DOCKER_PUSH_TAG=true`
`DOCKER_IMAGE_TAG=staging` | Builder/Runtime: `608f5cc`, `staging`
Deps: `latest` | `608f5cc`, `staging` |
+| New deps version | `DOCKER_DEPS_TAG=v2`
`make docker_build_dependencies` | Deps: `v2` | Manual: `make docker_push_dependencies` |
+
+**Optimization applied:**
+- Strip debug symbols from binaries (~30-50% size reduction)
+- Copy only `.so` files from vcpkg (not static libs, headers, cmake files)
+- Result: **14x smaller** runtime image (240 MB vs 3.4 GB)
+
+## When to Rebuild
+
+**Dependencies** - rebuild when:
+- `vcpkg.json` changes
+- System dependencies update (cmake, gcc, rust versions)
+- Rarely - maybe once per month or when adding new libraries
+
+**Builder** - rebuild when:
+- Code changes in `src/`
+- `CMakeLists.txt` changes
+- Every commit (fast - only 3-5 min!)
+
+**Runtime** - automatically rebuilt after builder
+
+## Workflow Examples
+
+### Team Setup (First Time)
+
+```bash
+# Lead/DevOps builds and pushes dependencies (once)
+make docker_build_dependencies
+make docker_push_dependencies
+
+# Or push everything
+make docker_build_all
+make docker_push
+```
+
+### Developer Daily Work
+
+```bash
+# Pull dependencies (once per setup)
+make docker_pull_dependencies
+
+# Daily development cycle
+# 1. Edit code
+# 2. Rebuild (fast!)
+make docker_build # ~4 min
+# Creates: qlean-mini:abc1234 + qlean-mini:localBuild (local only)
+
+# Test
+make docker_run ARGS='--version'
+make docker_verify
+
+# Push to registry (only commit tag)
+make docker_push # Push: qlean-mini:abc1234
+```
+
+### CI/CD Pipeline
+
+```bash
+# Set registry
+export DOCKER_REGISTRY=your-registry
+
+# Build (pulls dependencies from registry)
+make docker_build_ci # ~4 min
+
+# Test
+make docker_verify
+
+# Push scenarios:
+
+# 1. Feature branch / PR - only commit tag
+make docker_push # Push: qlean-mini:608f5cc
+
+# 2. Master branch - commit + latest
+DOCKER_PUSH_LATEST=true make docker_push # Push: qlean-mini:608f5cc + latest
+
+# 3. Release tag - commit + version + latest
+DOCKER_PUSH_TAG=true DOCKER_PUSH_LATEST=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push
+# Push: qlean-mini:608f5cc + v1.0.0 + latest
+
+# 4. Staging environment - commit + staging
+DOCKER_PUSH_TAG=true DOCKER_IMAGE_TAG=staging make docker_push # Push: commit + staging
+```
+
+## CI/CD Integration
+
+### Automatic (Recommended)
+
+```bash
+# One command: pull dependencies from registry, then build
+export DOCKER_REGISTRY=your-registry
+make docker_build_ci # ~4 min (if deps cached)
+make docker_verify
+make docker_push
+```
+
+### Manual Control
+
+```bash
+# 1. Pull dependencies from registry
+export DOCKER_REGISTRY=your-registry
+make docker_pull_dependencies
+
+# 2. Build code only
+make docker_build # ~4 min
+
+# 3. Test and push
+make docker_verify
+make docker_push
+```
+
+## Tracking Dependency Changes
+
+Dependencies rebuild when these files change:
+- `vcpkg.json`
+- `vcpkg-configuration.json`
+- `vcpkg-overlay/`
+- `.ci/.env` (CMAKE_VERSION, GCC_VERSION, RUST_VERSION)
+
+### Auto-detect in CI
+
+```bash
+# Check if dependencies changed
+if git diff HEAD~1 HEAD -- vcpkg.json vcpkg-configuration.json vcpkg-overlay/ .ci/.env | grep .; then
+ make docker_build_dependencies
+ make docker_push_dependencies
+else
+ docker pull qdrvm/qlean-mini-dependencies:latest
+ docker tag qdrvm/qlean-mini-dependencies:latest qlean-mini-dependencies:latest
+fi
+make docker_build
+```
+
+## Troubleshooting
+
+**Error: dependencies image not found**
+```bash
+make docker_build_dependencies
+# or
+docker pull qdrvm/qlean-mini-dependencies:latest
+docker tag qdrvm/qlean-mini-dependencies:latest qlean-mini-dependencies:latest
+```
+
+**Changes not reflected**
+```bash
+docker builder prune -a
+make docker_build
+```
+
+**Library not found in runtime**
+```bash
+docker run --rm qlean-mini:latest ldd /usr/local/bin/qlean
+```
+
+**Check image sizes**
+```bash
+make docker_inspect
+```
+
+## CI/CD Examples
+
+### GitHub Actions
+
+```yaml
+name: Docker Build
+
+on:
+ push:
+ branches: [ master, develop ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ secrets.DOCKER_REGISTRY_URL }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Build with cached dependencies
+ run: |
+ export DOCKER_REGISTRY=${{ secrets.DOCKER_REGISTRY }}
+ make docker_build_ci
+
+ - name: Verify
+ run: make docker_verify
+
+ - name: Push (commit tag only)
+ if: github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/v')
+ run: make docker_push
+
+ - name: Push with latest tag (master)
+ if: github.ref == 'refs/heads/master'
+ run: DOCKER_PUSH_LATEST=true make docker_push
+
+ - name: Push with version and latest tags (releases)
+ if: startsWith(github.ref, 'refs/tags/v')
+ run: |
+ VERSION=${GITHUB_REF#refs/tags/}
+ DOCKER_PUSH_TAG=true DOCKER_PUSH_LATEST=true DOCKER_IMAGE_TAG=$VERSION make docker_push
+```
+
+### GitLab CI
+
+```yaml
+build:
+ stage: build
+ image: docker:24-dind
+ services:
+ - docker:24-dind
+ before_script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - apk add --no-cache make bash git
+ script:
+ - export DOCKER_REGISTRY=$CI_REGISTRY
+ - make docker_build_ci
+ - make docker_verify
+ only:
+ - master
+ - develop
+
+push_commit:
+ stage: push
+ script:
+ - make docker_push
+ only:
+ - develop
+
+push_latest:
+ stage: push
+ script:
+ - export DOCKER_PUSH_LATEST=true
+ - make docker_push
+ only:
+ - master
+
+push_version:
+ stage: push
+ script:
+ - export DOCKER_PUSH_TAG=true
+ - export DOCKER_PUSH_LATEST=true
+ - export DOCKER_IMAGE_TAG=$CI_COMMIT_TAG
+ - make docker_push
+ only:
+ - tags
+```
+
+### Key Points for CI/CD
+
+1. **Use `docker_build_ci`** - automatically pulls dependencies from registry
+2. **Set `DOCKER_REGISTRY`** environment variable
+3. **Dependencies tag** - use `DOCKER_DEPS_TAG` to specify which dependencies version to use (default: `latest`)
+4. **Dependencies only rebuild** when vcpkg.json changes
+5. **Fast builds** - ~4 min instead of 25 min
+6. **Automatic tagging** - images always tagged by git commit hash
+7. **Push up to 3 tags**:
+ - Commit tag (always): `qlean-mini:608f5cc`
+ - Custom tag (optional): set `DOCKER_PUSH_TAG=true` + `DOCKER_IMAGE_TAG=v1.0.0`
+ - Latest tag (optional): set `DOCKER_PUSH_LATEST=true`
+8. **Flexible tagging** - use any custom tag: latest, v1.0.0, staging, production, etc.
+9. **Git required** - for version detection and commit-based tagging
+10. **Optimized runtime** - 240 MB production image (stripped binaries)
+
+## Working with Dependencies Tag
+
+The dependencies image is **shared across all code commits** and only needs to be rebuilt when dependencies change.
+
+### Understanding `DOCKER_DEPS_TAG`
+
+- **Variable**: `DOCKER_DEPS_TAG` (default: `latest`)
+- **Purpose**: Specify which version of dependencies to use
+- **When to change**: After updating `vcpkg.json`, `vcpkg-configuration.json`, or system dependencies
+
+### Typical Workflow
+
+**1. Team uses default (latest):**
+```bash
+# Everyone pulls the same dependencies
+make docker_pull_dependencies # Pulls: qlean-mini-dependencies:latest
+make docker_build # Builds code using latest dependencies
+```
+
+**2. Developer updates dependencies:**
+```bash
+# Edit vcpkg.json to add new library
+vim vcpkg.json
+
+# Build new dependencies version
+DOCKER_DEPS_TAG=v2 make docker_build_dependencies
+
+# Push for team
+DOCKER_DEPS_TAG=v2 make docker_push_dependencies
+
+# Update CI/CD to use v2
+# Set DOCKER_DEPS_TAG=v2 in CI environment variables
+```
+
+**3. Using specific dependency version:**
+```bash
+# Pull specific version
+DOCKER_DEPS_TAG=v2 make docker_pull_dependencies
+
+# Build code with specific dependencies
+DOCKER_DEPS_TAG=v2 make docker_build
+
+# Or set as default in your shell
+export DOCKER_DEPS_TAG=v2
+make docker_pull_dependencies
+make docker_build
+```
+
+### Best Practices
+
+1. **Use `latest` for active development** - simplest for most developers
+2. **Version dependencies for releases** - e.g., `v1`, `v2`, `v3` when making breaking changes
+3. **Push after building** - always push dependencies to registry after rebuilding:
+ ```bash
+ DOCKER_DEPS_TAG=v2 make docker_build_dependencies
+ DOCKER_DEPS_TAG=v2 make docker_push_dependencies
+ ```
+4. **Document in team** - notify team when dependencies version changes
+5. **CI/CD pinning** - for stable builds, pin `DOCKER_DEPS_TAG` in CI config instead of using `latest`
+
+## Platform Support
+
+The build system supports multiple architectures via the `DOCKER_PLATFORM` variable.
+
+### Supported Platforms
+
+- **`linux/arm64`** (default) - ARM 64-bit (Apple Silicon, AWS Graviton, etc.)
+- **`linux/amd64`** - x86_64 / Intel/AMD 64-bit
+
+### Usage
+
+**Build for specific platform:**
+```bash
+# ARM64 (default)
+make docker_build_all
+
+# AMD64
+DOCKER_PLATFORM=linux/amd64 make docker_build_all
+
+# Only dependencies for AMD64
+DOCKER_PLATFORM=linux/amd64 make docker_build_dependencies
+```
+
+**Pull from registry:**
+```bash
+# Pull ARM64 (default)
+make docker_pull_dependencies
+
+# Pull AMD64
+DOCKER_PLATFORM=linux/amd64 make docker_pull_dependencies
+```
+
+**Run and verify:**
+```bash
+# Run on ARM64 (default)
+make docker_run ARGS='--version'
+
+# Run on AMD64
+DOCKER_PLATFORM=linux/amd64 make docker_run ARGS='--version'
+
+# Verify AMD64 image
+DOCKER_PLATFORM=linux/amd64 make docker_verify
+```
+
+### Important Notes
+
+1. **Platform consistency** - Use the same platform for all stages (dependencies, builder, runtime)
+2. **Registry separation** - Different platforms use the same image tags, stored separately by Docker
+3. **Native builds** - Build on native platform when possible for best performance
+4. **Cross-compilation** - Docker supports cross-platform builds via QEMU (slower)
+5. **CI/CD** - Set `DOCKER_PLATFORM` in CI environment variables for consistent builds
+
+### CI/CD Example
+
+```yaml
+# GitHub Actions
+- name: Build for AMD64
+ run: |
+ export DOCKER_PLATFORM=linux/amd64
+ make docker_build_ci
+ make docker_verify
+ make docker_push
+
+# GitLab CI
+variables:
+ DOCKER_PLATFORM: linux/amd64
+
+build:
+ script:
+ - make docker_build_ci
+ - make docker_verify
+```
+
+### Multi-platform Builds (Unified Manifest)
+
+**CI/CD with native runners (RECOMMENDED)**
+
+Best practice - build on native machines in parallel, then create manifest:
+
+```bash
+# === Job 1: Build on ARM64 runner ===
+export DOCKER_PLATFORM=linux/arm64
+make docker_build_all # Build on ARM64 machine
+make docker_push_platform # Push with -arm64 tag
+
+# === Job 2: Build on AMD64 runner ===
+export DOCKER_PLATFORM=linux/amd64
+make docker_build_all # Build on AMD64 machine
+make docker_push_platform # Push with -amd64 tag
+
+# === Job 3: Create manifest (any machine) ===
+make docker_manifest_create # Create unified manifest (no pull!)
+# Result: qlean-mini:608f5cc points to both architectures
+```
+
+**Verify multi-arch image:**
+
+```bash
+docker manifest inspect qdrvm/qlean-mini:608f5cc
+```
+
+**How it works:**
+
+1. **Build stage**: Creates platform-specific images locally with `-arm64` and `-amd64` suffixes
+2. **Push stage**: Pushes both platform images to registry
+3. **Manifest creation**: Creates Docker manifest that points to both images
+4. **Result**: Single tag (`qlean-mini:608f5cc`) works on both ARM64 and AMD64
+5. **Auto-selection**: Docker automatically pulls correct architecture
+
+**Commands:**
+
+| Command | Description |
+|---------|-------------|
+| `make docker_push_platform` | Push builder + runtime with arch suffix (`-arm64` / `-amd64`) |
+| `make docker_push_platform_dependencies` | Push dependencies with arch suffix |
+| `make docker_manifest_create` | Create unified manifest from pushed images (builder + runtime) |
+| `make docker_manifest_dependencies` | Create unified manifest for dependencies |
+
+**CI/CD Integration (RECOMMENDED):**
+
+```yaml
+# GitHub Actions - Build on native runners
+jobs:
+ build-arm64:
+ runs-on: ubuntu-latest-arm64 # ARM64 runner
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build ARM64
+ run: |
+ export DOCKER_PLATFORM=linux/arm64
+ make docker_build_all
+ make docker_push_platform # Push with -arm64 suffix
+
+ build-amd64:
+ runs-on: ubuntu-latest # AMD64 runner
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build AMD64
+ run: |
+ export DOCKER_PLATFORM=linux/amd64
+ make docker_build_all
+ make docker_push_platform # Push with -amd64 suffix
+
+ create-manifest:
+ needs: [build-arm64, build-amd64]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Create multi-arch manifest
+ run: make docker_manifest_create # No build, no pull!
+
+# GitLab CI - Build on native runners
+build:arm64:
+ tags: [arm64]
+ script:
+ - export DOCKER_PLATFORM=linux/arm64
+ - make docker_build_all
+ - make docker_push_platform
+
+build:amd64:
+ tags: [amd64]
+ script:
+ - export DOCKER_PLATFORM=linux/amd64
+ - make docker_build_all
+ - make docker_push_platform
+
+manifest:
+ needs: [build:arm64, build:amd64]
+ script:
+ - make docker_manifest_create
+```
diff --git a/Dockerfile.builder b/Dockerfile.builder
new file mode 100644
index 0000000..3961afc
--- /dev/null
+++ b/Dockerfile.builder
@@ -0,0 +1,83 @@
+# Stage 2: Build project code
+# Uses pre-built dependencies from Stage 1
+
+ARG DEPS_IMAGE=qlean-mini-dependencies:latest
+FROM ${DEPS_IMAGE} AS dependencies
+
+FROM ubuntu:24.04 AS builder
+
+ARG DEBIAN_FRONTEND=noninteractive
+ARG GIT_COMMIT=unknown
+ENV DEBIAN_FRONTEND=${DEBIAN_FRONTEND}
+ENV VCPKG_FORCE_SYSTEM_BINARIES=1
+ENV GIT_COMMIT=${GIT_COMMIT}
+
+ENV PROJECT=/qlean-mini
+ENV VENV=${PROJECT}/.venv
+ENV BUILD=${PROJECT}/.build
+ENV PATH=${VENV}/bin:/root/.cargo/bin:${PATH}
+ENV CARGO_HOME=/root/.cargo
+ENV RUSTUP_HOME=/root/.rustup
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ build-essential \
+ cmake \
+ ninja-build \
+ git \
+ curl \
+ ca-certificates \
+ pkg-config \
+ python3 \
+ python3-venv \
+ libstdc++6 \
+ zip \
+ unzip && \
+ rm -rf /var/lib/apt/lists/*
+
+WORKDIR ${PROJECT}
+
+# Copy dependencies from dependencies image
+COPY --from=dependencies ${VENV} ${VENV}
+COPY --from=dependencies ${PROJECT}/.vcpkg ${PROJECT}/.vcpkg
+COPY --from=dependencies ${BUILD}/vcpkg_installed ${BUILD}/vcpkg_installed
+COPY --from=dependencies /root/.cargo /root/.cargo
+COPY --from=dependencies /root/.rustup /root/.rustup
+
+# Copy project source code
+COPY . ${PROJECT}
+
+# Build project
+RUN set -eux; \
+ export PATH="${HOME}/.cargo/bin:${PATH}"; \
+ source ${HOME}/.cargo/env 2>/dev/null || true; \
+ echo "=== Checking vcpkg_installed ==="; \
+ ls -la ${BUILD}/vcpkg_installed/ || echo "No vcpkg_installed found!"; \
+ VCPKG_ROOT=${PROJECT}/.vcpkg cmake -G Ninja --preset=default \
+ -DPython3_EXECUTABLE="${VENV}/bin/python3" \
+ -DTESTING=OFF \
+ -DVCPKG_INSTALLED_DIR=${BUILD}/vcpkg_installed \
+ -DVCPKG_MANIFEST_MODE=OFF \
+ -B ${BUILD} \
+ ${PROJECT}; \
+ cmake --build ${BUILD} --parallel; \
+ mkdir -p /opt/artifacts/bin /opt/artifacts/modules /opt/artifacts/lib /opt/artifacts/vcpkg; \
+ cp -v ${BUILD}/src/executable/qlean /opt/artifacts/bin/; \
+ strip /opt/artifacts/bin/qlean; \
+ find ${BUILD}/src/modules -type f -name "*_module.so" -exec cp -v {} /opt/artifacts/modules/ \; || true; \
+ find /opt/artifacts/modules/ -name "*.so" -exec strip {} \; || true; \
+ find ${BUILD}/src -type f -name "*.so" ! -name "*_module.so" -exec cp -v {} /opt/artifacts/lib/ \; || true; \
+ find /opt/artifacts/lib/ -name "*.so" -exec strip {} \; || true; \
+ if [ -d "${BUILD}/vcpkg_installed" ]; then \
+ echo "Collecting only runtime .so libraries from vcpkg..."; \
+ find ${BUILD}/vcpkg_installed -name "*.so*" -type f -exec cp -v {} /opt/artifacts/vcpkg/ \; 2>/dev/null || true; \
+ find /opt/artifacts/vcpkg/ -name "*.so*" -exec strip {} \; 2>/dev/null || true; \
+ fi; \
+ echo "=== Artifacts ==="; \
+ ls -lh /opt/artifacts/bin/; \
+ ls -lh /opt/artifacts/modules/ || true; \
+ ls -lh /opt/artifacts/lib/ || true; \
+ echo "Vcpkg libraries: $(find /opt/artifacts/vcpkg/ -name '*.so*' | wc -l) files"
+
+CMD ["/bin/bash"]
+
diff --git a/Dockerfile.dependencies b/Dockerfile.dependencies
new file mode 100644
index 0000000..f01638b
--- /dev/null
+++ b/Dockerfile.dependencies
@@ -0,0 +1,117 @@
+# Stage 1: Build dependencies (vcpkg + system libs)
+# Rebuild only when vcpkg.json changes
+
+FROM ubuntu:24.04 AS dependencies
+
+ARG DEBIAN_FRONTEND=noninteractive
+ARG CMAKE_VERSION=3.31.1
+ARG GCC_VERSION=14
+ARG RUST_VERSION=stable
+
+ENV DEBIAN_FRONTEND=${DEBIAN_FRONTEND}
+ENV CMAKE_VERSION=${CMAKE_VERSION}
+ENV GCC_VERSION=${GCC_VERSION}
+ENV RUST_VERSION=${RUST_VERSION}
+ENV VCPKG_FORCE_SYSTEM_BINARIES=1
+
+ENV PROJECT=/qlean-mini
+ENV VENV=${PROJECT}/.venv
+ENV BUILD=${PROJECT}/.build-deps
+ENV PATH=${VENV}/bin:/root/.cargo/bin:${PATH}
+ENV CARGO_HOME=/root/.cargo
+ENV RUSTUP_HOME=/root/.rustup
+
+WORKDIR ${PROJECT}
+
+# Copy only files needed for dependency installation
+COPY .ci/ ${PROJECT}/.ci/
+COPY vcpkg.json vcpkg-configuration.json ${PROJECT}/
+COPY vcpkg-overlay/ ${PROJECT}/vcpkg-overlay/
+COPY CMakeLists.txt CMakePresets.json ${PROJECT}/
+COPY Makefile ${PROJECT}/
+
+# Create minimal src structure (stubs for CMake)
+RUN mkdir -p ${PROJECT}/src/app \
+ ${PROJECT}/src/blockchain \
+ ${PROJECT}/src/clock \
+ ${PROJECT}/src/crypto \
+ ${PROJECT}/src/executable \
+ ${PROJECT}/src/injector \
+ ${PROJECT}/src/log \
+ ${PROJECT}/src/metrics \
+ ${PROJECT}/src/modules \
+ ${PROJECT}/src/se \
+ ${PROJECT}/src/serde \
+ ${PROJECT}/src/storage \
+ ${PROJECT}/src/utils
+
+# Create stub CMakeLists.txt files
+RUN for dir in app blockchain clock crypto executable injector log metrics modules se serde storage utils; do \
+ echo "# Stub" > ${PROJECT}/src/$dir/CMakeLists.txt; \
+ done && \
+ echo "# Stub for deps stage" > ${PROJECT}/src/CMakeLists.txt
+
+# Install system dependencies
+RUN set -eux; \
+ apt-get update && \
+ apt-get install -y --no-install-recommends zip unzip && \
+ rm -rf /var/lib/apt/lists/*; \
+ chmod +x .ci/scripts/*.sh; \
+ ./.ci/scripts/init.sh; \
+ rm -rf ${VENV}; \
+ make init_py
+
+# Install vcpkg dependencies
+RUN --mount=type=cache,target=/qlean-mini/.vcpkg,id=vcpkg-deps \
+ set -eux; \
+ export PATH="${HOME}/.cargo/bin:${PATH}"; \
+ source ${HOME}/.cargo/env 2>/dev/null || true; \
+ if [ ! -f "/qlean-mini/.vcpkg/vcpkg" ]; then \
+ make init_vcpkg; \
+ fi; \
+ printf '# Stub - skip building project sources\nmessage(STATUS "Dependencies stage: skipping project sources")\n' > ${PROJECT}/src/CMakeLists.txt; \
+ VCPKG_ROOT=${PROJECT}/.vcpkg cmake -G Ninja --preset=default \
+ -DPython3_EXECUTABLE="${VENV}/bin/python3" \
+ -DTESTING=OFF \
+ -B ${BUILD} \
+ ${PROJECT}; \
+ mkdir -p /opt/dependencies/vcpkg; \
+ if [ -d "${BUILD}/vcpkg_installed" ]; then \
+ cp -R ${BUILD}/vcpkg_installed /opt/dependencies/vcpkg/; \
+ fi; \
+ cp -R ${VENV} /opt/dependencies/venv; \
+ cp -R ${PROJECT}/.vcpkg /opt/dependencies/vcpkg-install
+
+# Final dependencies image
+FROM ubuntu:24.04 AS dependencies-final
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ build-essential \
+ cmake \
+ ninja-build \
+ git \
+ curl \
+ ca-certificates \
+ pkg-config \
+ libstdc++6 \
+ zip \
+ unzip && \
+ rm -rf /var/lib/apt/lists/*
+
+ENV PROJECT=/qlean-mini
+ENV VENV=${PROJECT}/.venv
+ENV BUILD=${PROJECT}/.build
+ENV PATH=${VENV}/bin:/root/.cargo/bin:${PATH}
+ENV CARGO_HOME=/root/.cargo
+ENV RUSTUP_HOME=/root/.rustup
+
+COPY --from=dependencies /opt/dependencies/venv ${VENV}
+COPY --from=dependencies /opt/dependencies/vcpkg-install ${PROJECT}/.vcpkg
+COPY --from=dependencies /opt/dependencies/vcpkg/vcpkg_installed ${BUILD}/vcpkg_installed
+COPY --from=dependencies /root/.cargo /root/.cargo
+COPY --from=dependencies /root/.rustup /root/.rustup
+
+WORKDIR ${PROJECT}
+
+CMD ["/bin/bash"]
diff --git a/Dockerfile.runtime b/Dockerfile.runtime
index bd3ecda..eccbe52 100644
--- a/Dockerfile.runtime
+++ b/Dockerfile.runtime
@@ -1,43 +1,38 @@
-# Use existing builder image
-FROM qlean-mini:latest-builder AS builder
+# Stage 3: Minimal runtime image
+# Production-ready image with binaries only
+
+ARG BUILDER_IMAGE=qlean-mini-builder:latest
+FROM ${BUILDER_IMAGE} AS builder
-# ==================== Stage 2: Runtime ====================
FROM ubuntu:24.04 AS runtime
-# Install minimal runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libstdc++6 \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
-# Environment variables for runtime
-ENV LD_LIBRARY_PATH=/opt/qlean/lib:/opt/qlean/vcpkg/installed/x64-linux/lib:/opt/qlean/vcpkg/installed/x64-linux-dynamic/lib:/opt/qlean/vcpkg/installed/lib:/usr/local/lib
+ENV LD_LIBRARY_PATH=/opt/qlean/lib:/opt/qlean/vcpkg:/usr/local/lib
ENV QLEAN_MODULES_DIR=/opt/qlean/modules
WORKDIR /work
-# Copy binary
-COPY --from=builder /qlean-mini/.build/src/executable/qlean /usr/local/bin/qlean
-# Copy all project .so libraries
-COPY --from=builder /qlean-mini/.build/src/app/*.so /opt/qlean/lib/
-COPY --from=builder /qlean-mini/.build/src/utils/*.so /opt/qlean/lib/
-# Copy modules
-COPY --from=builder /qlean-mini/.build/src/modules/example/libexample_module.so /opt/qlean/modules/
-COPY --from=builder /qlean-mini/.build/src/modules/networking/libnetworking_module.so /opt/qlean/modules/
-COPY --from=builder /qlean-mini/.build/src/modules/production/libproduction_module.so /opt/qlean/modules/
-COPY --from=builder /qlean-mini/.build/src/modules/synchronizer/libsynchronizer_module.so /opt/qlean/modules/
-# Copy vcpkg libraries
-COPY --from=builder /qlean-mini/.build/vcpkg_installed/ /opt/qlean/vcpkg/installed/
-
-# Verify artifacts
+# Copy artifacts from builder (binaries are already stripped)
+COPY --from=builder /opt/artifacts/bin/qlean /usr/local/bin/qlean
+COPY --from=builder /opt/artifacts/lib/ /opt/qlean/lib/
+COPY --from=builder /opt/artifacts/modules/ /opt/qlean/modules/
+COPY --from=builder /opt/artifacts/vcpkg/ /opt/qlean/vcpkg/
+
+# Verify runtime image
RUN echo "=== Runtime image contents ===" && \
- ls -lh /usr/local/bin/qlean && \
- echo "=== Project libraries ===" && \
- ls -lh /opt/qlean/lib/ || true && \
- echo "=== Modules ===" && \
- ls -lh /opt/qlean/modules/ || true
+ echo "Binary:" && ls -lh /usr/local/bin/qlean && \
+ echo "" && echo "Project libraries:" && ls -lh /opt/qlean/lib/ 2>/dev/null || echo " (none)" && \
+ echo "" && echo "Modules:" && ls -lh /opt/qlean/modules/ 2>/dev/null || echo " (none)" && \
+ echo "" && echo "Vcpkg libraries:" && ls /opt/qlean/vcpkg/ | wc -l && echo " files" && \
+ echo "" && echo "Total image libraries:" && find /opt/qlean -name "*.so*" | wc -l && echo " .so files"
+
+LABEL org.opencontainers.image.description="Qlean-mini runtime image (optimized)"
+LABEL stage=runtime
ENTRYPOINT ["qlean", "--modules-dir", "/opt/qlean/modules"]
CMD ["--help"]
-
diff --git a/Makefile b/Makefile
index 07a771f..17dd82d 100644
--- a/Makefile
+++ b/Makefile
@@ -14,11 +14,42 @@ endif
OS_TYPE := $(shell bash -c 'source $(CI_DIR)/scripts/detect_os.sh && detect_os')
-DOCKER_IMAGE ?= qlean-mini:latest
-DOCKER_PLATFORM ?= linux/amd64
+# Docker image configuration
+# Override these variables to customize image names and tags:
+# make docker_build_all DOCKER_IMAGE_NAME=my-project DOCKER_IMAGE_TAG=v1.0.0
+DOCKER_IMAGE_NAME ?= qlean-mini
+DOCKER_IMAGE_TAG ?= localBuild
+DOCKER_DEPS_TAG ?= latest
+DOCKER_PLATFORM ?= linux/arm64 #linux/amd64
DOCKER_REGISTRY ?= qdrvm
+DOCKER_PUSH_TAG ?= false
+DOCKER_PUSH_LATEST ?= false
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+# Supported platforms: linux/arm64, linux/amd64
+# Usage: make docker_build_all DOCKER_PLATFORM=linux/amd64
+#
+# Multi-arch support:
+# - DOCKER_MULTIARCH=true - enable multi-arch manifest creation
+# - Builds for both platforms and creates unified manifest
+
+# Derived image names for each stage:
+#
+# Dependencies (single version for all commits, changes only when vcpkg.json changes):
+# qlean-mini-dependencies:latest (configurable via DOCKER_DEPS_TAG)
+# qlean-mini-dependencies:v1 (custom)
+#
+# Builder and Runtime (tagged by commit):
+# Commit tag (always): qlean-mini-builder:608f5cc, qlean-mini:608f5cc
+# Additional tag: qlean-mini-builder:localBuild, qlean-mini:localBuild (via DOCKER_IMAGE_TAG)
+#
+DOCKER_IMAGE_DEPS := $(DOCKER_IMAGE_NAME)-dependencies:$(DOCKER_DEPS_TAG)
+DOCKER_IMAGE_BUILDER := $(DOCKER_IMAGE_NAME)-builder:$(GIT_COMMIT)
+DOCKER_IMAGE_RUNTIME := $(DOCKER_IMAGE_NAME):$(GIT_COMMIT)
+
+DOCKER_IMAGE_BUILDER_TAG := $(DOCKER_IMAGE_NAME)-builder:$(DOCKER_IMAGE_TAG)
+DOCKER_IMAGE_RUNTIME_TAG := $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)
+
all: init_all configure build test
@@ -59,46 +90,139 @@ clean_all:
# ==================== Docker Commands ====================
-docker_build_builder:
- @echo "=== [Stage 1/2] Building Docker BUILDER image (init + configure + build) ==="
+# Three-stage build
+
+# Pull dependencies from registry (for CI/CD optimization)
+docker_pull_dependencies:
+ @echo "=== Pulling dependencies from registry ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Image: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)"
+ @echo "Platform: $(DOCKER_PLATFORM)"
+ @if docker pull --platform $(DOCKER_PLATFORM) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS) 2>/dev/null; then \
+ docker tag $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS) $(DOCKER_IMAGE_DEPS); \
+ echo "✓ Dependencies pulled and tagged"; \
+ else \
+ echo "⚠ Dependencies not found in registry, will need to build"; \
+ fi
+
+docker_build_dependencies:
+ @echo "=== [Stage 1/3] Building DEPENDENCIES image ==="
@echo "=== Using build args from .ci/.env ==="
@echo " - CMAKE_VERSION=$(CMAKE_VERSION)"
@echo " - GCC_VERSION=$(GCC_VERSION)"
@echo " - RUST_VERSION=$(RUST_VERSION)"
- @echo " - DEBIAN_FRONTEND=$(DEBIAN_FRONTEND)"
+ @echo " - Platform=$(DOCKER_PLATFORM)"
+ @echo ""
+ @echo "Tag: $(DOCKER_IMAGE_DEPS) (shared across all commits)"
@echo ""
DOCKER_BUILDKIT=1 docker build \
+ --platform $(DOCKER_PLATFORM) \
--build-arg CMAKE_VERSION=$(CMAKE_VERSION) \
--build-arg GCC_VERSION=$(GCC_VERSION) \
--build-arg RUST_VERSION=$(RUST_VERSION) \
--build-arg DEBIAN_FRONTEND=$(DEBIAN_FRONTEND) \
- --target builder \
+ -f Dockerfile.dependencies \
+ --target dependencies-final \
+ --progress=plain \
+ -t $(DOCKER_IMAGE_DEPS) .
+ @echo ""
+ @echo "✓ Dependencies image built: $(DOCKER_IMAGE_DEPS)"
+
+docker_build_builder:
+ @echo "=== [Stage 2/3] Building BUILDER image ==="
+ @echo "=== Using dependencies: $(DOCKER_IMAGE_DEPS) ==="
+ @echo "=== Platform: $(DOCKER_PLATFORM) ==="
+ @echo ""
+ @if ! docker image inspect $(DOCKER_IMAGE_DEPS) >/dev/null 2>&1; then \
+ echo "ERROR: Dependencies image not found: $(DOCKER_IMAGE_DEPS)"; \
+ echo "Run: make docker_build_dependencies"; \
+ echo "Or: make docker_pull_dependencies"; \
+ exit 1; \
+ fi
+ @echo "✓ Using dependencies image: $(DOCKER_IMAGE_DEPS)"
+ @echo ""
+ @echo "Primary tag: $(DOCKER_IMAGE_BUILDER)"
+ @echo "Additional tag: $(DOCKER_IMAGE_BUILDER_TAG)"
+ @echo ""
+ DOCKER_BUILDKIT=1 docker build \
+ --platform $(DOCKER_PLATFORM) \
+ --build-arg DEPS_IMAGE=$(DOCKER_IMAGE_DEPS) \
+ --build-arg GIT_COMMIT=$(GIT_COMMIT) \
+ -f Dockerfile.builder \
--progress=plain \
- -t $(DOCKER_IMAGE)-builder .
+ -t $(DOCKER_IMAGE_BUILDER) \
+ -t $(DOCKER_IMAGE_BUILDER_TAG) .
@echo ""
- @echo "✓ Builder image built: $(DOCKER_IMAGE)-builder"
+ @echo "✓ Builder image built with tags:"
+ @echo " - $(DOCKER_IMAGE_BUILDER)"
+ @echo " - $(DOCKER_IMAGE_BUILDER_TAG)"
docker_build_runtime:
- @echo "=== [Stage 2/2] Building Docker RUNTIME image (final) ==="
- @echo "=== Using existing builder image: $(DOCKER_IMAGE)-builder ==="
+ @echo "=== [Stage 3/3] Building RUNTIME image ==="
+ @echo "=== Using builder: $(DOCKER_IMAGE_BUILDER_TAG) ==="
+ @echo "=== Platform: $(DOCKER_PLATFORM) ==="
+ @echo ""
+ @if docker image inspect $(DOCKER_IMAGE_BUILDER) >/dev/null 2>&1; then \
+ echo "Using builder image: $(DOCKER_IMAGE_BUILDER)"; \
+ elif docker image inspect $(DOCKER_IMAGE_BUILDER_TAG) >/dev/null 2>&1; then \
+ echo "Using builder image: $(DOCKER_IMAGE_BUILDER_TAG)"; \
+ else \
+ echo "ERROR: Builder image not found!"; \
+ echo "Tried: $(DOCKER_IMAGE_BUILDER) and $(DOCKER_IMAGE_BUILDER_TAG)"; \
+ echo "Run: make docker_build_builder"; \
+ exit 1; \
+ fi
+ @echo ""
+ @echo "Primary tag: $(DOCKER_IMAGE_RUNTIME)"
+ @echo "Additional tag: $(DOCKER_IMAGE_RUNTIME_TAG)"
@echo ""
DOCKER_BUILDKIT=1 docker build \
+ --platform $(DOCKER_PLATFORM) \
+ --build-arg BUILDER_IMAGE=$(DOCKER_IMAGE_BUILDER_TAG) \
-f Dockerfile.runtime \
--progress=plain \
- -t $(DOCKER_IMAGE) .
+ -t $(DOCKER_IMAGE_RUNTIME) \
+ -t $(DOCKER_IMAGE_RUNTIME_TAG) .
@echo ""
- @echo "✓ Runtime image built: $(DOCKER_IMAGE)"
+ @echo "✓ Runtime image built with tags:"
+ @echo " - $(DOCKER_IMAGE_RUNTIME)"
+ @echo " - $(DOCKER_IMAGE_RUNTIME_TAG)"
-docker_build: docker_build_runtime
+# Build all stages from scratch (dependencies + code)
+docker_build_all: docker_build_dependencies docker_build_builder docker_build_runtime
+ @echo ""
+ @echo "=== ✓ All Docker images built successfully (3 stages) ==="
+ @echo " - Dependencies: $(DOCKER_IMAGE_DEPS)"
+ @echo " - Builder: $(DOCKER_IMAGE_BUILDER)"
+ @echo " - Runtime: $(DOCKER_IMAGE_RUNTIME)"
-docker_build_all: docker_build_builder docker_build_runtime
+# Fast rebuild: only code (assumes dependencies exist)
+docker_build: docker_build_builder docker_build_runtime
+ @echo ""
+ @echo "=== ✓ Code rebuilt (using cached dependencies) ==="
+ @echo " - Builder: $(DOCKER_IMAGE_BUILDER)"
+ @echo " - Runtime: $(DOCKER_IMAGE_RUNTIME)"
+
+# CI/CD optimized build: pull dependencies from registry, then build code
+docker_build_ci:
+ @echo "=== CI/CD Build ==="
+ @echo "Step 1: Pull dependencies from registry..."
+ @$(MAKE) docker_pull_dependencies || echo "Dependencies not in registry, will build"
+ @echo ""
+ @echo "Step 2: Check if dependencies exist..."
+ @if ! docker image inspect $(DOCKER_IMAGE_DEPS) >/dev/null 2>&1; then \
+ echo "Building dependencies (not found in registry)..."; \
+ $(MAKE) docker_build_dependencies; \
+ fi
@echo ""
- @echo "=== ✓ All Docker images built successfully ==="
- @echo " - Builder: $(DOCKER_IMAGE)-builder"
- @echo " - Runtime: $(DOCKER_IMAGE)"
+ @echo "Step 3: Build project code..."
+ @$(MAKE) docker_build
+ @echo ""
+ @echo "=== ✓ CI/CD build completed ==="
docker_run:
- @echo "=== Running Docker image $(DOCKER_IMAGE) ==="
+ @echo "=== Running Docker image $(DOCKER_IMAGE_RUNTIME) ==="
+ @echo "Platform: $(DOCKER_PLATFORM)"
@echo "Note: --modules-dir is already set in ENTRYPOINT"
@echo ""
@echo "Usage examples:"
@@ -106,35 +230,47 @@ docker_run:
@echo " make docker_run ARGS='--version' # Show version"
@echo " make docker_run ARGS='--base-path /work ...' # Run with custom args"
@echo ""
- docker run --rm -it $(DOCKER_IMAGE) $(ARGS)
+ docker run --rm -it --platform $(DOCKER_PLATFORM) $(DOCKER_IMAGE_RUNTIME) $(ARGS)
docker_clean:
- @echo "=== Cleaning Docker images ==="
- docker rmi -f $(DOCKER_IMAGE) $(DOCKER_IMAGE)-builder 2>/dev/null || true
- @echo "✓ Docker images cleaned"
+ @echo "=== Cleaning Docker images (builder + runtime) ==="
+ @echo "Note: Dependencies will be kept for fast rebuilds"
+ docker rmi -f $(DOCKER_IMAGE_RUNTIME) $(DOCKER_IMAGE_RUNTIME_TAG) \
+ $(DOCKER_IMAGE_BUILDER) $(DOCKER_IMAGE_BUILDER_TAG) 2>/dev/null || true
+ @echo "✓ Builder and runtime images cleaned"
+ @echo "Tip: Use 'make docker_clean_all' to also remove dependencies"
+
+docker_clean_all:
+ @echo "=== Cleaning ALL Docker images (including dependencies) ==="
+ docker rmi -f $(DOCKER_IMAGE_RUNTIME) $(DOCKER_IMAGE_RUNTIME_TAG) \
+ $(DOCKER_IMAGE_BUILDER) $(DOCKER_IMAGE_BUILDER_TAG) \
+ $(DOCKER_IMAGE_DEPS) 2>/dev/null || true
+ @echo "✓ All Docker images cleaned"
docker_inspect:
@echo "=== Docker images info ==="
@docker images | grep qlean-mini || echo "No qlean-mini images found"
docker_verify:
- @echo "=== Verifying Docker image: $(DOCKER_IMAGE) ==="
+ @echo "=== Verifying Docker runtime image ==="
+ @echo "Image: $(DOCKER_IMAGE_RUNTIME)"
+ @echo "Platform: $(DOCKER_PLATFORM)"
@echo ""
@echo "[1/6] Testing help command..."
- @docker run --rm $(DOCKER_IMAGE) --help > /dev/null && echo " ✓ Help works" || (echo " ✗ Help failed" && exit 1)
+ @docker run --rm --platform $(DOCKER_PLATFORM) $(DOCKER_IMAGE_RUNTIME) --help > /dev/null && echo " ✓ Help works" || (echo " ✗ Help failed" && exit 1)
@echo ""
@echo "[2/6] Testing version command..."
- @docker run --rm $(DOCKER_IMAGE) --version && echo " ✓ Version works" || (echo " ✗ Version failed" && exit 1)
+ @docker run --rm --platform $(DOCKER_PLATFORM) $(DOCKER_IMAGE_RUNTIME) --version && echo " ✓ Version works" || (echo " ✗ Version failed" && exit 1)
@echo ""
@echo "[3/6] Checking binary dependencies..."
- @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\
+ @docker run --rm --platform $(DOCKER_PLATFORM) --entrypoint /bin/bash $(DOCKER_IMAGE_RUNTIME) -c '\
apt-get update -qq && apt-get install -y -qq file > /dev/null 2>&1 && \
echo "Binary info:" && file /usr/local/bin/qlean && \
echo "" && echo "Checking for missing libraries..." && \
ldd /usr/local/bin/qlean | grep "not found" && exit 1 || echo " ✓ All binary dependencies OK"'
@echo ""
@echo "[4/6] Checking modules..."
- @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\
+ @docker run --rm --platform $(DOCKER_PLATFORM) --entrypoint /bin/bash $(DOCKER_IMAGE_RUNTIME) -c '\
echo "Modules:" && ls -lh /opt/qlean/modules/ && \
echo "" && echo "Checking module dependencies..." && \
for mod in /opt/qlean/modules/*.so; do \
@@ -143,7 +279,7 @@ docker_verify:
done'
@echo ""
@echo "[5/6] Checking environment variables..."
- @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\
+ @docker run --rm --platform $(DOCKER_PLATFORM) --entrypoint /bin/bash $(DOCKER_IMAGE_RUNTIME) -c '\
echo "LD_LIBRARY_PATH=$$LD_LIBRARY_PATH" && \
echo "QLEAN_MODULES_DIR=$$QLEAN_MODULES_DIR" && \
echo "" && echo "Verifying paths exist:" && \
@@ -151,69 +287,217 @@ docker_verify:
ls -d /opt/qlean/lib > /dev/null && echo " ✓ Lib dir exists" || (echo " ✗ Lib dir missing" && exit 1)'
@echo ""
@echo "[6/6] Checking project libraries..."
- @docker run --rm --entrypoint /bin/bash $(DOCKER_IMAGE) -c '\
+ @docker run --rm --platform $(DOCKER_PLATFORM) --entrypoint /bin/bash $(DOCKER_IMAGE_RUNTIME) -c '\
apt-get update -qq && apt-get install -y -qq file > /dev/null 2>&1 && \
echo "Project libraries:" && ls /opt/qlean/lib/ && \
echo "" && echo "Checking libapplication.so dependencies..." && \
ldd /opt/qlean/lib/libapplication.so | grep "not found" && exit 1 || echo " ✓ All project libraries OK"'
@echo ""
- @echo "=== ✓ All verification checks passed! ==="
+ @echo "=== ✓ Runtime image verified successfully! ==="
+
+# Internal function to push a single Docker image (commit tag + optional additional tags)
+# Args: $(1) = commit tag, $(2) = additional tag, $(3) = stage name, $(4) = build target, $(5) = latest tag
+define push_image
+ @if ! docker image inspect $(1) >/dev/null 2>&1; then \
+ echo "ERROR: $(3) image not found: $(1)"; \
+ echo "Run: make docker_build_$(4)"; \
+ exit 1; \
+ fi
+ @echo "Pushing commit tag: $(DOCKER_REGISTRY)/$(1)"
+ @docker tag $(1) $(DOCKER_REGISTRY)/$(1)
+ @docker push $(DOCKER_REGISTRY)/$(1)
+ @echo "✓ Pushed: $(DOCKER_REGISTRY)/$(1)"
+ @if [ "$(DOCKER_PUSH_TAG)" = "true" ]; then \
+ echo "Pushing additional tag: $(DOCKER_REGISTRY)/$(2)"; \
+ docker tag $(1) $(DOCKER_REGISTRY)/$(2); \
+ docker push $(DOCKER_REGISTRY)/$(2); \
+ echo "✓ Pushed: $(DOCKER_REGISTRY)/$(2)"; \
+ else \
+ echo "Skipping additional tag: $(2) (set DOCKER_PUSH_TAG=true to push)"; \
+ fi
+ @if [ "$(DOCKER_PUSH_LATEST)" = "true" ] && [ "$(DOCKER_IMAGE_TAG)" != "latest" ]; then \
+ echo "Pushing latest tag: $(DOCKER_REGISTRY)/$(5)"; \
+ docker tag $(1) $(DOCKER_REGISTRY)/$(5); \
+ docker push $(DOCKER_REGISTRY)/$(5); \
+ echo "✓ Pushed: $(DOCKER_REGISTRY)/$(5)"; \
+ elif [ "$(DOCKER_PUSH_LATEST)" = "true" ]; then \
+ echo "Skipping latest tag (already pushed as additional tag)"; \
+ fi
+endef
-docker_verify_all: docker_verify
+# Push individual images to registry
+docker_push_dependencies:
+ @echo "=== Pushing dependencies image ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Dependencies tag: $(DOCKER_DEPS_TAG)"
@echo ""
- @echo "=== Verifying builder image: $(DOCKER_IMAGE)-builder ==="
+ @if ! docker image inspect $(DOCKER_IMAGE_DEPS) >/dev/null 2>&1; then \
+ echo "ERROR: Dependencies image not found: $(DOCKER_IMAGE_DEPS)"; \
+ echo "Run: make docker_build_dependencies"; \
+ exit 1; \
+ fi
+ @echo "Pushing: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)"
+ @docker tag $(DOCKER_IMAGE_DEPS) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)
+ @docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)
+ @echo "✓ Pushed: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)"
+
+docker_push_builder:
+ @echo "=== Pushing builder image ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Additional tag: $(DOCKER_IMAGE_TAG) (push: $(DOCKER_PUSH_TAG))"
+ @echo "Latest tag: $(DOCKER_PUSH_LATEST)"
@echo ""
- @echo "Checking builder image exists..."
- @docker image inspect $(DOCKER_IMAGE)-builder > /dev/null 2>&1 && echo " ✓ Builder image found" || (echo " ✗ Builder image not found" && exit 1)
- @echo "Checking builder image size..."
- @docker images $(DOCKER_IMAGE)-builder --format " Size: {{.Size}}"
+ $(call push_image,$(DOCKER_IMAGE_BUILDER),$(DOCKER_IMAGE_BUILDER_TAG),Builder,builder,$(DOCKER_IMAGE_NAME)-builder:latest)
+
+docker_push_runtime:
+ @echo "=== Pushing runtime image ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Additional tag: $(DOCKER_IMAGE_TAG) (push: $(DOCKER_PUSH_TAG))"
+ @echo "Latest tag: $(DOCKER_PUSH_LATEST)"
@echo ""
- @echo "=== ✓ All images verified! ==="
+ $(call push_image,$(DOCKER_IMAGE_RUNTIME),$(DOCKER_IMAGE_RUNTIME_TAG),Runtime,runtime,$(DOCKER_IMAGE_NAME):latest)
-docker_tag:
- @echo "=== Tagging Docker images for push ==="
+# Push all built images to registry
+docker_push:
+ @echo "=== Pushing all Docker images ==="
@echo "Registry: $(DOCKER_REGISTRY)"
- @echo "Commit: $(GIT_COMMIT)"
+ @echo "Commit tag: $(GIT_COMMIT) (always pushed)"
+ @echo "Additional tag: $(DOCKER_IMAGE_TAG) (push: $(DOCKER_PUSH_TAG))"
+ @echo "Latest tag: $(DOCKER_PUSH_LATEST)"
@echo ""
- docker tag $(DOCKER_IMAGE)-builder $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)
- docker tag $(DOCKER_IMAGE)-builder $(DOCKER_REGISTRY)/qlean-mini-builder:latest
- docker tag $(DOCKER_IMAGE) $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)
- docker tag $(DOCKER_IMAGE) $(DOCKER_REGISTRY)/qlean-mini:latest
+ @if docker image inspect $(DOCKER_IMAGE_DEPS) >/dev/null 2>&1; then \
+ echo "[1/3] Pushing dependencies..."; \
+ $(MAKE) docker_push_dependencies; \
+ else \
+ echo "[1/3] Skipping dependencies (not built)"; \
+ fi
@echo ""
- @echo "✓ Images tagged:"
- @echo " - $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)"
- @echo " - $(DOCKER_REGISTRY)/qlean-mini-builder:latest"
- @echo " - $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)"
- @echo " - $(DOCKER_REGISTRY)/qlean-mini:latest"
-
-docker_push: docker_tag
+ @if docker image inspect $(DOCKER_IMAGE_BUILDER) >/dev/null 2>&1; then \
+ echo "[2/3] Pushing builder..."; \
+ $(MAKE) docker_push_builder; \
+ else \
+ echo "[2/3] Skipping builder (not built)"; \
+ fi
@echo ""
- @echo "=== Pushing Docker images to $(DOCKER_REGISTRY) ==="
+ @if docker image inspect $(DOCKER_IMAGE_RUNTIME) >/dev/null 2>&1; then \
+ echo "[3/3] Pushing runtime..."; \
+ $(MAKE) docker_push_runtime; \
+ else \
+ echo "[3/3] Skipping runtime (not built)"; \
+ fi
+ @echo ""
+ @echo "✓ All images pushed to $(DOCKER_REGISTRY)!"
+
+# Push single platform with architecture suffix (for CI/CD on native runners)
+docker_push_platform_dependencies:
+ @echo "=== Pushing dependencies for platform: $(DOCKER_PLATFORM) ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Base tag: $(DOCKER_IMAGE_DEPS)"
@echo ""
- @echo "[1/4] Pushing builder with commit tag..."
- docker push $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)
+ @if ! docker image inspect $(DOCKER_IMAGE_DEPS) >/dev/null 2>&1; then \
+ echo "ERROR: Dependencies image not found: $(DOCKER_IMAGE_DEPS)"; \
+ echo "Run: make docker_build_dependencies"; \
+ exit 1; \
+ fi
+ @if [ "$(DOCKER_PLATFORM)" = "linux/arm64" ]; then \
+ ARCH_SUFFIX="-arm64"; \
+ elif [ "$(DOCKER_PLATFORM)" = "linux/amd64" ]; then \
+ ARCH_SUFFIX="-amd64"; \
+ else \
+ echo "ERROR: Unknown platform $(DOCKER_PLATFORM)"; \
+ exit 1; \
+ fi; \
+ echo "Pushing: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)$$ARCH_SUFFIX"; \
+ docker tag $(DOCKER_IMAGE_DEPS) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)$$ARCH_SUFFIX; \
+ docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)$$ARCH_SUFFIX; \
+ echo "✓ Pushed: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)$$ARCH_SUFFIX"
+
+docker_push_platform:
+ @echo "=== Pushing builder and runtime for platform: $(DOCKER_PLATFORM) ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
@echo ""
- @echo "[2/4] Pushing builder:latest..."
- docker push $(DOCKER_REGISTRY)/qlean-mini-builder:latest
+ @if ! docker image inspect $(DOCKER_IMAGE_BUILDER) >/dev/null 2>&1; then \
+ echo "ERROR: Builder image not found: $(DOCKER_IMAGE_BUILDER)"; \
+ echo "Run: make docker_build_builder"; \
+ exit 1; \
+ fi
+ @if ! docker image inspect $(DOCKER_IMAGE_RUNTIME) >/dev/null 2>&1; then \
+ echo "ERROR: Runtime image not found: $(DOCKER_IMAGE_RUNTIME)"; \
+ echo "Run: make docker_build_runtime"; \
+ exit 1; \
+ fi
+ @if [ "$(DOCKER_PLATFORM)" = "linux/arm64" ]; then \
+ ARCH_SUFFIX="-arm64"; \
+ elif [ "$(DOCKER_PLATFORM)" = "linux/amd64" ]; then \
+ ARCH_SUFFIX="-amd64"; \
+ else \
+ echo "ERROR: Unknown platform $(DOCKER_PLATFORM)"; \
+ exit 1; \
+ fi; \
+ echo "[1/2] Pushing builder..."; \
+ docker tag $(DOCKER_IMAGE_BUILDER) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)$$ARCH_SUFFIX; \
+ docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)$$ARCH_SUFFIX; \
+ echo "✓ Pushed: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)$$ARCH_SUFFIX"; \
+ echo ""; \
+ echo "[2/2] Pushing runtime..."; \
+ docker tag $(DOCKER_IMAGE_RUNTIME) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)$$ARCH_SUFFIX; \
+ docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)$$ARCH_SUFFIX; \
+ echo "✓ Pushed: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)$$ARCH_SUFFIX"; \
+ echo ""; \
+ echo "✓ Platform images pushed to $(DOCKER_REGISTRY)!"
+
+# Create manifest from already pushed platform images (no build, no pull)
+docker_manifest_dependencies:
+ @echo "=== Creating multi-arch manifest for dependencies ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo "Tag: $(DOCKER_IMAGE_DEPS)"
@echo ""
- @echo "[3/4] Pushing runtime with commit tag..."
- docker push $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)
+ @echo "Creating manifest from registry images..."
+ @docker manifest rm $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS) 2>/dev/null || true
+ @docker manifest create $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS) \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)-arm64 \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)-amd64
@echo ""
- @echo "[4/4] Pushing runtime:latest..."
- docker push $(DOCKER_REGISTRY)/qlean-mini:latest
+ @echo "Pushing manifest..."
+ @docker manifest push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)
+ @echo "✓ Multi-arch manifest created: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)"
@echo ""
- @echo "✓ All images pushed successfully!"
+ @echo "Verify with: docker manifest inspect $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_DEPS)"
-docker_build_push: docker_build_all docker_push
+docker_manifest_create:
+ @echo "=== Creating multi-arch manifests for builder and runtime ==="
+ @echo "Registry: $(DOCKER_REGISTRY)"
+ @echo ""
+ @echo "[1/4] Creating builder manifest..."
+ @docker manifest rm $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER) 2>/dev/null || true
+ @docker manifest create $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER) \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)-arm64 \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)-amd64
+ @echo ""
+ @echo "[2/4] Pushing builder manifest..."
+ @docker manifest push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)
+ @echo "✓ Builder manifest: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)"
+ @echo ""
+ @echo "[3/4] Creating runtime manifest..."
+ @docker manifest rm $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME) 2>/dev/null || true
+ @docker manifest create $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME) \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)-arm64 \
+ --amend $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)-amd64
+ @echo ""
+ @echo "[4/4] Pushing runtime manifest..."
+ @docker manifest push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)
+ @echo "✓ Runtime manifest: $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)"
+ @echo ""
+ @echo "✓ All multi-arch manifests created!"
@echo ""
- @echo "=== ✓ Build and push completed ==="
- @echo "Images available at:"
- @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini:latest"
- @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini:$(GIT_COMMIT)"
- @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini-builder:latest"
- @echo " docker pull $(DOCKER_REGISTRY)/qlean-mini-builder:$(GIT_COMMIT)"
+ @echo "Verify with:"
+ @echo " docker manifest inspect $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_BUILDER)"
+ @echo " docker manifest inspect $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_RUNTIME)"
.PHONY: all init_all os init init_py init_vcpkg configure build test clean_all \
- docker_build_builder docker_build_runtime docker_build docker_build_all \
- docker_run docker_clean docker_inspect docker_verify docker_verify_all \
- docker_tag docker_push docker_build_push
+ docker_pull_dependencies \
+ docker_build_dependencies docker_build_builder docker_build_runtime docker_build docker_build_all docker_build_ci \
+ docker_push_dependencies docker_push_builder docker_push_runtime docker_push \
+ docker_push_platform_dependencies docker_push_platform \
+ docker_manifest_dependencies docker_manifest_create \
+ docker_run docker_clean docker_clean_all docker_inspect docker_verify
diff --git a/README.md b/README.md
index eaaeac6..0ad7f2b 100644
--- a/README.md
+++ b/README.md
@@ -72,13 +72,144 @@ cmake --preset default
cmake --build build -j
```
-You can also build the project's Docker images (builder + runtime) with:
+You can also build the project's Docker images with **three-stage build**:
```bash
+# First time (full build)
+make docker_build_all # ~25 min
+
+# Daily development (fast rebuild)
+make docker_build # ~3-5 min ⚡
+```
+
+**Three stages:**
+- `qlean-mini-dependencies:latest` - vcpkg libs (~18 GB, rebuild rarely)
+- `qlean-mini-builder:latest` - project code (~19 GB, rebuild often)
+- `qlean-mini:latest` - runtime image (~240 MB, production)
+
+**When to rebuild dependencies:**
+- `vcpkg.json` or `vcpkg-configuration.json` changes (new libraries)
+- System dependencies update (`.ci/.env`: cmake, gcc, rust versions)
+- Typically: once per month or when adding new dependencies
+- **Tip:** Push dependencies to registry after rebuild for team reuse
+
+**Main commands:**
+```bash
+make docker_build_all # Full build (all 3 stages)
+make docker_build # Fast rebuild (code only)
+make docker_build_ci # CI/CD: pull deps + build (~4 min)
+```
+
+**Platform selection:**
+```bash
+# ARM64 (default)
make docker_build_all
+
+# AMD64 / x86_64
+make docker_build_all DOCKER_PLATFORM=linux/amd64
+
+# Multi-arch: CI/CD on native runners
+# Job 1 (ARM64 runner):
+DOCKER_PLATFORM=linux/arm64 make docker_build_all
+make docker_push_platform # Push with -arm64 tag
+
+# Job 2 (AMD64 runner):
+DOCKER_PLATFORM=linux/amd64 make docker_build_all
+make docker_push_platform # Push with -amd64 tag
+
+# Job 3 (any machine):
+make docker_manifest_create # Create unified manifest
+
+# Run on specific platform
+make docker_run DOCKER_PLATFORM=linux/amd64 ARGS='--version'
+```
+
+**Push/Pull:**
+```bash
+make docker_push_dependencies # Push dependencies to registry (once)
+make docker_push # Push all images (commit tag only)
+make docker_pull_dependencies # Pull dependencies from registry
+
+# Push with custom tag
+DOCKER_PUSH_TAG=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push # commit + v1.0.0
+
+# Push with latest tag
+DOCKER_PUSH_LATEST=true make docker_push # commit + latest
+
+# Push all 3 tags (commit + custom + latest)
+DOCKER_PUSH_TAG=true DOCKER_PUSH_LATEST=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push
+```
+
+**Image tagging:**
+
+Dependencies (single version, shared across commits):
+- Tag: `qlean-mini-dependencies:latest` (configurable via `DOCKER_DEPS_TAG`)
+- Changes only when `vcpkg.json` or system dependencies change
+
+Builder & Runtime (per commit):
+- Each build creates 2 local tags: `qlean-mini:608f5cc` (commit) + `qlean-mini:localBuild` (default)
+- Push behavior:
+ - **Commit tag** (`608f5cc`): always pushed to registry
+ - **Custom tag** (`DOCKER_IMAGE_TAG`): pushed if `DOCKER_PUSH_TAG=true` (default: `localBuild`, not pushed)
+ - **Latest tag**: pushed if `DOCKER_PUSH_LATEST=true`
+- Push up to 3 tags: commit + custom (v1.0.0) + latest
+
+**Utility:**
+```bash
+make docker_run # Run node
+make docker_run ARGS='--version'
+make docker_verify # Test runtime image
+make docker_clean # Clean builder + runtime (keep deps)
+make docker_clean_all # Clean everything (including deps)
+```
+
+**For CI/CD:**
+```bash
+# One command - pulls dependencies from registry, builds code
+export DOCKER_REGISTRY=your-registry
+make docker_build_ci # ~4 min
+make docker_verify
+make docker_push # Push commit tag only
+
+# Production release with version and latest
+DOCKER_PUSH_TAG=true DOCKER_PUSH_LATEST=true DOCKER_IMAGE_TAG=v1.0.0 make docker_push
+# Pushes: qlean-mini:608f5cc + qlean-mini:v1.0.0 + qlean-mini:latest
+
+# Only latest
+DOCKER_PUSH_LATEST=true make docker_push # qlean-mini:608f5cc + qlean-mini:latest
+
+# Staging environment
+DOCKER_PUSH_TAG=true DOCKER_IMAGE_TAG=staging make docker_push # commit + staging
+```
+
+See [DOCKER_BUILD.md](DOCKER_BUILD.md) for details. See the `Makefile` for all Docker targets.
+
+### Automated CI/CD (GitHub Actions)
+
+This project includes GitHub Actions workflow for automated multi-arch Docker builds:
+
+- ✅ **Auto-build on push** to `ci/docker` branch (or tags)
+- ✅ **Manual builds** via GitHub UI with flexible parameters
+- ✅ **Native multi-arch** (ARM64 + AMD64) on free GitHub-hosted runners
+- ✅ **Fast builds** (~20-30 min per architecture, native compilation)
+- ✅ **Smart caching** (rebuilds dependencies only when `vcpkg.json` changes)
+- ✅ **Flexible tagging** (commit hash + custom tag + latest)
+- ✅ **Zero setup** - works out of the box, 100% free for public repos
+
+**Quick actions:**
+
+```bash
+# Push to ci/docker branch → auto-build and push
+git push origin ci/docker
+
+# Create tag → auto-build and push with tag
+git tag v1.0.0 && git push origin v1.0.0
+
+# Manual build via GitHub UI:
+# Actions → Docker Build → Run workflow
```
-See the `Makefile` for more Docker targets.
+See [.github/workflows/README.md](.github/workflows/README.md) for CI/CD documentation.
This will:
- Configure the project into `./build/`
diff --git a/scripts/get_version.sh b/scripts/get_version.sh
index b7d7e35..9d26631 100755
--- a/scripts/get_version.sh
+++ b/scripts/get_version.sh
@@ -53,7 +53,10 @@ cd "$(dirname "$(realpath "$0")")"
SANITIZED=false
[ "$#" -gt 0 ] && [ "$1" = "--sanitized" ] && SANITIZED=true
-if [ -x "$(command -v git)" ] && [ -d "$(git rev-parse --git-dir 2>/dev/null)" ]; then
+# Use GIT_COMMIT environment variable if available (for Docker builds)
+if [ -n "${GIT_COMMIT:-}" ]; then
+ RESULT="$GIT_COMMIT"
+elif [ -x "$(command -v git)" ] && [ -d "$(git rev-parse --git-dir 2>/dev/null)" ]; then
HEAD=$(git rev-parse --short HEAD)
# Determine the main branch (fallback to default names if necessary)
diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp
index e2e91b9..5256731 100644
--- a/src/app/configurator.cpp
+++ b/src/app/configurator.cpp
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
#include
@@ -175,8 +176,8 @@ namespace lean::app {
if (vm.contains("help")) {
std::cout << "Lean-node version " << buildVersion() << '\n';
std::cout << cli_options_ << '\n';
- std::println(std::cout, "Other commands:");
- std::println(std::cout, " qlean key generate-node-key");
+ fmt::println("Other commands:");
+ fmt::println(" qlean key generate-node-key");
return true;
}
diff --git a/src/executable/cmd_key_generate_node_key.hpp b/src/executable/cmd_key_generate_node_key.hpp
index 3177b0e..43b856a 100644
--- a/src/executable/cmd_key_generate_node_key.hpp
+++ b/src/executable/cmd_key_generate_node_key.hpp
@@ -19,6 +19,6 @@ inline void cmdKeyGenerateNodeKey() {
std::make_shared()};
auto keypair = secp256k1.generate().value();
auto peer_id = libp2p::peerIdFromSecp256k1(keypair.public_key);
- std::println("{}", fmt::format("{:0xx}", qtils::Hex{keypair.private_key}));
- std::println("{}", peer_id.toBase58());
+ fmt::println("{}", fmt::format("{:0xx}", qtils::Hex{keypair.private_key}));
+ fmt::println("{}", peer_id.toBase58());
}
diff --git a/src/executable/lean_node.cpp b/src/executable/lean_node.cpp
index b59ce3a..7a07a99 100644
--- a/src/executable/lean_node.cpp
+++ b/src/executable/lean_node.cpp
@@ -135,8 +135,8 @@ int main(int argc, const char **argv, const char **env) {
cmdKeyGenerateNodeKey();
return EXIT_SUCCESS;
}
- std::println(std::cerr, "Expected one of following commands:");
- std::println(std::cerr, " qlean key generate-node-key");
+ fmt::println(stderr, "Expected one of following commands:");
+ fmt::println(stderr, " qlean key generate-node-key");
return EXIT_FAILURE;
}