A .image unit runs `podman image pull`, which contacts the registry even when
the image is already in local storage. A machine that boots before its network
does fails that pull with "Temporary failure in name resolution", and the
Requires= quadlet generates on the .image unit takes the container down with it.
A failed dependency is never retried -- Restart=always only applies to a unit
that started at least once -- so the service stays down until someone notices.
Observed on the target: the radio head was moved to a basement, powered on, and
cabled some minutes later. It booted, NetworkManager-wait-online declared the
network online 26 s in, the pull failed on DNS, and zwave-js-ui never started
even though the image was sitting in local storage and the controller was
plugged in.
Naming the image in the container lets podman resolve it at start with its
default --pull=missing: the local image starts with no registry involved, and a
genuinely missing image fails the container, which Restart= then retries until
the network is there. Auto-update is unaffected, it reads the container's label.
This is the one deviation from the convention of this repository, and the README
says so and why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
":Z" relabels what it mounts to container_file_t, recursively and on disk, and
init.sh lives in /etc/quadlets/zwave-js-ui. Mounting that directory into the
container therefore left a script systemd could no longer execute:
zwave-js-ui-init.service failed with 203/EXEC at every boot after the first
start of the container, and the container's Requires= on it kept the container
down too. A machine that survived its installation stopped surviving a reboot.
Found on the target machine, not in review: the first installation works,
because Ignition writes /etc with the right labels and the init service runs
before the container. The fault only appears at the second boot.
The README also gains a "Clearing the store" section. The seed is a first-boot
mechanism, so an operator who deletes settings.json -- after rotating the
security keys, for instance, since Z-Wave JS UI keeps its own copy of the old
ones -- has to restart the init service explicitly. "systemctl restart
zwave-js-ui.target" does not re-run it: it is RemainAfterExit=yes and already
active. Getting that wrong leaves a container unhealthy forever with a happy
driver, and authentication silently off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Z-Wave JS UI exposes a Z-Wave controller twice: a web control panel on 8091 and
the Z-Wave JS Server websocket on 3000, which is what the Home Assistant
zwave_js integration connects to. MQTT stays off.
The controller is taken as /dev/zwave, a name a udev rule of the site gives it,
so no site value is baked into the cookbook. The container path is spelled out
in AddDevice: a udev SYMLINK is a symbolic link, and a device passed without one
arrives under its resolved name, which is not the name ZWAVE_PORT asks for.
The security keys, the Z-Wave JS Server settings and the log level come from an
operator-provided file through ZWAVE_EXTERNAL_SETTINGS, so Z-Wave JS UI marks
them managed externally and refuses to edit them. The two settings that file
cannot carry -- authEnabled and mqtt.disabled -- are seeded into the store once,
following the home-assistant cookbook pattern.
The health probe sends Accept: text/plain. Without it the single-page-application
fallback answers 200 for any request that accepts HTML, driver or no driver.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The /dev/vdb -> /var partitioning in cookbooks/base/overlay.bu was merged
into every project's main spec, so it ended up in the production/test
ignition even though the second disk (var.qcow2) only exists in the libvirt
dev/test VM. A real host should not have `base` blindly repartition /dev/vdb.
Move it to local.bu / local.bu.template, which feeds local.ign and is merged
into both fcos-dev.ign and fcos-test.ign but is never part of the shipped
package. This restores partitioning for all VMs while keeping it out of
production.
Also add the overlay-examples.bu merge mechanism to scripts/common.mk (mirror
of the existing overlay.bu merge) for future per-project example overlays, and
whitelist overlay-examples.bu in .gitignore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- home-assistant.container now gates on the live /config/configuration.yaml
instead of the /etc/quadlets bootstrap template: once Home Assistant owns
/config the bootstrap files are no longer needed.
- Move the init unit's inline shell into an external script,
config/init.sh -> /etc/quadlets/home-assistant/init.sh, referenced from
home-assistant-init.service.
- Drop the (non-essential) ConditionPathExists from home-assistant.target;
its members self-gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Podman Quadlet cookbook for Home Assistant.
- Runs as the non-root home-assistant user (UID 10034, GID itix-svc/10000);
s6-overlay v3 self-adjusts /run so no root or idmap workaround is needed.
- PostgreSQL recorder backend (postgresql hook) with the connection string kept
out of packaged files via !secret; Traefik ingress (traefik hook) with the
X-Forwarded-For / trusted_proxies settings a reverse-proxied HA requires.
- /config is precious data on the virtiofs mount; a one-shot init unit bootstraps
configuration.yaml and secrets.yaml into it only when absent (never overwriting
the live copy Home Assistant owns) and creates the empty automations/scenes/
scripts include targets so a fresh /config does not fall into recovery mode.
- Weekly native backup automation writing into /config/backups.
- Health check on /manifest.json with cold-start-sized timeouts.
Verified on a clean Fedora CoreOS VM (real virtiofs): home-assistant.target
active, 8123 answering, recorder writing 13 tables into PostgreSQL, all
containers healthy, and a re-run of the init unit leaving the live config intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `mosquitto` cookbook running Eclipse Mosquitto 2 (multi-arch
docker.io/library/eclipse-mosquitto:2) as a rootless, non-root container
(UID 10033 / GID 10000) serving as the MQTT backbone of a home-automation
stack.
Design highlights:
- Plain MQTT on 127.0.0.1:1883 only; TLS is terminated at Traefik (:443,
SNI-routed) via a TCP router hook fragment (other/traefik/mosquitto.yaml).
Depends on the traefik cookbook.
- Anonymous access refused; password_file and acl_file are operator-provided
(working examples shipped under config/examples/, installed 0640 10033:10000).
No credential is baked into a packaged file.
- Retained messages and sessions persist to the virtiofs precious-data mount
(/var/lib/virtiofs/data/mosquitto), purged on uninstall via a tmpfiles `d$`
entry.
- log_dest stdout so journalctl is the single place to look.
- include_dir conf.d shipped as an empty packaged directory (mosquitto fails if
it is missing); hooks.mk lets dependent cookbooks drop conf.d fragments.
- Health check is an unauthenticated `nc -z 127.0.0.1 1883` TCP connect, so it
needs no secret that may not exist yet.
Verified: make dryrun, whole-repo `make package`, and a podman-level functional
run (non-root startup, loopback-only bind, anonymous refused, authenticated
pub/sub round trip, retained message surviving a restart, health probe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs a browser on the seedbox and displays it on the workstation through
waypipe, for the cases FlareSolverr cannot cover: logging into a private
tracker, or reaching an interface only exposed on the seedbox network.
waypipe lives in the container rather than on the Fedora CoreOS host. The
waypipe server has to share a mount namespace with the browser, and putting it
in the image keeps the host a stock, unlayered CoreOS while pinning the waypipe
version -- 0.9 (C) and 0.10+ (Rust) do not interoperate. The entry point is
seedbox-waypipe.sh, passed to the client with --remote-bin, because
"ssh <host> <command>" never sources /etc/profile.d.
LibreWolf because the seedbox is aarch64, which rules out Tor Browser and
Mullvad Browser (no Linux arm64 build). It ships resistFingerprinting, uBlock
Origin and no telemetry out of the box, and disables its own updater, which
suits an image rebuilt nightly by librewolf-build.timer.
Four things that are not obvious from the code:
- SELinux checks "connectto" on a Unix socket against the listening process,
not the socket file, and the listener is the unconfined SSH session. No
label the container could carry is allowed to connect, hence
--security-opt label=disable rather than a system-wide policy module.
- Firefox's content sandbox chroots itself and needs CAP_SYS_CHROOT in the
bounding set, while bubblewrap -- used by the SVG image loader -- refuses to
run when the caller holds capabilities without being setuid. podman puts
--cap-add in the ambient set too, so the image entrypoint drops ambient and
inheritable capabilities with setpriv, leaving the bounding set alone.
- The forwarded socket is bind-mounted by directory, not by file: mounting the
file alone makes podman synthesize a root-owned 0755 parent inside the
container, and waypipe then cannot unlink its own socket on shutdown.
- "waypipe ssh" forwards several of its own options to the server side, and a
repeated flag is a hard error, so the shim only injects what is absent.
The browser deliberately does not use Network=host, unlike the rest of the
stack: a hostile page must not reach the *arr interfaces on localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kXTkLKMkwYSHZET7ckzmd