Tumbleweed Server on AlphaVPS
This experiment targets an AlphaVPS C6G in Sofia: 3 shared vCPU, 6 GB RAM and 55 GB NVMe.
Install openSUSE Tumbleweed using its Server role, without a desktop, then use
deployments/ansible/
to prepare the ordinary runtime account and observe the host floor. Tumbleweed Server is an
installation choice, not a separate distribution.
For a laptop, use the independent KDE workstation profile. Personal and administrator shells selects Zsh by default or Fish for the existing administrator, separately from the runtime account; the guarded provisioning branch includes it. AutoYaST prepares explicit installation inputs for either class. Optional private access has its own host lifecycle and one-owner acceptance boundary. Snapshot upgrade qualification separately tests one exact OS transition in an owned disposable QEMU guest; ordinary host apply still performs no rolling upgrade or reboot.
One pilot before a fleet
The playbooks and source checks are candidate tooling. No successful Tumbleweed installation, second converged host run, rootless container operation, or recovery cycle has been observed by this campaign. PR #136 reports five VPS trials; those are contributor reports, not a reproduced receipt for this changed candidate. Reinstalling a VPS destroys its existing disk. Select one explicitly disposable pilot, retain recovery access, and inspect the installer proposal before permitting erasure. The playbooks never partition, format or reboot a host.
Host preparation and application readiness are separate. Summoning owns the application rite; State of Work owns its incomplete acceptance. An initial application without a model can test startup and authenticated database/queue readback. It cannot answer chat. Active container lifecycle and Portal inference remain blocked; see Application acceptance.
What each layer owns
| Layer | Responsibility |
|---|---|
| Provider and installer | Console, boot media, network, destructive OS installation, firmware-compatible boot and Btrfs layout. |
| Ansible host playbook | Already installed Tumbleweed, required packages, a dedicated non-root account, checked subordinate IDs, public keys, linger and modest existing Snapper retention. |
| Source preparation | One exact public source revision and its host CLI; no moving branch independently on each machine. |
| LychD CLI | Application layout, settings, secrets, generated units, datastore ownership, bind and guarded lifecycle. |
| Operator acceptance | Real host observations, recovery, application readback and the decision to repeat the experiment elsewhere. |
Ansible does not generate an independent application unit/configuration stack. It leaves existing firewall rules, service enablement and SSH authentication policy unchanged; review those explicitly before calling a host ready. It does not run rolling upgrades, prune images, delete snapshots, or install a model. Private input belongs outside every application checkout. The optional access playbook owns only its declared separate infrastructure services.
Install one headless host
Record the selected service, its existing data disposition, firmware, disk/NIC models, addresses/prefixes/gateways/DNS, console and rescue access in a private worksheet. A working Ubuntu template does not prove DHCP will work in another installer. Do not reset a password or change another person's SSH access merely to avoid resolving those inputs.
Prefer an authenticated official Offline or NET ISO with the AutoYaST procedure. PR #136 reports a corrupted installer driver image and missing NIC through netboot.xyz on the provider, while its official NET ISO trial worked. Treat netboot.xyz as a fallback to test through the console, not a guaranteed installation path. AlphaVPS documents ISO/boot controls. Its existing Netboot.xyz entry can reach the upstream openSUSE menu:
- Attach Netboot.xyz only to the selected pilot and open its verified noVNC console.
- Select CD-ROM before disk and reboot only within the authorized interruption window.
- In Netboot.xyz select Linux Network Installs (64-bit) → openSUSE → Tumbleweed.
- Select the Server role. Review the package selection and omit desktop patterns.
- Review networking, SELinux Enforcing, boot loader, administrator access and the complete storage proposal. Stop before destructive acceptance until the exact disk and data loss have been approved.
- Complete installation, detach the ISO, restore disk-first boot, and observe an ordinary reboot and SSH/console recovery before using Ansible.
The upstream installer control defines Server as a small package set with a text interface. Observe the actual installer; upstream source is not a receipt for the provider's boot medium.
Network installation trust
The Netboot.xyz openSUSE script normally uses an HTTP mirror for the installer kernel and
initrd; printing MD5 values does not authenticate them. After the loader has read its
configuration, its iPXE shell can set opensuse_mirror to
https://download.opensuse.org, then return to the menu. Verify the actual displayed retrieval
URLs and stop on certificate errors. Do not disable certificate validation to continue.
That transport check is not the same as verifying a publisher-signed, pinned ISO.
Alternatively, openSUSE publishes a Network ISO with checksum and signature. Pin and authenticate the exact media before using a provider-assisted mount. Netboot.xyz's Secure Boot documentation does not establish end-to-end openSUSE Secure Boot installation. Confirm actual firmware and policy; do not silently disable an existing protection. Record the installed snapshot/version and source route.
Fit Btrfs and snapshots into 55 GB
Keep the installer-supported boot partitions and use one main Btrfs capacity pool, with
separately mounted subvolumes for /, /home and /var. Preserve its supported Snapper and
boot-loader integration. Do not allocate an arbitrary fixed 18 GiB root and 22 GiB home: that
can strand free space. Size the shared pool and reserve against the actual workload.
Btrfs snapshots initially share blocks. Changed or deleted extents retained by old snapshots, and metadata, consume additional space; a snapshot is not another full copy of the 55 GB disk. Subvolumes share free capacity unless quotas restrict them, and root snapshots are not recursive. Btrfs documents both properties.
Keep models, the actual rootless Podman graph root, application state, database and caches
outside the root OS snapshot set. Verify their backing mounts/subvolumes, not just path names.
For this pilot preserve the installer's /home and /var exclusion boundaries. Retain only a
few useful OS recovery points and monitor free space before package changes. The host role
changes retention only when the installer already created /etc/snapper/configs/root:
- timeline creation off;
- number cleanup on;
- six normal snapshots and two important snapshots.
Those are counts, not hard byte caps. The role does not invoke cleanup, create a replacement root config, or delete the active/default recovery point. Review installed cleanup timers and quota behavior separately. Aim for roughly 10 GB free as an operating target, then revise the budget against actual installed packages, images and database growth. An OS snapshot is neither a consistent PostgreSQL capture nor an off-host backup.
Inspect after installation and after one controlled update:
lsblk -f
findmnt -T /
findmnt -T /home
findmnt -T /var
df -h / /home /var
sudo btrfs filesystem usage /
sudo snapper -c root list
sudo snapper -c root get-config
Prepare the controller and private inventory
Run Ansible on a separate Linux x86_64 controller with Python 3.13, its venv module and
pip available. Install those controller prerequisites using its distribution packages first.
From the source checkout, choose an absolute private directory outside it:
LYCHD_DEPLOY_WORK="$HOME/.local/state/lychd-operator"
mkdir -p "$LYCHD_DEPLOY_WORK"
chmod 700 "$LYCHD_DEPLOY_WORK"
python3.13 -m venv "$LYCHD_DEPLOY_WORK/controller"
"$LYCHD_DEPLOY_WORK/controller/bin/python" -m pip install \
-r deployments/ansible/requirements-controller.txt
cp deployments/ansible/inventory.example.yml "$LYCHD_DEPLOY_WORK/inventory.yml"
cp deployments/ansible/vars.example.yml "$LYCHD_DEPLOY_WORK/host-vars.yml"
chmod 600 "$LYCHD_DEPLOY_WORK/inventory.yml" "$LYCHD_DEPLOY_WORK/host-vars.yml"
Edit those private copies. The inventory identifies an existing administrator and one alias
pilot; use its real address and a verified host key. The host variables require an ordinary
runtime UID/GID, unoccupied subordinate UID/GID ranges, and plain public SSH keys. Inspect
the actual host before choosing IDs. Never put private keys in Ansible variables, Git, a ticket
or a container. Keep runtime and infrastructure-recovery credentials separate.
Host-key checking stays enabled. After a deliberate reinstall, compare the new SSH public-key
fingerprint through the verified provider console before replacing the corresponding local
known-host entry. Do not use StrictHostKeyChecking=no as an installation shortcut.
Apply and observe the host
Resolve the stock sudo policy before creating the runtime UID
Tumbleweed's vendor default can grant every user sudo using the root password. The dedicated
lychd account must have no sudo grant, so the host role refuses that policy. AutoYaST's fresh
profile writes a named-administrator policy. For an already installed host still using the exact
vendor policy, the separate admin.yml operation can install a standalone policy granting only
root and the explicitly named administrator, authenticated with that administrator's own password.
It refuses custom sudoers and existing drop-ins rather than discarding their rules.
Keep the verified provider console open. Confirm the administrator already has a working own
password before changing sudo policy. From deployments/ansible, preview the exact one-host change:
ANSIBLE_HOME="$LYCHD_DEPLOY_WORK/ansible" \
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" \
-i "$LYCHD_DEPLOY_WORK/inventory.yml" playbooks/admin.yml --limit pilot -K \
-e lychd_admin_name=existing_admin -e lychd_admin_policy_target=pilot --check --diff
Replace existing_admin with the actual inventory login, then repeat without --check.
The first invocation's sudo prompt follows the old policy (often the root password). The playbook
ends after validated publication: Ansible retains one -K password and cannot switch it mid-run.
Use a separate new invocation/session with the administrator's own password. Verify fresh SSH and
sudo before closing the console. The target-local backup is recovery material. Custom policy
needs console reconciliation; do not delete files to bypass a refusal.
Converge the runtime host floor
From deployments/ansible, run the controller with the private inventory and variables.
If the administrator requires a sudo password, add --ask-become-pass (-K) to the host
and verification commands; enter it interactively rather than storing it in the inventory:
cd deployments/ansible
ANSIBLE_HOME="$LYCHD_DEPLOY_WORK/ansible" \
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" \
-i "$LYCHD_DEPLOY_WORK/inventory.yml" playbooks/host.yml \
-e "@$LYCHD_DEPLOY_WORK/host-vars.yml" --limit pilot --check --diff
Inspect the predicted changes, then repeat without --check. Check mode does not create the
new runtime account: dependent home/key/mapping/linger work is skipped until that account
exists. It is not a successful target execution. Run the actual playbook a second time and
retain its result; the expected steady state is no persistent changes.
The role refuses a wrong distro, non-enforcing SELinux, unsuitable storage, identity collisions,
and an existing runtime account that lacks its own matching root-owned receipt. It never
remaps an existing container store or adopts arbitrary account state. Its package installation
uses existing repositories and installs only missing capabilities, without zypper dup or
GPG bypasses. Refresh trusted repository metadata explicitly if a stale cache prevents install.
Interrupted account creation needs explicit recovery; do not remove its receipt to force a retry.
Once the mapping stage has been recorded, both exact subordinate ranges must remain present.
Runtime home and SSH-file work uses the runtime user's authority, with no privileged fallback.
The floor also owns one UID-specific systemd drop-in,
user@UID.service.d/80-lychd-delegate.conf, requesting Delegate=cpu memory pids.
This overrides Tumbleweed's vendor delegation reset only for the runtime manager; it changes
no resource ceilings. New identities receive it after the actual UID and no-sudo assertion,
before the initialized-maps checkpoint and before linger starts their manager. Every post-map
receipt produced by this recipe therefore already has the artifact, and ordinary interrupted
post-map creation can resume. An existing identity missing this file requires explicit one-time
operator preparation before retrying, including identities created by an older recipe with a
valid maps_initialized or configured schema-1 receipt. The receipt remains valid; do not delete
or rewrite it to bypass this maintenance boundary. The role does not infer application inactivity,
inspect container storage, query Podman state, or automatically prepare legacy identities.
An exact existing artifact is left unchanged even when an application is active. Foreign local
controller policy (including /usr/local/lib/systemd/system) and custom manager placement are
refused. The manager and its standard ancestor slices must report NeedDaemonReload=no before
convergence or verification; stale or unknown unit policy requires operator review, not an
automatic preflight reload.
The floor reloads PID1 only after publishing the file. It never restarts or reexecutes a user
manager, reboots, or kills a workload. If the current manager still lacks controllers, verification
reports activation pending: stop provisioning, perform the separately authorized controlled
boot, then rerun verification and the second no-change apply. A successful reload is not proof
of effective delegation. Root controller support, the exact managed file, effective delegation
and available controllers at the user-manager boundary are required; a resource-limited build
or container remains a separate live acceptance check. CPUAccounting= does not enable CPU
control on current systemd. See systemd's delegation contract.
For an existing valid schema-1 identity whose delegation artifact is absent, perform this one-time operator maintenance before retrying the host role:
- Independently establish that the body is quiescent and this maintenance is authorized. Retain tested provider-console access and an authorized controlled-boot recovery plan. The role does not establish application inactivity; public active-container stop/start is still incomplete. Do not substitute an improvised stop, kill, or restart procedure.
- Inspect and preserve
/etc/lychd-host-floor.json. Confirm its identity matches the actuallychdaccount's numeric UID/GID, exact retained subordinate maps and intended host variables; confirm the runtime has no sudo grants. A validmaps_initializedorconfiguredreceipt must not be deleted or rewritten to make the account appear new. Acreatingreceipt needs separate interrupted-creation recovery. - Review the manager and standard ancestor slices' effective and on-disk controller policy,
including local units/drop-ins under
/etc,/runand/usr/local/lib/systemd/system. Resolve stale policy through separate operator review; do not blindly reload or overwrite it. Require standard placement and no conflicting local delegation or disabled required controllers. Use the actual runtime UID in the destination below, not an assumed UID. - Verify every destination ancestor is a real, root-owned directory, with no symlinks or
group/world write permission. Create the absent per-UID directory
/etc/systemd/system/user@UID.service.das root, mode0755; preserve any existing trusted directory. Create only an absent80-lychd-delegate.confthere, as a root-owned, single-link regular file with mode0644. Do not replace a foreign file or follow a link. Its content must be exactly these three lines, including the ownership comment and a final LF newline, with no leading indentation or extra blank lines:
# Managed by LychD host floor: runtime cgroup delegation v1
[Service]
Delegate=cpu memory pids
- After changed publication, reload PID1 with
systemctl daemon-reload. An already exact file requires no publication or reload. Do not restart or reexecute the live user manager. - For a
configuredreceipt with the existing floor complete, run the read-only observation playbook below first. After it passes, review a host-role check run and apply. If observation reports activation pending, follow step 8 before continuing with that check/apply. - For a valid
maps_initializedreceipt, resume only the independently confirmed pre-application identity admitted in step 1. After exact artifact publication and PID1 reload, review a host-role check run and apply to finish keys, linger and theconfiguredcheckpoint. Do not require the observation playbook to pass before this resume: it requires configured state and linger. The host apply performs verification after configuration and may then report activation pending if a user manager already existed. Preparation or a successful check alone does not establish a configured floor. - On activation pending in either path, stop further provisioning, perform the separately authorized controlled boot, then run read-only observation again. Daemon reload alone does not prove that the required controllers reached the manager. After configuration and verification pass, repeat host apply; this second apply must report no persistent changes. Retain a separate successful resource-limited native build or container observation before accepting CPU/memory enforcement. These checks do not establish application recovery support.
Then run the observation playbook:
ANSIBLE_HOME="$LYCHD_DEPLOY_WORK/ansible" \
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" \
-i "$LYCHD_DEPLOY_WORK/inventory.yml" playbooks/verify.yml \
-e "@$LYCHD_DEPLOY_WORK/host-vars.yml" --limit pilot
This administrative observation does not replace a direct SSH login as lychd. From that
login, verify the responding user manager and actual rootless storage/network behavior:
id
loginctl show-user "$USER" --property=Linger
systemctl --user show --property=Version --value
podman --version
podman info --format '{{.Host.Security.Rootless}} {{.Store.GraphRoot}}'
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map
Require effective Podman and user Quadlet at least 5.4, cgroup v2, correct maps, Enforcing
SELinux and the intended Btrfs graph-root placement. Current Tumbleweed mapping helpers can
come from account-utils and delegate to newidmapd; merely installing shadow does not
prove rootless mapping works. Observe failures before changing services or permissions.
The verification output reports the actual Landlock ABI; require at least 6 before accepting
that host capability. A separate actual-container-policy observation remains necessary;
neither probe implements Tomb/nono.
Prepare one exact LychD source
Resolve the desired main revision once on the controller and record its full SHA. Reuse
that exact revision on every host in the campaign; independently resolving moving main
on five servers does not identify one deployment.
git ls-remote https://github.com/hexanomicon/lychd.git refs/heads/main
The source-preparation playbook stages a versioned checkout and host CLI through direct SSH
as lychd. It does not build or start the application, edit its settings, bind units or
generate credentials. An active or unknown current/retired Vessel blocks source changes;
an existing interpreter path must not be replaced beneath Host Reactor. A completed versioned
environment is preserved, while dirty, unreceipted or partially prepared workspaces refuse.
The controller requirements also install uv 0.13.4 from PyPI. Admit that Linux x86_64 executable by its actual SHA-256 and copy it through the role; no remote installer is run. Substitute the full source SHA observed above and the private key corresponding to the installed runtime key:
LYCHD_SOURCE_REV=FULL_40_CHARACTER_SHA
LYCHD_RUNTIME_KEY=/absolute/path/to/runtime-private-key
"$LYCHD_DEPLOY_WORK/controller/bin/python" - \
"$LYCHD_DEPLOY_WORK/source-vars.json" \
"$LYCHD_DEPLOY_WORK/controller/bin/uv" "$LYCHD_SOURCE_REV" <<'PY'
import hashlib
import json
import re
import sys
from pathlib import Path
destination, binary = Path(sys.argv[1]), Path(sys.argv[2]).resolve(strict=True)
revision = sys.argv[3]
if not re.fullmatch(r"[0-9a-f]{40}", revision):
raise SystemExit("Supply the recorded full source SHA")
destination.write_text(json.dumps({
"lychd_source_revision": revision,
"lychd_source_uv_local_binary": str(binary),
"lychd_source_uv_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
"lychd_source_python": "3.14.8",
}, indent=2) + "\n")
destination.chmod(0o600)
PY
ANSIBLE_HOME="$LYCHD_DEPLOY_WORK/ansible" \
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" \
-i "$LYCHD_DEPLOY_WORK/inventory.yml" playbooks/source.yml \
-e "@$LYCHD_DEPLOY_WORK/source-vars.json" \
--private-key "$LYCHD_RUNTIME_KEY" --limit pilot --check
The command assumes the shell remains in deployments/ansible. After reviewing the check,
repeat without --check. Source check mode observes current refusal conditions but does not
clone or install. The first actual preparation can download the explicit managed CPython lane
and locked project dependencies; this is network/package work, not a free-space guarantee.
Repeat it only while the body remains inactive and record the actual result. Keep all model,
credential and private host files outside the source tree, including ignored directories.
For the application image and CLI, follow the same-source build in
Summoning. Prefer building once on a suitable Linux x86_64
workstation from the same recorded SHA, then transfer a verified image archive privately and
load it under each runtime account. Do not build a different moving branch on each VPS or copy
a host .venv between machines. The production build shape is:
podman build --file Containerfile --build-arg "VCS_REF=$LYCHD_SOURCE_REV" \
--tag "localhost/lychd:$LYCHD_SOURCE_REV" .
podman image inspect "localhost/lychd:$LYCHD_SOURCE_REV" \
--format '{{.Id}} {{index .Labels "org.opencontainers.image.revision"}}'
Execute the build in the exact source checkout and record its resolved base/output image IDs.
If transferring an archive, check its SHA-256 before podman load; retain it outside the source
checkout and inspect the loaded image identity. The committed Altar assets are already included;
no desktop or Node/npm installation is needed on the VPS for this path. Tags and source labels
do not prove byte-reproducible images. Do not run CPU benchmarks or continuous inference until
the provider clarifies permission for this shared-core plan.
Application acceptance
After host and same-source image preparation, connect directly as the runtime user and use the installed CLI in its stable versioned path:
# Set the same full SHA recorded on the controller.
LYCHD_SOURCE_REV=FULL_40_CHARACTER_SHA
LYCHD_SOURCE="$HOME/src/lychd-$LYCHD_SOURCE_REV"
LYCHD_CLI="$LYCHD_SOURCE/.venv/bin/lychd"
cd "$LYCHD_SOURCE"
"$LYCHD_CLI" init --web-image "localhost/lychd:$LYCHD_SOURCE_REV" --dry-run
"$LYCHD_CLI" init --web-image "localhost/lychd:$LYCHD_SOURCE_REV"
Inspect the generated private lychd.toml in the effective XDG config root. The pinned image
is part of its original creation digest. Preserve that receipt, identity, private mode and all
generated fields. The bounded profile keeps [extensions] builtins=[] and crypt=[], with no
model or Portal declarations, the generated PostgreSQL profile and Host Reactor actuator, and
the disabled raw SAQ admin UI. Existing settings are preserved by init; a different or edited
configuration requires separate operator reconciliation rather than automated adoption.
Then inspect and apply binding through the same CLI:
"$LYCHD_CLI" bind --dry-run
"$LYCHD_CLI" bind
"$LYCHD_CLI" status --json
Preserve existing settings, generated secrets and datastore ownership; do not rerun initialization to erase an unclear state. Configuration and image identity still need deliberate inspection; command exit success alone is not a health claim.
Before attempting lychd start, establish the separately authorized recovery plan required
by Summoning. Public active-container stop/start is still
incomplete. Do not install an Ansible systemctl restart, podman kill, native web process,
or fake database as a second application lifecycle.
A bounded model-free trial can observe the actual Vessel, PostgreSQL migration, Host Reactor
and queues. Use server-local authenticated reads of /api/v1/altar/status,
/orchestrator/status and /orchestrator/queues; require an unauthenticated protected request
to refuse, working database readback, empty capabilities and observed idle runs/rites.
Obtain the generated local access credential only in a private interactive terminal through
lychd access, never Ansible output or a URL. There is no invented generic /health route.
Do not publish or reverse-proxy the listener. The optional one-owner access candidate preserves loopback publication and authentication through a restricted private road; it still needs installed/browser qualification. General shared remote access remains unsupported. A Portal row or supplied API key does not enable inference: Portal grants refuse until egress admission is implemented. A running model-free body is not a working chat deployment, a complete lifecycle, or a recovery proof.
Automate one host end to end
The combined playbook connects as both the existing administrator and lychd. Select their
separate keys by host and remote user in a private OpenSSH configuration outside the checkout;
a single global Ansible --private-key or inventory ansible_ssh_private_key_file cannot select
both. For example, create $LYCHD_DEPLOY_WORK/ssh_config with mode 0600:
Host 192.0.2.10
IdentitiesOnly yes
StrictHostKeyChecking yes
Match host 192.0.2.10 user existing_admin
IdentityFile /absolute/private/administrator-key
Match host 192.0.2.10 user lychd
IdentityFile /absolute/private/runtime-key
Replace the address with the exact ansible_host and the administrator with the inventory login.
OpenSSH Match user uses the remote username selected
by each play. Pass this file with --ssh-common-args="-F $LYCHD_DEPLOY_WORK/ssh_config" on the
combined command below. Do not also set a global private-key option. Inspect the effective key
selection without connecting:
ssh -G -F "$LYCHD_DEPLOY_WORK/ssh_config" -l existing_admin 192.0.2.10
ssh -G -F "$LYCHD_DEPLOY_WORK/ssh_config" -l lychd 192.0.2.10
The first runtime connection will fail on a fresh host; the administrator must then observe that the account is absent before provisioning starts. An existing runtime account with inaccessible SSH refuses before any host-floor change: resolve its key, host-key or network failure instead of treating it as a fresh install.
playbooks/server.yml is the server entry point and imports deploy.yml, which composes
the steps above for exactly one --limited host. It first
asks the source-admitted CLI for its native deployment assessment through a small protocol-checking
JSON adapter (roles/lychd_app/files/app_probe.py), then admits
only one mode:
| Observed state | Mode |
|---|---|
running: owned units active, settings retain their original init identity and digest with the pinned image selected during init, Vessel and settings agree on an image labelled with the pinned SHA, Reactor bound to the pinned CLI |
read-only verification |
administrator confirms no lychd account, absent, initialized or bound_inactive |
provisioning, only with -e lychd_deploy_target=<host> |
foreign_revision or anything else (unknown) |
stop and print the exact reasons |
Provisioning converges the floor, stages the source, loads the admitted image, then runs
lychd init --web-image localhost/lychd:SHA --dry-run/init --web-image localhost/lychd:SHA,
then runs lychd bind --dry-run/bind. The image is part of the original generated settings and
creation digest; the controller never edits settings or reads lifecycle receipts. It re-classifies
after each step. Existing PostgreSQL data,
secrets and settings with any other change are never adopted, regenerated or overwritten.
lychd start runs only with -e lychd_app_start_authorized=true, after the recovery plan for that
host has been approved. Convergence is then observed read-only; nothing is restarted or killed.
Build or select the campaign image once. Then admit its archive on the controller, which verifies every blob digest, the revision label and, when given, the expected image ID, and records the archive SHA-256:
REV=FULL_40_CHARACTER_SHA
mkdir -p -m 700 "$LYCHD_DEPLOY_WORK/artifacts"
ssh lychd@BUILD_HOST "podman save --format oci-archive localhost/lychd:$REV" \
> "$LYCHD_DEPLOY_WORK/artifacts/lychd-$REV.oci.tar"
"$LYCHD_DEPLOY_WORK/controller/bin/python" -I \
roles/lychd_image/files/admit_oci_archive.py \
"$LYCHD_DEPLOY_WORK/artifacts/lychd-$REV.oci.tar" "$REV" \
"$LYCHD_DEPLOY_WORK/image-vars.json" EXPECTED_64_HEX_IMAGE_ID
Each host's image role refuses a different image under the campaign tag. It checks the transferred
SHA-256 before podman load and the exact image ID and label afterwards. Set
lychd_floor_enable_network_online: true in the host variables. On Tumbleweed Server nothing else
pulls network-online.target into boot, so rootless Podman's
podman-user-wait-network-online.service times out. The floor links the target into
multi-user.target.wants and starts it once; it changes no network configuration.
ANSIBLE_HOME="$LYCHD_DEPLOY_WORK/ansible" \
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" \
-i "$LYCHD_DEPLOY_WORK/inventory.yml" playbooks/server.yml \
-e "@$LYCHD_DEPLOY_WORK/host-vars.yml" -e "@$LYCHD_DEPLOY_WORK/source-vars.json" \
-e "@$LYCHD_DEPLOY_WORK/image-vars.json" \
--ssh-common-args="-F $LYCHD_DEPLOY_WORK/ssh_config" \
--limit NEW_HOST -e lychd_deploy_target=NEW_HOST -K
Repeat the same command with -e lychd_app_start_authorized=true to start the bound body. Then
run it again without either authorization: a healthy host must report mode=verify with no
changes. When image variables are supplied, verification still requires their exact admitted image
ID; a matching source label alone is insufficient. playbooks/app-verify.yml runs the same
acceptance without sudo. Its native assessment reads the local credential through the owned
access service into memory and sends it only to the loopback Altar, using the shared nonambient
HTTP client without redirects. It
requires 401 for anonymous and wrong-credential requests, authenticated status/orchestrator/queue
reads, empty capabilities, idle runs/rites, database tables and a non-superuser runtime role.
It also requires the receipted Phylactery subvolume identity (or an exact mountpoint) and a
loopback-only listener.
On Btrfs, init creates the PostgreSQL data directory as a subvolume and records its ID and
UUID in the version-2 lifecycle receipt. A subvolume is not a mountpoint, so lychd status
reports not an exact mountpoint. That is the expected unmounted-subvolume shape; acceptance
joins the typed receipt authority to the existing isolated live subvolume observer instead.
Native deployment protocol 1
Use matching controller and installed CLI revisions that implement protocol 1. The public machine
entrypoint is lychd status --json --deployment-revision SHA; --deployment-image-id ID pins the
admitted exact image and --deployment-verify adds bounded server-local acceptance. Ordinary
status --json retains its existing shape. The envelope identifies schema=lychd.deployment,
version=1, the observed state, explicit next action, safe refusal codes and acceptance checks.
Only LychD interprets its configuration, ownership, generated bindings and lifecycle state.
A failed or incompatible response stops automation; human output is never parsed.
The profile accepts only original pinned settings for the empty-extension container body. Default-image initialized bodies and the earlier pilot's post-init image-line edit are refused. Keep that pilot's previous private controller for read-only observation; this change does not migrate its settings or rewrite its creation receipt. Reconcile an existing installation through an independently reviewed recovery/migration procedure before adopting it into this profile. An active body remains observation-only, even when all acceptance checks pass. The native assessment owner and focused refusal tests are source evidence; installed host qualification remains a separate receipt.
Repeat on another host
After the pilot passes its declared host and recovery observations, add each fresh Tumbleweed
host and its own IDs/keys to private inventory/host variables. Keep the same admitted source
SHA. Run --check --diff and apply with an explicit --limit; the playbook defaults to serial
one. The initial OS installation remains a separate destructive operation.
Record first apply, second apply, direct-runtime checks and a controlled OS recovery cycle for each new host. A passing pilot is reusable procedure, not evidence that an untouched server is ready. A developer or their coding assistant should read this page and run the same bounded commands; sharing root credentials or private inventories in Discord is unnecessary.