uCore
Both homelab servers — monitoring and kontti — run uCore, an immutable, image-based Linux built on Fedora CoreOS. The OS is never modified in place: updates are applied as atomic image swaps and take effect after a reboot.
Why immutable Linux
With a traditional Linux server, configuration drift accumulates over time — packages installed manually, files edited by hand, state that exists nowhere except on the running machine. uCore flips this: the base OS is read-only, everything is provisioned declaratively, and the server can be rebuilt from scratch at any time using the same Ignition config and Ansible playbooks.
Initial provisioning
A new server is provisioned in two steps:
1. Generate the Ignition config from Butane
podman run --interactive --rm quay.io/coreos/butane:release \
--pretty --strict < test.butane > test.ign
Butane is a human-friendly YAML format that compiles down to Ignition JSON. The config declares the initial user, SSH keys, hostname, keyboard layout, and the auto-rebase systemd services:
variant: fcos
version: 1.4.0
passwd:
users:
- name: core
ssh_authorized_keys:
- ssh-ed25519 <public-key>
password_hash: <yescrypt-hash>
storage:
directories:
- path: /etc/ucore-autorebase
mode: 0754
files:
- path: /etc/hostname
mode: 0644
contents:
inline: monitoring
- path: /etc/vconsole.conf
mode: 0644
contents:
inline: KEYMAP=fi
systemd:
units:
- name: ucore-unsigned-autorebase.service
enabled: true
contents: |
[Unit]
Description=uCore autorebase to unsigned OCI and reboot
ConditionPathExists=!/etc/ucore-autorebase/unverified
ConditionPathExists=!/etc/ucore-autorebase/signed
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/rpm-ostree rebase --bypass-driver \
ostree-unverified-registry:ghcr.io/ublue-os/ucore:stable
ExecStart=/usr/bin/touch /etc/ucore-autorebase/unverified
ExecStart=/usr/bin/systemctl disable ucore-unsigned-autorebase.service
ExecStart=/usr/bin/systemctl reboot
[Install]
WantedBy=multi-user.target
- name: ucore-signed-autorebase.service
enabled: true
contents: |
[Unit]
Description=uCore autorebase to signed OCI and reboot
ConditionPathExists=/etc/ucore-autorebase/unverified
ConditionPathExists=!/etc/ucore-autorebase/signed
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/rpm-ostree rebase --bypass-driver \
ostree-image-signed:docker://ghcr.io/ublue-os/ucore:stable
ExecStart=/usr/bin/touch /etc/ucore-autorebase/signed
ExecStart=/usr/bin/systemctl disable ucore-signed-autorebase.service
ExecStart=/usr/bin/systemctl reboot
[Install]
WantedBy=multi-user.target
The two autorebase services handle the uCore bootstrap automatically. On first boot, ucore-unsigned-autorebase rebases to the unverified image and reboots. On the second boot, ucore-signed-autorebase takes over and rebases to the signed stable image. The ConditionPathExists guards ensure each service runs exactly once.
2. Boot from ISO and install
# Write ISO to USB
sudo dd if=fedora-coreos-*.iso of=/dev/sdX bs=4M status=progress && sync
# Serve Ignition file over HTTP and install
python3 -m http.server
sudo coreos-installer install /dev/nvme0n1 \
--insecure-ignition \
--ignition-url http://<YOUR_IP>:8000/test.ign
On first boot, two systemd services handle the uCore rebase automatically — first to the unsigned image, then to the signed stable image — rebooting between each step. After that the server is ready for Ansible.
Ansible
All ongoing configuration is managed with Ansible from the ucore-ansible repo. The playbook is run from the ansible/ directory:
# Both servers
ansible-playbook install.yml
# Single server
ansible-playbook install.yml --limit kontti
# Single role
ansible-playbook install.yml --limit kontti --tags media
# Only config files, no service restarts
ansible-playbook install.yml --tags config
Roles map to tags, making it easy to apply targeted changes without running the full playbook.
Podman and Quadlets
All services run as Podman containers managed by root. Instead of docker-compose, containers are defined as Quadlet files — systemd unit files that Podman generates into actual services at boot. Quadlet files live under /etc/containers/systemd/ (system-level), not under a user home directory, so Podman runs as root rather than rootless.
This means every container is a native systemd service: systemctl start plex, journalctl -u plex, restart policies, dependencies — all handled by systemd.
Automatic updates
podman-auto-update runs on a daily timer and pulls new image versions for any container with AutoUpdate=registry set. After a successful update, a script sends a Telegram notification with the list of updated containers.
Scheduled reboots
Since uCore applies OS updates as image swaps, a reboot is needed to activate them. A systemd timer triggers a nightly reboot at a configurable time, ensuring OS updates are picked up automatically without manual intervention.