penguin/monok8s

k8s image for Mono Gateway Dev Kit

commit 58da0aada60a54190580b9929be8cfcfd94190eb6b47a1e58f8d6bca40b9951f

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-03-24T12:52:17Z
subjectcrictl can import images but slow
commit 58da0aada60a54190580b9929be8cfcfd94190eb6b47a1e58f8d6bca40b9951f
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-03-24T12:52:17Z

    crictl can import images but slow
---
 README.md                                      |  28 +++-
 alpine/build-rootfs.sh                         |  17 ++-
 alpine/install-packages.sh                     |  16 +-
 alpine/merge-rootfs.sh                         | 194 +++++++++++++++++++++++++
 alpine/preload-k8s-images.sh                   | 106 ++++++++++++++
 alpine/{ => rootfs-extra}/etc/fancontrol       |   0
 alpine/rootfs-extra/etc/fstab                  |   1 +
 alpine/rootfs-extra/etc/hostname.tmpl.override |   1 +
 alpine/rootfs-extra/etc/inittab                |   2 +
 alpine/rootfs-extra/opt/scripts/load-images.sh |  56 +++++++
 build.env                                      |   7 +
 docker/alpine.Dockerfile                       |   8 +-
 docker/build-base.Dockerfile                   |   7 +-
 docker/initramfs.Dockerfile                    |   6 +-
 makefile                                       |  44 +++++-
 15 files changed, 474 insertions(+), 19 deletions(-)

diff --git a/README.md b/README.md
index f964891..6d43a77 100644
--- a/README.md
+++ b/README.md
@@ -5,17 +5,16 @@ Kubernetes image for Mono Gateway Development Kit
 https://docs.mono.si/gateway-development-kit/getting-started
 
 ## DISCLAIMER
-I leverage ChatGPT heavily for this. I also tested them thoroughly enough that until I am confident that's what we need.
+USE AT YOUR OWN RISKS. I leverage ChatGPT heavily for this. I am testing them all by myself right now.
 
 ### The device's dts files are located at here
 https://github.com/we-are-mono/OpenWRT-ASK/tree/mono-25.12.0-rc3/target/linux/layerscape/files/arch/arm64/boot/dts/freescale
 * We need both `mono-gateway-dk-sdk.dts` and `mono-gateway-dk.dts` since the sdk one includes the non-sdk one.
 * The actual dts being used is the `mono-gateway-dk-sdk.dts`
 
-
 ## Build
 
