diff --git a/.github/workflows/push_image.yml b/.github/workflows/push_image.yml
index 72eff44bd22b48e2a7d1b111c5f6bf3bbd285bff..f2f1846bc17ccd9f66719e287f1150959ea78615 100644
--- a/.github/workflows/push_image.yml
+++ b/.github/workflows/push_image.yml
@@ -5,11 +5,10 @@ on:
 
 env:
   REGISTRY_USER: netobserv+github_ci
-  REGISTRY_PASSWORD: ${{ secrets.QUAY_SECRET }}
   REGISTRY: quay.io/netobserv
   IMAGE: netobserv-ebpf-agent
-  IMAGE_ORG: netobserv
-  TAGS: main
+  ORG: netobserv
+  VERSION: main
 
 jobs:
   push-image:
@@ -27,22 +26,13 @@ jobs:
           go-version: ${{ matrix.go }}
       - name: checkout
         uses: actions/checkout@v3
-      - name: build images
-        run: make ci-images-build
-      - name: podman login to quay.io
-        uses: redhat-actions/podman-login@v1
+      - name: docker login to quay.io
+        uses: docker/login-action@v2
         with:
           username: ${{ env.REGISTRY_USER }}
-          password: ${{ env.REGISTRY_PASSWORD }}
+          password: ${{ secrets.QUAY_SECRET }}
           registry: quay.io
-      - name: get short sha
-        run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
-      - name: push to quay.io
-        id: push-to-quay
-        uses: redhat-actions/push-to-registry@v2
-        with:
-          image: ${{ env.IMAGE }}
-          tags: ${{ env.TAGS }} ${{ env.short_sha }}
-          registry: ${{ env.REGISTRY }}
+      - name: build and push manifest with images
+        run: IMAGE_ORG=${{ env.ORG }} VERSION=${{ env.VERSION }} make ci
       - name: print image url
         run: echo "Image pushed to ${{ steps.push-to-quay.outputs.registry-paths }}"
\ No newline at end of file
diff --git a/.github/workflows/push_image_pr.yml b/.github/workflows/push_image_pr.yml
index a7e44de1b28593851e25ca9765b768ce52803785..7b78f2a7c98d2d56ee732198d33fdf59922d5b43 100644
--- a/.github/workflows/push_image_pr.yml
+++ b/.github/workflows/push_image_pr.yml
@@ -7,7 +7,8 @@ env:
   REGISTRY_USER: netobserv+github_ci
   REGISTRY: quay.io/netobserv
   IMAGE: netobserv-ebpf-agent
-  IMAGE_ORG: netobserv
+  ORG: netobserv
+  VERSION: temp
 
 jobs:
   push-pr-image:
@@ -28,23 +29,18 @@ jobs:
         uses: actions/checkout@v3
         with:
           ref: "refs/pull/${{ github.event.number }}/merge"
-      - name: build images
-        run: SW_VERSION=temp make ci-images-build
-      - name: podman login to quay.io
-        uses: redhat-actions/podman-login@v1
+      - name: docker login to quay.io
+        uses: docker/login-action@v2
         with:
           username: ${{ env.REGISTRY_USER }}
           password: ${{ secrets.QUAY_SECRET }}
           registry: quay.io
       - name: get short sha
         run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
-      - name: push to quay.io
-        id: push-to-quay
-        uses: redhat-actions/push-to-registry@v2
-        with:
-          image: ${{ env.IMAGE }}
-          tags: ${{ env.short_sha }}
-          registry: ${{ env.REGISTRY }}
+      - name: build and push images
+        run: IMAGE_ORG=${{ env.ORG }} IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ env.short_sha }} make images
+      - name: build and push manifest
+        run: IMAGE_ORG=${{ env.ORG }} VERSION=${{ env.VERSION }} make ci-manifest
       - uses: actions/github-script@v6
         with:
           github-token: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7ad5373f1009b4b57558aefa6028fd9a12c47d07..ea18a8ca404aa0117f2d8b4c43009bba7c14203b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,20 +37,13 @@ jobs:
         uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go }}
