A self-updating Kubernetes operating system built on Fedora bootc, designed for single-node bare metal deployments.
KubeOS is an immutable, container-native OS image that runs a complete Kubernetes cluster.
- OS updates: Fedora updates pulled daily via GitHub Actions + Renovate
- Kubernetes updates: Managed by Renovate, auto-applied on boot via
kubeadm-auto-upgrade.service - Zero manual intervention: Just reboot when ready - updates apply automatically
- Atomic rollback: If something breaks,
bootc rollbackrestores previous version
- Kubernetes (kubeadm, kubelet, kubectl)
- CRI-O container runtime
- Flannel CNI (10.244.0.0/16 pod network)
- Automatic kubelet CSR approval
- Pre-configured systemd-networkd
- SSH access with your public key
The image is built with production defaults:
- Node IP: 192.168.16.7
- Cluster name: home
- Pod subnet: 10.244.0.0/16
- Service subnet: 10.96.0.0/12
- Storage mounts:
/var/mnt/backup,/var/mnt/media - Node labels:
home.k8s/disk-backup,home.k8s/disk-media,home.k8s/device=coral-tpu,home.k8s/device-igpu
- GitHub Actions runs daily at 2 AM UTC
- Builds container image from Fedora bootc base
- Installs Kubernetes packages from upstream repos
- Copies configuration files from
rootfs/ - Pushes to
quay.io/acardace/kubeos:latestandkubeos:<k8s-version>
- Renovate bot monitors:
- Fedora bootc base image updates
- Kubernetes package updates in the official repos
- Opens PRs when updates are available
- Merge PR β GitHub Action builds new image
- On the node:
bootc upgrade --checkruns daily via timer - Finds new image β stages it for next boot
- User reboots at their convenience
- On boot:
kubeadm-auto-upgrade.servicedetects version mismatch β runskubeadm upgrade apply - Kubernetes cluster upgraded automatically
You only need to reboot. Everything else is automatic.
# Build and push production image
make build
# Build with specific tag
make build TAG=v1.34.1The build script:
- Builds with production network config (192.168.16.0/24)
- Logs into Quay.io using Bitwarden credentials
- Pushes to
quay.io/acardace/kubeos:latest
Note: For local builds, you need rootfs/usr/lib/ostree/auth.json with your Quay.io credentials:
{
"auths": {
"quay.io": {
"auth": "BASE64_ENCODED_USERNAME_PASSWORD"
}
}
}To generate the base64 auth string:
echo -n "username:password" | base64For automated builds, set the QUAY_AUTH_JSON GitHub secret with the entire auth.json content. The workflow will create the file automatically during builds.
# Create isolated test VM
make test
# Test with different Kubernetes version (e.g., for upgrade testing)
KUBERNETES_VERSION=1.34.0 make test
# Quick health check
make test-check
# Full cluster verification
make test-verify
# Debug VM connectivity
make test-debug
# Clean up test environment
make test-cleanThe test script:
- Builds image with test network (192.168.122.0/24)
- Uses default libvirt network with VLAN 2 configuration
- Deploys VM with Fedora CoreOS β switches to bootc image
- Configures VLAN-aware bridge for networking
- Waits for Kubernetes to initialize
VM access: ssh [email protected] (password: debug)
-
Boot bare metal server with Fedora CoreOS live ISO
-
Login and switch to KubeOS image:
# Login to Quay podman login quay.io # Switch to bootc image and apply immediately sudo bootc switch quay.io/acardace/kubeos:latest sudo systemctl reboot
-
After reboot, Kubernetes initializes automatically via
kubeadm-init.service
Updates happen automatically, but you control when:
# Check for available updates
bootc upgrade --check
# Stage update for next boot (automatic via timer, or manual)
bootc upgrade
# Reboot when ready
systemctl rebootOn boot, kubeadm-auto-upgrade.service detects Kubernetes version mismatch and upgrades the cluster automatically.
# Rollback to previous image
bootc rollback
systemctl reboot# Quick health check on production
make remote-check
# Full cluster verification on production
make remote-verifykubeos/
βββ Containerfile # Image definition
βββ Makefile # Build and test targets
βββ .github/
β βββ workflows/
β βββ image.yml # Daily build automation
βββ .renovaterc.json # Renovate bot configuration
βββ rootfs/ # Files copied into image
β βββ etc/
β β βββ kubernetes/
β β β βββ kubeadm-config.yaml # Cluster configuration
β β β βββ patches/ # kubeadm upgrade patches
β β βββ yum.repos.d/
β β β βββ kubernetes.repo # Official Kubernetes repo
β β βββ crio/
β β β βββ crio.conf.d/ # CRI-O CNI config
β β βββ sysctl.d/ # Kernel parameters
β β βββ modules-load.d/ # Kernel modules
β βββ usr/
β β βββ lib/systemd/
β β β βββ network/ # systemd-networkd (VLAN 2)
β β β βββ system/ # Service units
β β βββ local/bin/
β β β βββ kubeadm-auto-upgrade.sh # Auto-upgrade script
β β β βββ approve-kubelet-csr.sh # CSR approval
β β βββ share/ssh-keys/
β β βββ core # Your SSH public key
β βββ var/lib/kubelet/
β βββ config.yaml # Kubelet runtime config
βββ scripts/
βββ build-production.sh # Build and push image
βββ test-vm.sh # Create test VM
βββ cleanup-test.sh # Remove test VM
βββ remote-quick-check.sh # Quick SSH health check
βββ remote-verify-cluster.sh # Full SSH verification
βββ verify-cluster.sh # Local cluster verification
- Renovate monitors
quay.io/fedora/fedora-bootc:42 - Opens PR when new Fedora image available
- Merge β GitHub Actions rebuilds image
bootc upgradeon the node pulls new image
- Renovate monitors Kubernetes package repo
- Updates
ARG KUBERNETES_VERSION=in Containerfile - Merge β GitHub Actions rebuilds with new version
bootc upgradestages new image- Reboot β
kubeadm-auto-upgrade.servicerunskubeadm upgrade apply
All stateful data lives in /var (persistent across image updates):
/var/lib/kubelet- Kubelet data/var/lib/etcd- etcd database/var/lib/containers- CRI-O images/var/lib/rook- Rook-Ceph cluster metadataβ οΈ Must be backed up/var/mnt/*- Your storage mounts/etc- Merged via 3-way merge (local changes preserved)
All configuration lives in rootfs/. To customize:
- Edit files in
rootfs/ - Commit changes
- GitHub Actions rebuilds image automatically (daily, or on push to main)
- Pull update with
bootc upgrade
Example: Change node IP, edit rootfs/usr/lib/systemd/network/30-vlan2.network (or pass build arg).
Cluster won't initialize:
# Check kubeadm-init service
ssh [email protected] journalctl -u kubeadm-init.service -fUpgrade failed:
# Rollback to previous version
bootc rollback
systemctl rebootCheck what's staged for next boot:
bootc statusManual kubeadm upgrade:
# If auto-upgrade fails
sudo systemctl stop kubeadm-auto-upgrade.service
sudo kubeadm upgrade apply v1.34.1 --yes