-To avoid frequent downloading on rebuild. Find the latest packages and update build.env
+Find the latest package versions and update build.env
  * [kernel](https://github.com/nxp-qoriq/linux/archive/refs/tags/)
  * [busybox](https://github.com/mirror/busybox/archive/refs/tags/)
  * [CRI-O](https://github.com/cri-o/cri-o/releases)
@@ -23,12 +22,15 @@ To avoid frequent downloading on rebuild. Find the latest packages and update bu
 
 Then run
 ```
-make download-packages
+make release
 ```
 
+### Making sub stages
 ```bash
-make itb # for out/board.itb (contains the kernel and the initramfs)
-make release # WORK IN PROGRESS
+make build-base # The image the builds kernel and everything
+make kernel     # Builds our kernel from NXP
+make initramfs
+make itb        # Builds out/board.itb (contains the kernel and the initramfs)
 ```
 
 ## Architecture
@@ -66,7 +68,9 @@ setenv serverip 10.0.0.129
 tftp 0x80000000 board.itb
 ```
 
-## USB
+## USB Flashing
+1. Copy board.itb into an SDCard
+2. Copy `[RELEASE_NAME].img.gz` into an SDCard
 ```
 usb start
 usb tree
@@ -77,3 +81,13 @@ fatload usb 0 0x80000000 board.itb
 setenv bootargs "console=ttyS0,115200 earlycon=uart8250,mmio,0x21c0500 root=/dev/ram0 rootwait rw"
 bootm 0x80000000
 ```
+
+3. Inside initramfs
+```
+mount /dev/sda /mnt
+gunzip -c /mnt/[RELEASE_IMAGE].img.gz | dd of=/dev/mmcblk0
+reboot -f
+```
+
+4. If it boots, create the A/B deployment scheme
+ - (WORK IN PROGRESS)
diff --git a/alpine/build-rootfs.sh b/alpine/build-rootfs.sh
index be3074f..fe65074 100755
--- a/alpine/build-rootfs.sh
+++ b/alpine/build-rootfs.sh
@@ -1,8 +1,19 @@
 #!/bin/bash
 
+/preload-k8s-images.sh || exit 1
+
+REGISTRY_DATA_DIR="${KUBE_IMG_CACHE}/registry-data/${ARCH}/${KUBE_VERSION}"
+
+mkdir -p "${ROOTFS}/var/lib/registry"
+
+echo "Copying registry data into rootfs..."
+cp -a "${REGISTRY_DATA_DIR}/." "${ROOTFS}/var/lib/registry/"
+
 mkdir -p "$ROOTFS/var/cache/apk"
+mkdir -p "$ROOTFS/var/cache/k8s-images"
 mkdir -p "$ROOTFS/build"
 mount --bind /var/cache/apk "$ROOTFS/var/cache/apk"
+mount --bind /var/cache/k8s-images "$ROOTFS/var/cache/k8s-images"
 mount --bind /dev  "$ROOTFS/dev"
 mount --bind /proc "$ROOTFS/proc"
 mount --bind /sys  "$ROOTFS/sys"
@@ -11,6 +22,7 @@ mount --bind /run  "$ROOTFS/run"
 cp /usr/bin/qemu-aarch64-static "$ROOTFS/usr/bin/"
 cp /etc/resolv.conf "$ROOTFS/etc/resolv.conf"
 cp /build/crio.tar.gz "$ROOTFS/build/"
+cp /build/registry.tar.gz "$ROOTFS/build/"
 cp -r /build/rootfs/* "$ROOTFS/"
 
 chroot "$ROOTFS" /bin/sh -c "ln -s /var/cache/apk /etc/apk/cache"
@@ -21,6 +33,7 @@ chroot "$ROOTFS" /bin/bash /install-packages.sh || exit 1
 rm "$ROOTFS/install-packages.sh"
 
 umount "$ROOTFS/var/cache/apk"
+umount "$ROOTFS/var/cache/k8s-images"
 umount "$ROOTFS/dev"
 umount "$ROOTFS/proc"
 umount "$ROOTFS/sys"
@@ -28,11 +41,13 @@ umount "$ROOTFS/run"
 
 rm -r "$ROOTFS/build"
 
+/merge-rootfs.sh "/build/rootfs-extra" "$ROOTFS"
+
 ### Begin making full disk image for the device
 echo "=========================== RootFS "$( du -sh "$ROOTFS/" )
 
 IMG=output.img
-SIZE=1024MB
+SIZE=1536MB
 
 dd if=/dev/zero of="$IMG" bs=1 count=0 seek=$SIZE
 
diff --git a/alpine/install-packages.sh b/alpine/install-packages.sh
index 9d6f248..5510301 100755
--- a/alpine/install-packages.sh
+++ b/alpine/install-packages.sh
@@ -10,11 +10,20 @@ rc-update add devfs sysinit
 rc-update add procfs sysinit
 rc-update add sysfs sysinit
 rc-update add loopback boot
+rc-update add hostname boot
+rc-update add localmount boot
 rc-update add fancontrol default
-echo "ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100 -n -l /bin/sh" >> "/etc/inittab"
 echo '[ -x /bin/bash ] && exec /bin/bash -l' >> "/root/.profile"
-echo "export PATH=\"/usr/local/bin:$PATH\"" >> "/etc/profile.d/settings.sh"
 
+# We need this to ship k8s components. (coredns, kube-apiserver, etc)
+echo "##################################################### Install Local Registry"
+mkdir -p /usr/local/bin
+
+apk add skopeo
+
+tar zxf registry.tar.gz
+mv registry /usr/local/bin/registry
+/usr/local/bin/registry --version
 
 echo "##################################################### Installing CRI-O"
 
@@ -30,6 +39,9 @@ if [ $? -ne 0 ]; then
 	exit $?
 fi
 
+mv /etc/cni/net.d/10-crio-bridge.conflist.disabled \
+   /etc/cni/net.d/10-crio-bridge.conflist
+
 echo "--------------"
 sed -i "s/default_runtime = \"crun\"/\0\ncgroup_manager = \"cgroupfs\"/g" /etc/crio/crio.conf.d/10-crio.conf
 grep cgroup_manager /etc/crio/crio.conf.d/10-crio.conf || exit 1
diff --git a/alpine/merge-rootfs.sh b/alpine/merge-rootfs.sh
new file mode 100755
index 0000000..4dd0aae
--- /dev/null
+++ b/alpine/merge-rootfs.sh
@@ -0,0 +1,194 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Usage:
+#   export HOSTNAME=mybox
+#   ./merge-rootfs.sh rootfs-extra /out/rootfs
+#
+# Naming rules:
+#   foo                 -> normal file
+#   foo.tmpl            -> render with envsubst, then normal handling
+#   foo.override        -> replace target directly
+#   foo.tmpl.override   -> render with envsubst, then replace target directly
+#
+# Default handling:
+#   etc/*               -> merge missing lines
+#   opt/scripts/*       -> replace
+#   everything else     -> copy only if missing
+
+SRC_ROOT="${1:?source rootfs path required}"
+DST_ROOT="${2:?target rootfs path required}"
+
+if [[ ! -d "$SRC_ROOT" ]]; then
+    echo "Source rootfs does not exist or is not a directory: $SRC_ROOT" >&2
+    exit 1
+fi
+
+if [[ ! -d "$DST_ROOT" ]]; then
+    echo "Target rootfs does not exist or is not a directory: $DST_ROOT" >&2
+    exit 1
+fi
+
+if ! command -v envsubst >/dev/null 2>&1; then
+    echo "envsubst not found. Install gettext or gettext-envsubst." >&2
+    exit 1
+fi
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+append_missing_lines() {
+    local src="$1"
+    local dst="$2"
+    local changed=1
+    local line
+
+    while IFS= read -r line || [[ -n "$line" ]]; do
+        if ! grep -Fqx -- "$line" "$dst"; then
+            printf '%s\n' "$line" >> "$dst"
+            changed=0
+        fi
+    done < "$src"
+
+    return "$changed"
+}
+
+check_template_vars() {
+    local src="$1"
+    local missing=0
+    local vars var
+
+    vars="$(
+        grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*' "$src" \
+        | sed -E 's/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/\1/' \
+        | sort -u || true
+    )"
+
+    while IFS= read -r var; do
+        [[ -z "$var" ]] && continue
+        if [[ -z "${!var+x}" ]]; then
+            echo "Missing required env var: $var (used in $src)" >&2
+            missing=1
+        fi
+    done <<< "$vars"
+
+    [[ "$missing" -eq 0 ]]
+}
+
+render_template() {
+    local src="$1"
+    local out="$2"
+
+    mkdir -p "$(dirname "$out")"
+    check_template_vars "$src"
+    envsubst < "$src" > "$out"
+}
+
+replace_file() {
+    local src="$1"
+    local dst="$2"
+
+    mkdir -p "$(dirname "$dst")"
+    cp -a "$src" "$dst"
+    echo "Replaced file: $dst"
+}
+
+merge_or_copy_file() {
+    local src="$1"
+    local dst="$2"
+    local rel="$3"
+
+    mkdir -p "$(dirname "$dst")"
+
+    # scripts: replace
+    if [[ "$rel" == opt/scripts/* ]]; then
+        cp -a "$src" "$dst"
+        echo "Replaced script: $dst"
+        return
+    fi
+
+    # /etc: merge missing lines
+    if [[ "$rel" == etc/* ]]; then
+        if [[ ! -e "$dst" ]]; then
+            cp -a "$src" "$dst"
+            echo "Copied new config: $dst"
+            return
+        fi
+
+        if [[ ! -f "$dst" ]]; then
+            echo "Skipping existing non-regular path: $dst" >&2
+            return
+        fi
+
+        if append_missing_lines "$src" "$dst"; then
+            echo "Appended missing lines: $dst"
+        else
+            echo "No changes needed: $dst"
+        fi
+        return
+    fi
+
+    # default: copy only if missing
+    if [[ ! -e "$dst" ]]; then
+        cp -a "$src" "$dst"
+        echo "Copied new file: $dst"
+    else
+        echo "Skipped existing file: $dst"
+    fi
+}
+
+find "$SRC_ROOT" -mindepth 1 | while IFS= read -r src_path; do
+    rel_path="${src_path#"$SRC_ROOT"/}"
+
+    if [[ -d "$src_path" ]]; then
+        mkdir -p "$DST_ROOT/$rel_path"
+        continue
+    fi
+
+    if [[ -L "$src_path" ]]; then
+        dst_path="$DST_ROOT/$rel_path"
+        if [[ -e "$dst_path" || -L "$dst_path" ]]; then
+            echo "Symlink exists, skipping: $dst_path"
+        else
+            mkdir -p "$(dirname "$dst_path")"
+            ln -s "$(readlink "$src_path")" "$dst_path"
+            echo "Created symlink: $dst_path"
+        fi
+        continue
+    fi
+
+    if [[ ! -f "$src_path" ]]; then
+        echo "Skipping unsupported file type: $src_path" >&2
+        continue
+    fi
+
+    src_for_merge="$src_path"
+    rel_for_target="$rel_path"
+    mode="default"
+
+    if [[ "$rel_for_target" == *.tmpl.override ]]; then
+        mode="override"
+        rel_for_target="${rel_for_target%.tmpl.override}"
+        rendered="$TMP_DIR/$rel_for_target"
+        render_template "$src_path" "$rendered"
+        src_for_merge="$rendered"
+
+    elif [[ "$rel_for_target" == *.override ]]; then
+        mode="override"
+        rel_for_target="${rel_for_target%.override}"
+
+    elif [[ "$rel_for_target" == *.tmpl ]]; then
+        rel_for_target="${rel_for_target%.tmpl}"
+        rendered="$TMP_DIR/$rel_for_target"
+        render_template "$src_path" "$rendered"
+        src_for_merge="$rendered"
+    fi
+
+    dst_path="$DST_ROOT/$rel_for_target"
+
+    if [[ "$mode" == "override" ]]; then
+        replace_file "$src_for_merge" "$dst_path"
+    else
+        merge_or_copy_file "$src_for_merge" "$dst_path" "$rel_for_target"
+    fi
+done
diff --git a/alpine/preload-k8s-images.sh b/alpine/preload-k8s-images.sh
new file mode 100755
index 0000000..22388dd
--- /dev/null
+++ b/alpine/preload-k8s-images.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+set -euo pipefail
+
+: "${KUBE_IMG_CACHE:?KUBE_IMG_CACHE is required}"
+: "${KUBE_VERSION:?KUBE_VERSION is required}"
+: "${ARCH:?ARCH is required}"
+
+OS=linux
+REGISTRY_PORT=5000
+
+# Keep everything version/arch scoped so caches do not get mixed.
+ARCHIVE_DIR="${KUBE_IMG_CACHE}/archives/${ARCH}/${KUBE_VERSION}"
+REGISTRY_DATA_DIR="${KUBE_IMG_CACHE}/registry-data/${ARCH}/${KUBE_VERSION}"
+REGISTRY_TAR="${KUBE_IMG_CACHE}/registry.tar"
+REGISTRY_IMAGE="docker.io/library/registry:2"
+
+if podman image exists "${REGISTRY_IMAGE}"; then
+    echo "Registry image already present: ${REGISTRY_IMAGE}"
+
+elif [ -f "${REGISTRY_TAR}" ]; then
+    echo "Loading registry image from cache: ${REGISTRY_TAR}"
+    podman load -i "${REGISTRY_TAR}"
+
+else
+    echo "Cache miss → pulling and saving registry image"
+
+    podman pull "${REGISTRY_IMAGE}"
+    podman save -o "${REGISTRY_TAR}" "${REGISTRY_IMAGE}"
+fi
+
+mkdir -p "${ARCHIVE_DIR}"
+rm -rf "${REGISTRY_DATA_DIR}"
+mkdir -p "${REGISTRY_DATA_DIR}"
+
+echo "============================================================"
+echo "Preparing Kubernetes image cache"
+echo "  KUBE_VERSION      = ${KUBE_VERSION}"
+echo "  ARCH              = ${ARCH}"
+echo "  ARCHIVE_DIR       = ${ARCHIVE_DIR}"
+echo "  REGISTRY_DATA_DIR = ${REGISTRY_DATA_DIR}"
+echo "============================================================"
+
+# Start a temporary local registry backed by REGISTRY_DATA_DIR.
+# We use host network here to avoid messing with nested networking.
+podman rm -f temp-registry >/dev/null 2>&1 || true
+
+podman run -d \
+    --name temp-registry \
+    --network host \
+    --cgroups=disabled \
+    -v "${REGISTRY_DATA_DIR}:/var/lib/registry" \
+    docker.io/library/registry:2
+
+cleanup() {
+    podman rm -f temp-registry >/dev/null 2>&1 || true
+}
+trap cleanup EXIT
+
+# Wait for registry to answer.
+for _ in $(seq 1 30); do
+    if curl -fsS "http://127.0.0.1:${REGISTRY_PORT}/v2/" >/dev/null; then
+        break
+    fi
+    sleep 1
+done
+
+if ! curl -fsS "http://127.0.0.1:${REGISTRY_PORT}/v2/" >/dev/null; then
+    echo "Temporary registry did not start"
+    exit 1
+fi
+
+# Cache and seed all kubeadm-required images.
+while read -r img; do
+    [ -n "$img" ] || continue
+
+    safe_name=$(printf '%s' "$img" | sed 's#/#_#g; s#:#__#g')
+    archive="${ARCHIVE_DIR}/${safe_name}.tar"
+    ref="${img#registry.k8s.io/}"
+
+    if [ ! -f "$archive" ]; then
+        echo "Caching: $img"
+        skopeo copy \
+            --override-os "${OS}" \
+            --override-arch "${ARCH}" \
+            "docker://$img" \
+            "oci-archive:$archive"
+    else
+        echo "Cache hit: $img"
+    fi
+
+    if skopeo inspect --tls-verify=false "docker://127.0.0.1:${REGISTRY_PORT}/${ref}" >/dev/null 2>&1; then
+        echo "Registry hit: 127.0.0.1:${REGISTRY_PORT}/${ref}"
+        continue
+    fi
+
+    echo "Seeding registry: $ref"
+    skopeo copy \
+        --dest-tls-verify=false \
+        "oci-archive:$archive" \
+        "docker://127.0.0.1:${REGISTRY_PORT}/${ref}"
+done < <(qemu-aarch64-static "$ROOTFS/usr/local/bin/kubeadm" config images list --kubernetes-version "${KUBE_VERSION}")
+
+echo
+echo "Done."
+echo "Archives:      ${ARCHIVE_DIR}"
+echo "Registry data: ${REGISTRY_DATA_DIR}"
diff --git a/alpine/etc/fancontrol b/alpine/rootfs-extra/etc/fancontrol
similarity index 100%
rename from alpine/etc/fancontrol
rename to alpine/rootfs-extra/etc/fancontrol
diff --git a/alpine/rootfs-extra/etc/fstab b/alpine/rootfs-extra/etc/fstab
new file mode 100644
index 0000000..8217562
--- /dev/null
+++ b/alpine/rootfs-extra/etc/fstab
@@ -0,0 +1 @@
+none /sys/fs/cgroup cgroup2 defaults 0 0
diff --git a/alpine/rootfs-extra/etc/hostname.tmpl.override b/alpine/rootfs-extra/etc/hostname.tmpl.override
new file mode 100644
index 0000000..1cdcb69
--- /dev/null
+++ b/alpine/rootfs-extra/etc/hostname.tmpl.override
@@ -0,0 +1 @@
+${ALPINE_HOSTNAME}
diff --git a/alpine/rootfs-extra/etc/inittab b/alpine/rootfs-extra/etc/inittab
new file mode 100644
index 0000000..55e1fb0
--- /dev/null
+++ b/alpine/rootfs-extra/etc/inittab
@@ -0,0 +1,2 @@
+# Dev only
+ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100 -n -l /bin/bash
diff --git a/alpine/rootfs-extra/opt/scripts/load-images.sh b/alpine/rootfs-extra/opt/scripts/load-images.sh
new file mode 100755
index 0000000..8700097
--- /dev/null
+++ b/alpine/rootfs-extra/opt/scripts/load-images.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+set -eu
+
+KUBE_VERSION="${KUBE_VERSION:-v1.35.3}"
+REGISTRY_ADDR="${REGISTRY_ADDR:-127.0.0.1:5000}"
+
+mkdir -p /opt/registry
+cat >/opt/registry/config.yml <<EOF
+version: 0.1
+log:
+  level: info
+storage:
+  filesystem:
+    rootdirectory: /var/lib/registry
+http:
+  addr: ${REGISTRY_ADDR}
+EOF
+
+cat >/opt/crictl.yaml <<'EOF'
+runtime-endpoint: unix:///var/run/crio/crio.sock
+image-endpoint: unix:///var/run/crio/crio.sock
+timeout: 10
+debug: false
+EOF
+
+registry serve /opt/registry/config.yml >/var/log/registry.log 2>&1 &
+REG_PID=$!
+
+cleanup() {
+    kill "$REG_PID" 2>/dev/null || true
+}
+trap cleanup EXIT INT TERM
+
+for _ in $(seq 1 30); do
+    if curl -fsS "http://${REGISTRY_ADDR}/v2/_catalog" >/dev/null; then
+        break
+    fi
+    sleep 1
+done
+
+curl -fsS "http://${REGISTRY_ADDR}/v2/_catalog" >/dev/null
+
+for img in $(kubeadm config images list --kubernetes-version "${KUBE_VERSION}"); do
+    name="${img#registry.k8s.io/}"
+    echo "Importing ${img} from ${REGISTRY_ADDR}/${name}"
+    skopeo copy --src-tls-verify=false \
+      "docker://${REGISTRY_ADDR}/${name}" \
+      "containers-storage:${img}"
+done
+
+echo "Imported images now visible to CRI-O:"
+crictl images
+
+kill "$REG_PID" 2>/dev/null || true
+wait "$REG_PID" 2>/dev/null || true
+trap - EXIT INT TERM
diff --git a/build.env b/build.env
index 26b7c50..769dd46 100644
--- a/build.env
+++ b/build.env
@@ -6,6 +6,10 @@ TAG=dev
 # The Linux kernel, from NXP
 NXP_VERSION=lf-6.18.2-1.0.0
 CRIO_VERSION=cri-o.arm64.v1.35.1
+KUBE_VERSION=v1.35.3
+
+# Local registry for exporting k8s images
+REGISTRY_VERSION=2.8.3
 
 # Mono's tutorial said fsl-ls1046a-rdb.dtb but our shipped board is not that one
 # We need fsl-ls1046a-rdb-sdk.dtb here
@@ -23,3 +27,6 @@ BUSYBOX_VERSION=1_36_1
 ## Alpine Linux
 ALPINE_VER=3.23.3
 ALPINE_ARCH=aarch64
+
+# Only applies to new installations
+ALPINE_HOSTNAME=monok8s-hostname
diff --git a/docker/alpine.Dockerfile b/docker/alpine.Dockerfile
index 0d86c9f..9d5a594 100644
--- a/docker/alpine.Dockerfile
+++ b/docker/alpine.Dockerfile
@@ -6,12 +6,14 @@ FROM --platform=$BUILDPLATFORM ${DOCKER_IMAGE_ROOT}/build-base:${TAG} AS build-b
 ARG ALPINE_ARCH
 ARG ALPINE_VER
 ARG CRIO_VERSION
+ARG REGISTRY_VERSION
 ARG DEVICE_TREE_TARGET
 
 RUN mkdir -p "/out/rootfs"
 
 COPY packages/alpine-minirootfs-${ALPINE_VER}-${ALPINE_ARCH}.tar.gz ./alpine.tar.gz
 COPY packages/${CRIO_VERSION}.tar.gz ./crio.tar.gz
+COPY packages/registry-${REGISTRY_VERSION}.tar.gz ./registry.tar.gz
 COPY out/board.itb ./
 COPY out/rootfs ./rootfs
 COPY out/${DEVICE_TREE_TARGET}.dtb ./
@@ -19,5 +21,9 @@ COPY out/Image.gz ./
 
 RUN tar -xf alpine.tar.gz -C "/out/rootfs"
 
-COPY alpine/etc ./rootfs/etc
+RUN mkdir -p /out/rootfs/usr/local/bin/
+COPY packages/kubernetes/* /out/rootfs/usr/local/bin/
+RUN chmod +x /out/rootfs/usr/local/bin/*
+
+COPY alpine/rootfs-extra ./rootfs-extra
 COPY alpine/*.sh /
diff --git a/docker/build-base.Dockerfile b/docker/build-base.Dockerfile
index 9a2119c..7d4e361 100644
--- a/docker/build-base.Dockerfile
+++ b/docker/build-base.Dockerfile
@@ -1,4 +1,4 @@
-FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS kernel-build
+FROM --platform=$BUILDPLATFORM debian:trixie-slim AS kernel-build
 
 ENV DEBIAN_FRONTEND=noninteractive
 
@@ -10,11 +10,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
     bison \
     build-essential \
     cpio \
+	ca-certificates \
     curl \
     dosfstools \
     file \
 	fdisk \
+    fuse-overlayfs \
 	gdisk \
+    e2fsprogs \
     flex \
     git \
     gettext-base \
@@ -27,6 +30,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
     perl \
     python3 \
 	qemu-user-static \
+	podman \
+	skopeo \
     rsync \
     tar \
 	udev \
diff --git a/docker/initramfs.Dockerfile b/docker/initramfs.Dockerfile
index c23a89e..ff73516 100644
--- a/docker/initramfs.Dockerfile
+++ b/docker/initramfs.Dockerfile
@@ -18,8 +18,10 @@ RUN tar -xf busybox-${BUSYBOX_VERSION}.tar.gz && mv "busybox-${BUSYBOX_VERSION}"
 
 WORKDIR /build/busybox
 
-RUN make ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} defconfig\ 
- && sed -i 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
+RUN make ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} defconfig \ 
+ && sed -i \
+ -e 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' \
+ -e 's/^CONFIG_TC=.*/# CONFIG_TC is not set/' .config
 RUN make CROSS_COMPILE=${CROSS_COMPILE} -j"$(nproc)"
 RUN make CROSS_COMPILE=${CROSS_COMPILE} CONFIG_PREFIX=/out/initramfs install
 
diff --git a/makefile b/makefile
index 7c08b8a..04c5a8b 100644
--- a/makefile
+++ b/makefile
@@ -6,10 +6,16 @@ TAG ?= dev
 PACKAGES_DIR := packages
 OUT_DIR      := out
 
-BUSYBOX_TAR := $(PACKAGES_DIR)/busybox-$(BUSYBOX_VERSION).tar.gz
-ALPINE_TAR  := $(PACKAGES_DIR)/alpine-minirootfs-$(ALPINE_VER)-$(ALPINE_ARCH).tar.gz
-NXP_TAR     := $(PACKAGES_DIR)/$(NXP_VERSION).tar.gz
-CRIO_TAR    := $(PACKAGES_DIR)/$(CRIO_VERSION).tar.gz
+BUSYBOX_TAR  := $(PACKAGES_DIR)/busybox-$(BUSYBOX_VERSION).tar.gz
+ALPINE_TAR   := $(PACKAGES_DIR)/alpine-minirootfs-$(ALPINE_VER)-$(ALPINE_ARCH).tar.gz
+NXP_TAR      := $(PACKAGES_DIR)/$(NXP_VERSION).tar.gz
+CRIO_TAR     := $(PACKAGES_DIR)/$(CRIO_VERSION).tar.gz
+REGISTRY_TAR := $(PACKAGES_DIR)/registry-$(REGISTRY_VERSION).tar.gz
+
+# Kubernetes components
+KUBELET_BIN := $(PACKAGES_DIR)/kubernetes/kubelet
+KUBEADM_BIN := $(PACKAGES_DIR)/kubernetes/kubeadm
+KUBECTL_BIN := $(PACKAGES_DIR)/kubernetes/kubectl
 
 BOARD_ITB     := $(OUT_DIR)/board.itb
 INITRAMFS     := $(OUT_DIR)/initramfs.cpio.gz
@@ -62,6 +68,10 @@ RELEASE_DEPS := \
 	$(BOARD_ITB) \
 	$(ALPINE_TAR) \
 	$(CRIO_TAR) \
+	$(KUBELET_BIN) \
+	$(KUBEADM_BIN) \
+	$(KUBECTL_BIN) \
+	$(REGISTRY_TAR) \
 	docker/alpine.Dockerfile \
 	$(ALPINE_SRCS) \
 	build.env \
@@ -71,11 +81,23 @@ RELEASE_DEPS := \
 
 $(PACKAGES_DIR):
 	mkdir -p $@
+	mkdir -p $@/kubernetes
 
 $(OUT_DIR):
 	mkdir -p $@
 
 # ---- Package downloads -------------------------------------------------------
+$(KUBELET_BIN): | $(PACKAGES_DIR)
+	curl -L -o $@ "https://dl.k8s.io/$(KUBE_VERSION)/bin/linux/$(ARCH)/kubelet"
+
+$(KUBEADM_BIN): | $(PACKAGES_DIR)
+	curl -L -o $@ "https://dl.k8s.io/$(KUBE_VERSION)/bin/linux/$(ARCH)/kubeadm"
+
+$(KUBECTL_BIN): | $(PACKAGES_DIR)
+	curl -L -o $@ "https://dl.k8s.io/$(KUBE_VERSION)/bin/linux/$(ARCH)/kubectl"
+
+$(KUBEADM_BIN): | $(PACKAGES_DIR)
+	curl -L -o $@ "https://dl.k8s.io/$(KUBE_VERSION)/bin/linux/$(ARCH)/kubeadm"
 
 $(BUSYBOX_TAR): | $(PACKAGES_DIR)
 	curl -L -o $@ "https://github.com/mirror/busybox/archive/refs/tags/$(BUSYBOX_VERSION).tar.gz"
@@ -83,6 +105,9 @@ $(BUSYBOX_TAR): | $(PACKAGES_DIR)
 $(ALPINE_TAR): | $(PACKAGES_DIR)
 	curl -L -o $@ "https://dl-cdn.alpinelinux.org/alpine/v$(ALPINE_SERIES)/releases/$(ALPINE_ARCH)/alpine-minirootfs-$(ALPINE_VER)-$(ALPINE_ARCH).tar.gz"
 
+$(REGISTRY_TAR): | $(PACKAGES_DIR)
+	curl -L -o $@ "https://github.com/distribution/distribution/releases/download/v$(REGISTRY_VERSION)/registry_$(REGISTRY_VERSION)_linux_$(ARCH).tar.gz"
+
 $(NXP_TAR): | $(PACKAGES_DIR)
 	curl -L -o $@ "https://github.com/nxp-qoriq/linux/archive/refs/tags/$(NXP_VERSION).tar.gz"
 
@@ -139,16 +164,22 @@ $(RELEASE_IMAGE): $(RELEASE_DEPS) | $(OUT_DIR)
 		--build-arg ALPINE_ARCH=$(ALPINE_ARCH) \
 		--build-arg ALPINE_VER=$(ALPINE_VER) \
 		--build-arg CRIO_VERSION=$(CRIO_VERSION) \
+		--build-arg REGISTRY_VERSION=$(REGISTRY_VERSION) \
 		--build-arg DEVICE_TREE_TARGET=$(DEVICE_TREE_TARGET) \
 		-t $(DOCKER_IMAGE_ROOT)/buildenv-alpine:$(TAG) .
 
 	@cid=$$(docker create \
 		--privileged \
+		-v /cache/k8s-images:/var/cache/k8s-images \
 		-v /cache/apk:/var/cache/apk \
 		--device=/dev/loop0:/dev/loop0 \
 		-e ROOTFS=/out/rootfs \
 		-e RELEASE_IMAGE=$(RELEASE_IMAGE) \
 		-e DEVICE_TREE_TARGET=$(DEVICE_TREE_TARGET) \
+		-e KUBE_IMG_CACHE=/var/cache/k8s-images \
+		-e KUBE_VERSION=$(KUBE_VERSION) \
+		-e ALPINE_HOSTNAME=$(ALPINE_HOSTNAME) \
+		-e ARCH=$(ARCH) \
 		$(DOCKER_IMAGE_ROOT)/buildenv-alpine:$(TAG) \
 		bash -lc '/build-rootfs.sh'); \
 	docker start -a $$cid; \
@@ -175,6 +206,9 @@ clean:
 		$(RELEASE_IMAGE)
 
 distclean: clean
-	rm -rf $(OUT_DIR) $(PACKAGES_DIR)
+	rm -rf $(OUT_DIR)
+
+pkgclean:
+	rm -rf $(PACKAGES_DIR)
 
 .PHONY: release kernel initramfs itb build-base clean distclean