Fedora CoreOS on Proxmox, rootless Podman and Quadlet applications managed with OpenTofu. This configuration is specific to this homelab; adapt storage, networking and accounts before use.
- main.tf, variables.tf: providers, connections and inputs.
- fcos.tf, butane/: Fedora CoreOS VM, first-boot configuration and system services.
- deployment.tf, configs/: application list, Quadlets and configuration.
- storage.tf: existing ZFS datasets exposed through Proxmox Directory Mappings.
- backups.tf: per-cloud PVE backup jobs and file-backup hooks.
- Gatus guide: external monitoring, configuration upload and coverage.
- proxmox-acme.tf: Proxmox certificates through Cloudflare DNS validation.
- Storage guide: ownership, boot and recovery.
Applications run as homelab through the Quadlet provider.
Traefik uses socket activation; SELinux and a default-deny nftables firewall remain enabled.
Alloy sends telemetry to VictoriaMetrics/VictoriaLogs/VictoriaTraces; Grafana displays it.
Gatus monitors availability externally.
- Prepare Proxmox API/SSH access and the existing storage. Terraform does not create or format the application pool, zvol or filesystem.
- Configure all infrastructure addresses in
network_config, then DNS, CA trust and required Bitwarden, Cloudflare, OIDC and backup accounts. - Use the OpenTofu version required by
main.tf. Build the Bitwarden fork withgo build -o bin/ .in its checkout and point a local.terraformrcdevelopment override atbin/. - Copy
terraform.tfvars.exampleto ignoredterraform.tfvars, fill in the values and restrict permissions withchmod 600 terraform.tfvars. - Load the Proxmox SSH key into the agent. Set Fedora CoreOS admin keys in
fcos_config.ssh_authorized_keys.admin, deployment public keys infcos_config.ssh_authorized_keys.applications, and the deployment private-key path indeployment_config.ssh_private_key_path.
export TF_CLI_CONFIG_FILE="$PWD/.terraformrc"
tofu init
tofu validate
tofu plan
tofu applyIgnition configures Fedora CoreOS on first boot; the Quadlet provider then installs application files and secrets over SSH. Uploading new Ignition does not reconfigure a running host.
The Quadlet connection currently disables SSH host-key verification: the server's identity is not checked. Use separate admin and deployment keys.
core: administrator with sudo, UID/GID1001:1001.homelab: applications without sudo, UID/GID1000:1000, subuid/subgid524288:65536.- Podman storage:
/var/mnt/docker/homelab; application data:/var/mnt/docker/app_data. - Blog uploads: user
homelab, directory/var/mnt/docker/blog.
Keep Terraform state and review the replacement while the current VM is reachable:
tofu plan -replace=proxmox_virtual_environment_vm.fcosApply with the same -replace argument only after review. The new VM receives
Ignition, reattaches the separate data storage and redeploys applications.
Each entry in applications in deployment.tf owns its files and activation:
| Field | Purpose |
|---|---|
paths |
Files/directories under configs, including unit drop-ins. |
restart |
Units to start or restart. |
try_restart |
Restart only if already active. |
enable |
Native units enabled at boot; Quadlets use [Install]. |
secrets |
Secret names whose rotation activates this application. |
Add files under configs, declare their owner and activation, then review and apply.
Rendered paths are relative to homelab's ~/.config; .tftpl is stripped.
- Pods normally restart
<name>-pod.service; containers join withPod=<name>.pod. Pods and standalone containers use[Install] WantedBy=default.targetfor startup. - Shared proxy-network consumers use
systemd-reverse-proxyand require/order afterreverse-proxy-network.service. - Traefik activates through sockets; OpenCloud uses a native target and extension-update timer.
- Shared container defaults are copied to each container. Editing them activates all applications with containers.
- Restart completion is not a readiness check.
CrowdSec reads Traefik logs through http://victoria:8427/vl/ using its dedicated token.
See acquisition and vmauth access rules.
Gatus runs on a separate VPS with its
configuration at /home/docker-user/gatus/config/config.yml. Service URLs use
site_config.base_domain; gatus_config supplies the interval, output directory,
Telegram chat ID and OAuth2 client ID. Check definitions and announcements live
in the template. secret_config.gatus contains the Bitwarden IDs and revisions
for the Telegram token, OAuth2 client secret, Remnawave API token, subscription path
and Healthchecks ping URL. The Bitwarden provider reads these secrets as one ephemeral batch.
Render locally using OpenTofu:
tofu apply -target=terraform_data.gatus_config
# Force a fresh render, for example after deleting the local file:
tofu apply -target=terraform_data.gatus_config -replace=terraform_data.gatus_configThe default output directory is ignored by Git; the rendered file has mode 0600.
gatus_config_path outputs its absolute path. A native terraform_data provisioner
writes the file atomically with printf, receiving ephemeral content through its
environment. Secret values and rendered content are absent from plan and state.
Normal applies render when the template, settings or secret references/revisions
change. After rotating a Gatus secret, bump its secret_config.gatus.<name>.revision.
This does not upload or restart Gatus on the VPS.
See upload commands and
Gatus coverage for monitored applications and remaining gaps.
Application secrets use ephemeral Bitwarden reads and write-only Podman
resource arguments. After rotation, bump secret_config.podman.<name>.revision
and apply. Names use hyphens; list shared secrets in every consumer's secrets field.
For the ACME token, bump secret_config.proxmox_acme_cloudflare_token.revision.
Renovate proposes image/provider updates. Selected images use
AutoUpdate=registry; OpenCloud extensions have a daily update timer.
Two PVE jobs back up Fedora CoreOS and then personal/observability:
Storj at 06:00 and Backblaze at 07:00, PVE local time. OpenTofu manages the jobs
and their file-backup hooks. Logs and results are in PVE Tasks.
Automated restore verification remains to be configured. See backup boundaries.
- Consider Flatcar Linux.
- Harden network configuration.
- Add
hashicorp/assertsupport.