A learning project that builds a custom Fedora OS image (bootc / OSTree-style) that you can install on real machines and update by rebasing, exactly like Fedora Atomic Desktop / Silverblue.
The source of truth is this repo. Every push to main (or tag) rebuilds the image and pushes it to
GitHub Container Registry (ghcr.io). Machines bootc switch to the image; when you edit the repo,
they pull the new commit on upgrade and boot it next time — with automatic rollback if it fails to boot.
- bootc (
github.com/bootc-dev/bootc) = the modern way to boot a machine from a normal OCI/Docker container image. The image includes a kernel in/usrand systemd runs as pid 1. - The OS is delivered as an image, not a package-by-package install. Updating = rebasing to a new image.
- We build on
quay.io/fedora/fedora-bootc— the official minimal Fedora bootc base — and layer our own packages/config on top.
- Base:
quay.io/fedora/fedora-bootc:latest(official Fedora bootc base) - nginx installed via
dnfand pinned to systemd (auto-start on boot) nginx/index.html-><h1>Test Succeeded!</h1>- Keycloak (dev mode) + Postgres running as a podman pod, auto-started by
systemd via quadlet (see
containers/). Admin console:http://<ip>:8080
The image ships a pod of two containers, defined in containers/ as podman quadlet
units (systemd translates .pod / .container / .volume files into services and
starts them at boot):
| Unit file | What it runs | Listens on |
|---|---|---|
containers/keycloak.pod |
shared pod (network namespace) | 8080, 5432 (LAN) |
containers/keycloak-db.container |
postgres:18 |
5432 in-pod |
containers/keycloak.container |
keycloak:26.7.2 start-dev |
8080 in-pod |
Because it's a pod, Keycloak reaches Postgres at localhost:5432 and both start as
systemd services (keycloak.service, keycloak-db.service, keycloak-pod.service,
auto-enabled to default.target) so they survive reboots and restart on crash.
After the machine is up, from another machine on the LAN:
http://<machine-ip>:8080/admin # admin console
Default dev credentials (set via KC_BOOTSTRAP_ADMIN_* in keycloak.container):
username: admin
password: admin
Postgres is exposed on 5432 (dev only) so you can poke at it:
psql "postgresql://keycloak:keycloak@<machine-ip>:5432/keycloak"
If firewalld is running, allow the ports:
systemctl enable --now firewalld
firewall-cmd --permanent --add-port=8080/tcp --add-port=5432/tcp
firewall-cmd --reloadNotes:
- In
devmode (start-dev) Keycloak binds to0.0.0.0, so the local IP works without a reverse proxy — exactly what we want for a dev box. - The pod images are pre-pulled into the image at build time (rootful/CI builds bake them in so first boot works offline). Rootless local builds skip the pre-pull and the pod just pulls the images on first start.
.
├── Containerfile # the OS image definition (FROM fedora-bootc + our layers)
├── nginx/
│ ├── nginx.conf # server config (port 80)
│ └── index.html # the "test succeeded!" page
├── containers/ # podman quadlet units for the keycloak pod
│ ├── keycloak.pod # pod: shared netns, publishes 8080 + 5432
│ ├── keycloak.container # keycloak:26.7.2 start-dev, admin admin/admin
│ ├── keycloak-db.container # postgres:18, db=user=pass=keycloak
│ └── *.volume # named volumes for postgres + keycloak state
└── .github/workflows/
└── build-image.yml # build + push to ghcr.io on push/tag
FROM quay.io/fedora/fedora-bootc:latest
LABEL org.opencontainers.image.title="immutabletesting"
LABEL org.opencontainers.image.source="https://github.com/budgiebailey/immutabletesting"
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/index.html /usr/share/nginx/html/index.html
RUN dnf -y install nginx && dnf clean all
RUN systemctl enable nginxEdit anything here and push — the workflow rebuilds and repushes the image.
On every push to main/tag:
- logs into ghcr.io (built-in
GITHUB_TOKEN, no extra secret) - builds the image with buildah (required for bootc, keeps the OCI layout intact)
- pushes
ghcr.io/<owner>/immutabletesting:latestand:<git-ref>
If you boot a Fedora Atomic install, you can rebase in-place from the live installer:
# from the installer, once the target disk is ready:
bootc switch --install-to-disk --replace-self \
ghcr.io/budgiebailey/immutabletesting:latestThen reboot into your custom image.
On the running machine:
bootc switch ghcr.io/budgiebailey/immutabletesting:latest
bootc upgrade # fetch the latest stored imageReboot when ready. bootc status shows the deployment; if it fails to boot, the previous
deployment is still there and boots next.
Because the image is managed in the repo:
- edit
nginx/orContainerfile git commit+git push-> CI rebuilds and repushesghcr.io/budgiebailey/immutabletesting:latest- on the target machine:
bootc upgrade(orbootc switch ghcr.io/budgiebailey/immutabletesting:latest) - reboot — the machine is now running your new image
No package installs directly on the box. The repo is the source of truth.
buildah build --label io.bootc.install=1 -f Containerfile -t immutabletesting .- Image builds and pushes automatically to ghcr.io.
- "Test Succeeded!" is served by nginx on port 80.
- Keycloak 26.7.2 (dev admin console) + Postgres 18 run as a systemd-managed podman pod on ports 8080 / 5432 — verified locally: postgres schema initialises and Keycloak listens.