-      - name: build images
-        run: SW_VERSION="${{ env.tag }}" make image-build
-      - name: podman login to quay.io
-        uses: redhat-actions/podman-login@v1
+      - name: docker login to quay.io
+        uses: docker/login-action@v2
         with:
           username: ${{ env.REGISTRY_USER }}
           password: ${{ env.REGISTRY_PASSWORD }}
           registry: quay.io
-      - name: push to quay.io
-        id: push-to-quay
-        uses: redhat-actions/push-to-registry@v2
-        with:
-          image: ${{ env.IMAGE }}
-          tags: ${{ env.tag }}
-          registry: ${{ env.REGISTRY }}
+      - name: build and push images
+        run: VERSION="${{ env.tag }}" make images
       - name: print image url
         run: echo "Image pushed to ${{ steps.push-to-quay.outputs.registry-paths }}"
diff --git a/.mk/shortcuts.mk b/.mk/shortcuts.mk
new file mode 100644
index 0000000000000000000000000000000000000000..cea71037b061e8a9f6b4ce30ccde18a90c0f329b
--- /dev/null
+++ b/.mk/shortcuts.mk
@@ -0,0 +1,31 @@
+##@ shortcuts helpers
+
+.PHONY: build
+build: prereqs fmt lint test vendors compile ## Test and Build ebpf agent
+
+.PHONY: build-image
+build-image: image-build ## Build MULTIARCH_TARGETS images
+
+.PHONY: push-image
+push-image: image-push ## Push MULTIARCH_TARGETS images
+
+.PHONY: build-manifest
+build-manifest: manifest-build ## Build MULTIARCH_TARGETS manifest
+
+.PHONY: push-manifest
+push-manifest: manifest-push ## Push MULTIARCH_TARGETS manifest
+
+.PHONY: images
+images: image-build image-push manifest-build manifest-push ## Build and push MULTIARCH_TARGETS images and related manifest
+
+.PHONY: build-ci-manifest
+build-ci-manifest: ci-manifest-build ## Build CI manifest
+
+.PHONY: push-ci-manifest
+push-ci-manifest: ci-manifest-push ## Push CI manifest
+
+.PHONY: ci-manifest
+ci-manifest: ci-manifest-build ci-manifest-push ## Build and push CI manifest
+
+.PHONY: ci
+ci: images ci-manifest ## Build and push CI images and manifest
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 1fa4974b4e168f632e457793b874b73dabf77caa..9a53a01f606830c793310d71feb6b6de4cb6dfa8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,12 @@
+# We do not use --platform feature to auto fill this ARG because of incompatibility between podman and docker
+ARG TARGETPLATFORM=linux/amd64
+ARG BUILDPLATFORM=linux/amd64
 # Build the manager binary
-FROM docker.io/library/golang:1.19 as builder
+FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.19 as builder
 
-ARG SW_VERSION="unknown"
+ARG TARGETPLATFORM
+ARG TARGETARCH=amd64
+ARG VERSION="unknown"
 
 WORKDIR /opt/app-root
 
@@ -14,12 +19,13 @@ COPY vendor/ vendor/
 COPY go.mod go.mod
 COPY go.sum go.sum
 COPY Makefile Makefile
+COPY .mk/ .mk/
 
 # Build
-RUN make compile
+RUN GOARCH=$TARGETARCH make compile
 
 # Create final image from minimal + built binary
-FROM registry.access.redhat.com/ubi9/ubi-minimal:9.1
+FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9/ubi-minimal:9.1
 WORKDIR /
 COPY --from=builder /opt/app-root/bin/netobserv-ebpf-agent .
 USER 65532:65532
diff --git a/Makefile b/Makefile
index efb3dafd76cee242a51654cddf74d73f1f1ccb09..2ef7fdcd06206fb09eaf64df2d4357303d7dcb75 100644
--- a/Makefile
+++ b/Makefile
@@ -1,67 +1,103 @@
-# SW_VERSION defines the project version for the bundle.
+# VERSION defines the project version for the bundle.
 # Update this value when you upgrade the version of your project.
 # To re-generate a bundle for another specific version without changing the standard setup, you can:
-# - use the SW_VERSION as arg of the bundle target (e.g make bundle SW_VERSION=0.0.2)
-# - use environment variables to overwrite this value (e.g export SW_VERSION=0.0.2)
-SW_VERSION ?= main
-BUILD_VERSION := $(shell git describe --long HEAD)
+# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
+# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
+VERSION ?= main
 BUILD_DATE := $(shell date +%Y-%m-%d\ %H:%M)
+TAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1)
+TAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true)
 BUILD_SHA := $(shell git rev-parse --short HEAD)
+BUILD_VERSION := $(TAG:v%=%)
+ifneq ($(COMMIT), $(TAG_COMMIT))
+	BUILD_VERSION := $(BUILD_VERSION)-$(BUILD_SHA)
+endif
+ifneq ($(shell git status --porcelain),)
+	BUILD_VERSION := $(BUILD_VERSION)-dirty
+endif
+
+# Go architecture and targets images to build
+GOARCH ?= amd64
+MULTIARCH_TARGETS ?= amd64 arm64 ppc64le
 
 # In CI, to be replaced by `netobserv`
 IMAGE_ORG ?= $(USER)
 
 # IMAGE_TAG_BASE defines the namespace and part of the image name for remote images.
-# This variable is used to construct full image tags for bundle and catalog images.
 IMAGE_TAG_BASE ?= quay.io/$(IMAGE_ORG)/netobserv-ebpf-agent
 
 # Image URL to use all building/pushing image targets
-IMG ?= $(IMAGE_TAG_BASE):$(SW_VERSION)
-IMG_SHA = $(IMAGE_TAG_BASE):$(BUILD_SHA)
+IMAGE ?= $(IMAGE_TAG_BASE):$(VERSION)
+IMAGE_SHA = $(IMAGE_TAG_BASE):$(BUILD_SHA)
 
-LOCAL_GENERATOR_IMAGE ?= ebpf-generator:latest
+# Image building tool (docker / podman)
+OCI_BIN_PATH := $(shell which podman  || which docker)
+OCI_BIN ?= $(shell v='$(OCI_BIN_PATH)'; echo "$${v##*/}")
 
+LOCAL_GENERATOR_IMAGE ?= ebpf-generator:latest
 CILIUM_EBPF_VERSION := v0.10.0
 GOLANGCI_LINT_VERSION = v1.50.1
-
 CLANG ?= clang
 CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)
 GOOS ?= linux
 PROTOC_ARTIFACTS := pkg/pbflow
-
 # regular expressions for excluded file patterns
 EXCLUDE_COVERAGE_FILES="(/cmd/)|(bpf_bpfe)|(/examples/)|(/pkg/pbflow/)"
 
-# Image building tool (docker / podman)
-ifndef OCI_BIN
-	ifeq (,$(shell which podman 2>/dev/null))
-	OCI_BIN=docker
-	else
-	OCI_BIN=podman
-	endif
-endif
+.DEFAULT_GOAL := help
+
+# build a single arch target provided as argument
+define build_target
+	echo 'building image for arch $(1)'; \
+	DOCKER_BUILDKIT=1 $(OCI_BIN) buildx build --load --build-arg TARGETPLATFORM=linux/$(1) --build-arg TARGETARCH=$(1) --build-arg BUILDPLATFORM=linux/amd64 -t ${IMAGE}-$(1) -f Dockerfile .;
+endef
+
+# push a single arch target image
+define push_target
+	echo 'pushing image ${IMAGE}-$(1)'; \
+	DOCKER_BUILDKIT=1 $(OCI_BIN) push ${IMAGE}-$(1);
+endef
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk commands is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
 
 .PHONY: vendors
-vendors:
+vendors: ## Check go vendors
 	@echo "### Checking vendors"
 	go mod tidy && go mod vendor
 
 .PHONY: prereqs
-prereqs:
-	@echo "### Check if prerequisites are met, and installing missing dependencies"
+prereqs: ## Check if prerequisites are met, and install missing dependencies
+	@echo "### Checking if prerequisites are met, and installing missing dependencies"
 	test -f $(shell go env GOPATH)/bin/golangci-lint || GOFLAGS="" go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}
 	test -f $(shell go env GOPATH)/bin/bpf2go || go install github.com/cilium/ebpf/cmd/bpf2go@${CILIUM_EBPF_VERSION}
 	test -f $(shell go env GOPATH)/bin/protoc-gen-go || go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
 	test -f $(shell go env GOPATH)/bin/protoc-gen-go-grpc || go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
 	test -f $(shell go env GOPATH)/bin/kind || go install sigs.k8s.io/kind@latest
 
