Fedora Atomic Desktop (Silverblue / Kinoite / Cosmic Atomic)
Fedora Atomic Desktop (Silverblue / Kinoite / Cosmic Atomic)
Section titled “Fedora Atomic Desktop (Silverblue / Kinoite / Cosmic Atomic)”Atomic Desktop variants use an immutable filesystem. The root filesystem is read-only: you cannot install packages or drop binaries into /usr at runtime. This changes the installation approach.
How it differs from standard Fedora
Section titled “How it differs from standard Fedora”/usris read-only. You cannot curl a binary into/usr/local/bin/on a running system.dnfis not available at runtime. Package installation happens throughrpm-ostreeor at image build time[@docsfedoraprojectorg2026technical]/etcand/varare writable. K0s config (/etc/k0s/) and data (/var/lib/k0s/) work normally.- System changes persist through image builds (Containerfile) or
rpm-ostreeoverlays.
Option A: Bake k0s into a custom image
Section titled “Option A: Bake k0s into a custom image”If you build your own Atomic image (using bootc, BlueBuild, or a plain Containerfile), add k0s at build time. This is the cleanest approach.
In your Containerfile (see e.g.[1] ) 1
# Download k0sRUN K0S_VERSION=$(curl -sSf https://docs.k0sproject.io/stable.txt) && \ curl -sSfL "https://github.com/k0sproject/k0s/releases/download/${K0S_VERSION}/k0s-${K0S_VERSION}-amd64" \ -o /usr/bin/k0s && \ chmod +x /usr/bin/k0s
# Open firewall ports (firewall-offline-cmd because firewalld is not running during build)RUN firewall-offline-cmd --add-port=6443/tcp && \ firewall-offline-cmd --add-port=2380/tcp && \ firewall-offline-cmd --add-port=9443/tcp && \ firewall-offline-cmd --add-port=8132/tcp && \ firewall-offline-cmd --add-port=10250/tcp && \ firewall-offline-cmd --add-port=179/tcp && \ firewall-offline-cmd --zone=trusted --add-interface=kube-bridge && \ firewall-offline-cmd --add-masquerade
# Ship config and setup scriptCOPY config/k0s/k0s.yaml /usr/share/k0s/k0s.yamlCOPY scripts/first-boot/setup-k0s.sh /usr/libexec/setup-k0s.shThen use a first-boot systemd service to run k0s install and k0s start. K0s
install creates systemd units and writes to /var/lib/k0s/, both of which
require a running system.
Example first-boot script:
#!/bin/bashset -ouex pipefail
mkdir -p /etc/k0s
# Read role from /etc/k0s/role, default to controllerROLE="controller"if [[ -f /etc/k0s/role ]]; then ROLE=$(cat /etc/k0s/role)fi
# Copy config from image to /etc (writable overlay)cp /usr/share/k0s/k0s.yaml /etc/k0s/k0s.yaml
case "$ROLE" in controller) k0s install controller --enable-worker --no-taints -c /etc/k0s/k0s.yaml ;; worker) k0s install worker --token-file /etc/k0s/join-token ;;esac
k0s starttouch /var/lib/k0s-first-boot-doneWire it with a systemd oneshot unit that runs once and marks itself done via the sentinel file.
Option B: Install to a writable path
Section titled “Option B: Install to a writable path”If you do not build a custom image, you can place the binary somewhere writable. /usr/local/bin is part of the ostree-managed filesystem on some variants, but /var is always writable:
sudo mkdir -p /var/usrlocal/binK0S_VERSION=$(curl -sSf https://docs.k0sproject.io/stable.txt)curl -sSfL "https://github.com/k0sproject/k0s/releases/download/${K0S_VERSION}/k0s-${K0S_VERSION}-amd64" \ -o /var/usrlocal/bin/k0ssudo chmod +x /var/usrlocal/bin/k0sAdd /var/usrlocal/bin to your PATH, or symlink. Then follow the standard Fedora steps for firewall, SELinux, and installation. Open firewall ports with firewall-cmd (the live firewalld works fine at runtime).
SELinux on Atomic
Section titled “SELinux on Atomic”The same SELinux steps from the standard Fedora section apply. container-selinux is pre-installed on most Atomic images. If not, layer it:
rpm-ostree install container-selinuxsystemctl rebootCreate the containerd SELinux config at /etc/k0s/containerd.d/selinux.toml as described above. /etc is writable, so this works at runtime.
Footnotes
Section titled “Footnotes”-
The
config/k0s.yaml:↩apiVersion: k0s.k0sproject.io/v1beta1kind: ClusterConfigmetadata:name: k0sspec:extensions:helm:repositories:- name: openebs-internalurl: https://openebs.github.io/openebs- name: metallburl: https://metallb.github.io/metallb- name: traefikurl: https://traefik.github.io/chartscharts:# Storage — order 0 so PVCs work before anything else needs them- name: openebschartname: openebs-internal/openebsversion: "4.4.0"namespace: openebsorder: 0values: |engines:local:lvm:enabled: falsezfs:enabled: falsereplicated:mayastor:enabled: falselocalpv-provisioner:hostpathClass:isDefaultClass: true# LoadBalancer — order 1, must be ready before Traefik requests an IP- name: metallbchartname: metallb/metallbversion: "0.14.9"namespace: metallb# Ingress — order 2, needs both storage (ACME persistence) and LB- name: traefikchartname: traefik/traefikversion: "39.0.5"namespace: traefikorder: 2values: |ports:web:port: 8000expose:default: trueexposedPort: 80redirections:entryPoint:to: websecurescheme: httpspermanent: truewebsecure:port: 8443expose:default: trueexposedPort: 443tls:enabled: truecertResolver: letsencryptcertificatesResolvers:letsencrypt:acme:# email: "you@example.com" # optional: get expiry warningsstorage: "/data/acme.json"httpChallenge:entryPoint: webpersistence:enabled: trueaccessMode: ReadWriteOncesize: 128Mipath: /dataservice:type: LoadBalancerworkerProfiles:- name: defaultvalues:evictionHard:nodefs.available: "10Gi"imagefs.available: "10Gi"memory.available: "256Mi"nodefs.inodesFree: "5%"