+##@ Develop
+
 .PHONY: fmt
 fmt: ## Run go fmt against code.
 	@echo "### Formatting code"
 	go fmt ./...
 
 .PHONY: lint
-lint: prereqs
+lint: prereqs ## Lint the code
 	@echo "### Linting code"
 	golangci-lint run ./... --timeout=3m
 
@@ -72,28 +108,25 @@ lint: prereqs
 .PHONY: generate
 generate: export BPF_CLANG := $(CLANG)
 generate: export BPF_CFLAGS := $(CFLAGS)
-generate: prereqs
+generate: prereqs ## Generate artifacts of the code repo (pkg/ebpf and pkg/proto packages)
 	@echo "### Generating BPF Go bindings"
 	go generate ./pkg/...
 	@echo "### Generating gRPC and Protocol Buffers code"
 	protoc --go_out=pkg --go-grpc_out=pkg proto/flow.proto
 
 .PHONY: docker-generate
-docker-generate:
+docker-generate: ## Create the container that generates the eBPF binaries
 	@echo "### Creating the container that generates the eBPF binaries"
 	$(OCI_BIN) build . -f scripts/generators.Dockerfile -t $(LOCAL_GENERATOR_IMAGE)
 	$(OCI_BIN) run --rm -v $(shell pwd):/src $(LOCAL_GENERATOR_IMAGE)
 
-.PHONY: build
-build: prereqs fmt lint test vendors compile
-
 .PHONY: compile
-compile:
+compile: ## Compile ebpf agent project
 	@echo "### Compiling project"
-	GOOS=$(GOOS) go build -ldflags "-X main.version=${SW_VERSION} -X 'main.buildVersion=${BUILD_VERSION}' -X 'main.buildDate=${BUILD_DATE}'" -mod vendor -a -o bin/netobserv-ebpf-agent cmd/netobserv-ebpf-agent.go
+	GOARCH=${GOARCH} GOOS=$(GOOS) go build -ldflags "-X main.version=${VERSION} -X 'main.buildVersion=${BUILD_VERSION}' -X 'main.buildDate=${BUILD_DATE}'" -mod vendor -a -o bin/netobserv-ebpf-agent cmd/netobserv-ebpf-agent.go
 
 .PHONY: test
-test:
+test: ## Test code using go test
 	@echo "### Testing code"
 	GOOS=$(GOOS) go test -mod vendor -a ./... -coverpkg=./... -coverprofile cover.all.out
 
@@ -102,33 +135,67 @@ cov-exclude-generated:
 	grep -vE "(/cmd/)|(bpf_bpfe)|(/examples/)|(/pkg/pbflow/)" cover.all.out > cover.out
 
 .PHONY: coverage-report
-coverage-report: cov-exclude-generated
+coverage-report: cov-exclude-generated ## Generate coverage report
 	@echo "### Generating coverage report"
 	go tool cover --func=./cover.out
 
 .PHONY: coverage-report-html
-coverage-report-html: cov-exclude-generated
+coverage-report-html: cov-exclude-generated ## Generate HTML coverage report
 	@echo "### Generating HTML coverage report"
 	go tool cover --html=./cover.out
 
-.PHONY: image-build
-image-build: ## Build OCI image with the manager.
-	$(OCI_BIN) build --build-arg SW_VERSION="$(SW_VERSION)" -t ${IMG} .
-
-.PHONY: ci-images-build
-ci-images-build: image-build
-	$(OCI_BIN) build --build-arg BASE_IMAGE=$(IMG) -t $(IMG_SHA) -f scripts/shortlived.Dockerfile .
-
-.PHONY: image-push
-image-push: ## Push OCI image with the manager.
-	$(OCI_BIN) push ${IMG}
-
 .PHONY: tests-e2e
 .ONESHELL:
-tests-e2e: prereqs
+tests-e2e: prereqs ## Run e2e tests
 	go clean -testcache
 	# making the local agent image available to kind in two ways, so it will work in different
 	# environments: (1) as image tagged in the local repository (2) as image archive.
 	$(OCI_BIN) build . -t localhost/ebpf-agent:test
 	$(OCI_BIN) save -o ebpf-agent.tar localhost/ebpf-agent:test
 	GOOS=$(GOOS) go test -p 1 -timeout 30m -v -mod vendor -tags e2e ./e2e/...
+
+##@ Images
+
+# note: to build and push custom image tag use: IMAGE_ORG=myuser VERSION=dev s
+.PHONY: image-build
+image-build: ## Build MULTIARCH_TARGETS images
+	trap 'exit' INT; \
+	$(foreach target,$(MULTIARCH_TARGETS),$(call build_target,$(target)))
+
+.PHONY: image-push
+image-push: ## Push MULTIARCH_TARGETS images
+	trap 'exit' INT; \
+	$(foreach target,$(MULTIARCH_TARGETS),$(call push_target,$(target)))
+
+.PHONY: manifest-build
+manifest-build: ## Build MULTIARCH_TARGETS manifest
+	@echo 'building manifest $(IMAGE)'
+	DOCKER_BUILDKIT=1 $(OCI_BIN) manifest create ${IMAGE} $(foreach target,$(MULTIARCH_TARGETS),--amend ${IMAGE}-$(target));
+
+.PHONY: manifest-push
+manifest-push: ## Push MULTIARCH_TARGETS manifest
+	@echo 'publish manifest $(IMAGE)'
+ifeq (${OCI_BIN}, docker)
+	DOCKER_BUILDKIT=1 $(OCI_BIN) manifest push ${IMAGE};
+else
+	DOCKER_BUILDKIT=1 $(OCI_BIN) manifest push ${IMAGE} docker://${IMAGE};
+endif
+
+.PHONY: ci-manifest-build
+ci-manifest-build: manifest-build ## Build CI manifest
+	$(OCI_BIN) build --build-arg BASE_IMAGE=$(IMAGE) -t $(IMAGE_SHA) -f scripts/shortlived.Dockerfile .
+ifeq ($(VERSION), main)
+# Also tag "latest" only for branch "main"
+	$(OCI_BIN) build -t $(IMAGE) -t $(IMAGE_TAG_BASE):latest -f scripts/shortlived.Dockerfile .
+endif
+
+.PHONY: ci-manifest-push
+ci-manifest-push: ## Push CI manifest
+	$(OCI_BIN) push $(IMAGE_SHA)
+ifeq ($(VERSION), main)
+# Also tag "latest" only for branch "main"
+	$(OCI_BIN) push ${IMAGE}
+	$(OCI_BIN) push $(IMAGE_TAG_BASE):latest
+endif
+
+include .mk/shortcuts.mk
diff --git a/README.md b/README.md
index 9295c246a12c3ac8f3739c6cf5b43f950d81c7e8..f184e5916dfb5e981b2a61303ecb87264ec6aecd 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 The Network Observability eBPF Agent allows collecting and aggregating all the ingress and
 egress flows on a Linux host (required a Kernel 4.18+ with eBPF enabled).
 
-* [How to compile](#how-to-compile)
+* [How to build](#how-to-build)
 * [How to configure](#how-to-configure)
 * [How to run](#how-to-run)
 * [Development receipts](#development-receipts)
@@ -13,16 +13,24 @@ egress flows on a Linux host (required a Kernel 4.18+ with eBPF enabled).
 * [Frequently-asked questions](#frequently-asked-questions)
 * [Troubleshooting](#troubleshooting)
 
-## How to compile
+## How to build
 
-```
+To build the agent image and push it to your Docker / Quay repository, run:
+```bash
+# compile project
 make build
-```
 
-To build the agent image and push it to your Docker / Quay repository, run:
+# build the default image (quay.io/netobserv/netobserv-ebpf-agent:main):
+make image-build
 
-```bash
-IMG=quay.io/myaccount/netobserv-ebpf-agent:dev make image-build image-push
+# push the default image (quay.io/netobserv/netobserv-ebpf-agent:main):
+make image-push
+
+# build and push on your own quay.io account (quay.io/myuser/netobserv-ebpf-agent:dev):
+IMAGE_ORG=myuser VERSION=dev make images
+
+# build and push on a different registry
+IMAGE=dockerhub.io/myuser/plugin:tag make images
 ```
 
 ## How to configure