feat: initial commit

krasovs.ky 4a0521da

+47
.gitignore
···
+
### Terraform template
+
# Local .terraform directories
+
**/.terraform/*
+
+
# .tfstate files
+
*.tfstate
+
*.tfstate.*
+
+
# Crash log files
+
crash.log
+
crash.*.log
+
+
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
+
# password, private keys, and other secrets. These should not be part of version
+
# control as they are data points which are potentially sensitive and subject
+
# to change depending on the environment.
+
*.tfvars
+
*.tfvars.json
+
+
# Ignore override files as they are usually used to override resources locally and so
+
# are not checked in
+
override.tf
+
override.tf.json
+
*_override.tf
+
*_override.tf.json
+
+
# Ignore transient lock info files created by terraform apply
+
.terraform.tfstate.lock.info
+
+
# Include override files you do wish to add to version control using negated pattern
+
# !example_override.tf
+
+
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+
# example: *tfplan*
+
+
# Ignore CLI configuration files
+
.terraformrc
+
terraform.rc
+
+
# JetBrains
+
.idea/
+
+
# Bitwarden Secrets Manager
+
.bitwarden/
+
+
# Fedora CoreOS image
+
fedora-coreos.qcow2.img
+96
.terraform.lock.hcl
···
+
# This file is maintained automatically by "tofu init".
+
# Manual edits may be lost in future updates.
+
+
provider "registry.opentofu.org/bpg/proxmox" {
+
version = "0.71.0"
+
constraints = "0.71.0"
+
hashes = [
+
"h1:SYI+oUlQMl17eiCjN1eqDxSryCSM2KNSV4jPAI+OPA8=",
+
"zh:17349fbd8a4bb2254cf63bde47b3ef451977de9619dd9b4a22d765350ebe2534",
+
"zh:1d4f43063e9c6a37106cd362da142e9555baed4635b25bccfdd15236b592661d",
+
"zh:410adc1eb2ae06153875da9ab0ca30d35331b325735a207accf8f39d3e5e7c99",
+
"zh:471c85f21f8b944370e3da47d307477beb671a192ffe24556cb7bfdf314b846b",
+
"zh:6af532cad44b90c78c64c938c6d8e3bca1f119ca06d6001c2ec0747df3bd6d73",
+
"zh:6ea1675e542e496753e2458254c8bdb1140a2b8c8b7b94127278f6f271809ffa",
+
"zh:bd21a53fd63021453204348f5ab4b3cef380ac2b1b4a83ddcdd41e2e94dcf30f",
+
"zh:c206b23b337cfcea9ad3bce901a076310ca4dce5b6d9335cbf8b141f67e83be3",
+
"zh:cd1f24e7f991716af25b66ede9c58977240beb750477f940e3ba14fc4f63a8ce",
+
"zh:cf12a04f3e51b83d01746c3a6cba47a5b3c2d1123d216d8989e742657d119b17",
+
"zh:ebf5ae64aa7a9807886d219862a1a5024fdad76d3bc56c8aad719bc4506c5893",
+
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
+
"zh:fcabd481fb507b47611533eb4d0db30ff182d95c64cd8707cc62eb12a1f0a8d2",
+
"zh:fef0e97e8ce1efcd4abda83aee48fdeba9fd9e7d57599dd6044bbc41cf9c32bb",
+
"zh:ff155828a40181b821b9affaa8af0273f05515407376a2d4a323d8442ffadc04",
+
]
+
}
+
+
provider "registry.opentofu.org/hashicorp/local" {
+
version = "2.5.2"
+
constraints = "2.5.2"
+
hashes = [
+
"h1:BUewjbhAQWuGHH36SozCTuESFJhbiHMaCFLnVVNZ1Es=",
+
"zh:25b95b76ceaa62b5c95f6de2fa6e6242edbf51e7fc6c057b7f7101aa4081f64f",
+
"zh:3c974fdf6b42ca6f93309cf50951f345bfc5726ec6013b8832bcd3be0eb3429e",
+
"zh:5de843bf6d903f5cca97ce1061e2e06b6441985c68d013eabd738a9e4b828278",
+
"zh:86beead37c7b4f149a54d2ae633c99ff92159c748acea93ff0f3603d6b4c9f4f",
+
"zh:8e52e81d3dc50c3f79305d257da7fde7af634fed65e6ab5b8e214166784a720e",
+
"zh:9882f444c087c69559873b2d72eec406a40ede21acb5ac334d6563bf3a2387df",
+
"zh:a4484193d110da4a06c7bffc44cc6b61d3b5e881cd51df2a83fdda1a36ea25d2",
+
"zh:a53342426d173e29d8ee3106cb68abecdf4be301a3f6589e4e8d42015befa7da",
+
"zh:d25ef2aef6a9004363fc6db80305d30673fc1f7dd0b980d41d863b12dacd382a",
+
"zh:fa2d522fb323e2121f65b79709fd596514b293d816a1d969af8f72d108888e4c",
+
]
+
}
+
+
provider "registry.opentofu.org/hashicorp/null" {
+
version = "3.2.3"
+
constraints = "3.2.3"
+
hashes = [
+
"h1:ZD7F/BQPzRy/smJgSwnDs0vrqstk71sx2p0qtUcc/iU=",
+
"zh:1d57d25084effd3fdfd902eca00020b34b1fb020253b84d7dd471301606015ac",
+
"zh:65b7f9799b88464d9c2ec529713b7f52ea744275b61a8dc86cdedab1b2dcb933",
+
"zh:80d3e9c95b7b4ae7c54005cd127cae82e5c53d2b7023ef24c147337bac9dadd9",
+
"zh:841b60c07683e4bf456799ccd718896fdafdcc2c49252ae09967f2e74d8c8a03",
+
"zh:8fa1c592a9c78222e35713c6edb3f1f818a4c6f3524a30a209f0a7e919827b68",
+
"zh:bb795cc1429e09466840c09d39a28edf1db5070b1ec76822fc1173906a264572",
+
"zh:da1784818a89bea29dfe660632f0060a7a843e4e564d74435fbeca002b0f7d2a",
+
"zh:f409bf21b1cdaa6dac47cd79806f3d93f67e9507fe4dbf33b0165335f53bc2e1",
+
"zh:fbea7a1ff84b430ba9594698e93196d81d03e4036de3d1cafccb2a96d5b38581",
+
"zh:fbf0c84663a7e85881388d7d71ac862184f05fbf2d17ecf76bc5d3d7503ea260",
+
]
+
}
+
+
provider "registry.opentofu.org/maxlaverse/bitwarden" {
+
version = "0.13.0"
+
constraints = "0.13.0"
+
hashes = [
+
"h1:cQJu6KCKHp32XpNJ3k3rre1NRG/Xyb3B81OvrtH0Gqs=",
+
"zh:055ccac1783ea875112b16592f6d6a0be265ab18b42553965077f1ac8dcf720d",
+
"zh:0e4c218199296bcf06d540ce6b017804233458a7e73ca1eecf886123079b9ed5",
+
"zh:0e85ed93f3a0047a77012b1bd8dbcb4dca6dd1690a2ac9c071e6fb917ab8dbd6",
+
"zh:2ffbce321f69bdad6510b0e9382031c127ab54b9e2f986b9ae6009ebd26cd6ff",
+
"zh:31139f4ba2d5c052294bd9169c0372c3b03de62db1a49bae8f86450660cd5ce0",
+
"zh:3e28c57d9a8eba0b03f1f8ff56ed12b0fc570622040e517d57404766b98327f4",
+
"zh:6a23689b1a1003c1f2ddd2c40f73b00b71b111fd8affbc23166bfc6d0c109411",
+
"zh:785c9b91e6e13b63afca73c694186e16a0fa8a24420a120d733e46a841b2e38c",
+
"zh:7e23c36b8d9875d7f0b93355d4bb816444eac6f9a022b2943192714e6dc58f46",
+
"zh:b565e3ceb4d6d6bce62914bd3c9ac2cbbe963f730473b2cee8d338941bf0a935",
+
"zh:b74969471bc69814c9c1705a0c145c654d976293049e69dc400d59a8aea9d3d1",
+
"zh:f1a3237ada5d276fc48bd49db40b2a2b7291ac1544a751cd8fb1f15ea7ff1db2",
+
"zh:f4e469f814aef71d9da864df4033ed3b0af11b2b797f1cfd38324d92c2cf717d",
+
"zh:fefab6a96dea8f0f52eda8a4b4436f949e4c97db076b674521b89c95cd6193fe",
+
]
+
}
+
+
provider "registry.opentofu.org/poseidon/ct" {
+
version = "0.13.0"
+
constraints = "0.13.0"
+
hashes = [
+
"h1:jZusJZjbV+TZ2lxKaVopvRRrKDWAb2Sq1AUEtgI2xIE=",
+
"zh:24d86adcba92ad0f13870d5e0d217c395aa90ff1e9234fe0c9b7c6eb65abb3a8",
+
"zh:317eeadf92d220fe546be624a9002190edeb623ac76ae7f6a93abd9fe1be65fd",
+
"zh:361dbff802ccbd94b87c9d77c0d9db9bdf4d5d408f8cf05e4dae203e60b310ca",
+
"zh:3b25cb8a0327886aa30c273561ecea3315cc4d729677cd6528ed1339486475da",
+
"zh:63455a68fee4ba0c9b131eb7e267eb17707184c55a5feb9e2bad2d9de5889d6a",
+
]
+
}
+201
LICENSE
···
+
Apache License
+
Version 2.0, January 2004
+
http://www.apache.org/licenses/
+
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+
1. Definitions.
+
+
"License" shall mean the terms and conditions for use, reproduction,
+
and distribution as defined by Sections 1 through 9 of this document.
+
+
"Licensor" shall mean the copyright owner or entity authorized by
+
the copyright owner that is granting the License.
+
+
"Legal Entity" shall mean the union of the acting entity and all
+
other entities that control, are controlled by, or are under common
+
control with that entity. For the purposes of this definition,
+
"control" means (i) the power, direct or indirect, to cause the
+
direction or management of such entity, whether by contract or
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
+
outstanding shares, or (iii) beneficial ownership of such entity.
+
+
"You" (or "Your") shall mean an individual or Legal Entity
+
exercising permissions granted by this License.
+
+
"Source" form shall mean the preferred form for making modifications,
+
including but not limited to software source code, documentation
+
source, and configuration files.
+
+
"Object" form shall mean any form resulting from mechanical
+
transformation or translation of a Source form, including but
+
not limited to compiled object code, generated documentation,
+
and conversions to other media types.
+
+
"Work" shall mean the work of authorship, whether in Source or
+
Object form, made available under the License, as indicated by a
+
copyright notice that is included in or attached to the work
+
(an example is provided in the Appendix below).
+
+
"Derivative Works" shall mean any work, whether in Source or Object
+
form, that is based on (or derived from) the Work and for which the
+
editorial revisions, annotations, elaborations, or other modifications
+
represent, as a whole, an original work of authorship. For the purposes
+
of this License, Derivative Works shall not include works that remain
+
separable from, or merely link (or bind by name) to the interfaces of,
+
the Work and Derivative Works thereof.
+
+
"Contribution" shall mean any work of authorship, including
+
the original version of the Work and any modifications or additions
+
to that Work or Derivative Works thereof, that is intentionally
+
submitted to Licensor for inclusion in the Work by the copyright owner
+
or by an individual or Legal Entity authorized to submit on behalf of
+
the copyright owner. For the purposes of this definition, "submitted"
+
means any form of electronic, verbal, or written communication sent
+
to the Licensor or its representatives, including but not limited to
+
communication on electronic mailing lists, source code control systems,
+
and issue tracking systems that are managed by, or on behalf of, the
+
Licensor for the purpose of discussing and improving the Work, but
+
excluding communication that is conspicuously marked or otherwise
+
designated in writing by the copyright owner as "Not a Contribution."
+
+
"Contributor" shall mean Licensor and any individual or Legal Entity
+
on behalf of whom a Contribution has been received by Licensor and
+
subsequently incorporated within the Work.
+
+
2. Grant of Copyright License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
copyright license to reproduce, prepare Derivative Works of,
+
publicly display, publicly perform, sublicense, and distribute the
+
Work and such Derivative Works in Source or Object form.
+
+
3. Grant of Patent License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
(except as stated in this section) patent license to make, have made,
+
use, offer to sell, sell, import, and otherwise transfer the Work,
+
where such license applies only to those patent claims licensable
+
by such Contributor that are necessarily infringed by their
+
Contribution(s) alone or by combination of their Contribution(s)
+
with the Work to which such Contribution(s) was submitted. If You
+
institute patent litigation against any entity (including a
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
+
or a Contribution incorporated within the Work constitutes direct
+
or contributory patent infringement, then any patent licenses
+
granted to You under this License for that Work shall terminate
+
as of the date such litigation is filed.
+
+
4. Redistribution. You may reproduce and distribute copies of the
+
Work or Derivative Works thereof in any medium, with or without
+
modifications, and in Source or Object form, provided that You
+
meet the following conditions:
+
+
(a) You must give any other recipients of the Work or
+
Derivative Works a copy of this License; and
+
+
(b) You must cause any modified files to carry prominent notices
+
stating that You changed the files; and
+
+
(c) You must retain, in the Source form of any Derivative Works
+
that You distribute, all copyright, patent, trademark, and
+
attribution notices from the Source form of the Work,
+
excluding those notices that do not pertain to any part of
+
the Derivative Works; and
+
+
(d) If the Work includes a "NOTICE" text file as part of its
+
distribution, then any Derivative Works that You distribute must
+
include a readable copy of the attribution notices contained
+
within such NOTICE file, excluding those notices that do not
+
pertain to any part of the Derivative Works, in at least one
+
of the following places: within a NOTICE text file distributed
+
as part of the Derivative Works; within the Source form or
+
documentation, if provided along with the Derivative Works; or,
+
within a display generated by the Derivative Works, if and
+
wherever such third-party notices normally appear. The contents
+
of the NOTICE file are for informational purposes only and
+
do not modify the License. You may add Your own attribution
+
notices within Derivative Works that You distribute, alongside
+
or as an addendum to the NOTICE text from the Work, provided
+
that such additional attribution notices cannot be construed
+
as modifying the License.
+
+
You may add Your own copyright statement to Your modifications and
+
may provide additional or different license terms and conditions
+
for use, reproduction, or distribution of Your modifications, or
+
for any such Derivative Works as a whole, provided Your use,
+
reproduction, and distribution of the Work otherwise complies with
+
the conditions stated in this License.
+
+
5. Submission of Contributions. Unless You explicitly state otherwise,
+
any Contribution intentionally submitted for inclusion in the Work
+
by You to the Licensor shall be under the terms and conditions of
+
this License, without any additional terms or conditions.
+
Notwithstanding the above, nothing herein shall supersede or modify
+
the terms of any separate license agreement you may have executed
+
with Licensor regarding such Contributions.
+
+
6. Trademarks. This License does not grant permission to use the trade
+
names, trademarks, service marks, or product names of the Licensor,
+
except as required for reasonable and customary use in describing the
+
origin of the Work and reproducing the content of the NOTICE file.
+
+
7. Disclaimer of Warranty. Unless required by applicable law or
+
agreed to in writing, Licensor provides the Work (and each
+
Contributor provides its Contributions) on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+
implied, including, without limitation, any warranties or conditions
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+
PARTICULAR PURPOSE. You are solely responsible for determining the
+
appropriateness of using or redistributing the Work and assume any
+
risks associated with Your exercise of permissions under this License.
+
+
8. Limitation of Liability. In no event and under no legal theory,
+
whether in tort (including negligence), contract, or otherwise,
+
unless required by applicable law (such as deliberate and grossly
+
negligent acts) or agreed to in writing, shall any Contributor be
+
liable to You for damages, including any direct, indirect, special,
+
incidental, or consequential damages of any character arising as a
+
result of this License or out of the use or inability to use the
+
Work (including but not limited to damages for loss of goodwill,
+
work stoppage, computer failure or malfunction, or any and all
+
other commercial damages or losses), even if such Contributor
+
has been advised of the possibility of such damages.
+
+
9. Accepting Warranty or Additional Liability. While redistributing
+
the Work or Derivative Works thereof, You may choose to offer,
+
and charge a fee for, acceptance of support, warranty, indemnity,
+
or other liability obligations and/or rights consistent with this
+
License. However, in accepting such obligations, You may act only
+
on Your own behalf and on Your sole responsibility, not on behalf
+
of any other Contributor, and only if You agree to indemnify,
+
defend, and hold each Contributor harmless for any liability
+
incurred by, or claims asserted against, such Contributor by reason
+
of your accepting any such warranty or additional liability.
+
+
END OF TERMS AND CONDITIONS
+
+
APPENDIX: How to apply the Apache License to your work.
+
+
To apply the Apache License to your work, attach the following
+
boilerplate notice, with the fields enclosed by brackets "[]"
+
replaced with your own identifying information. (Don't include
+
the brackets!) The text should be enclosed in the appropriate
+
comment syntax for the file format. We also recommend that a
+
file or class name and description of purpose be included on the
+
same "printed page" as the copyright notice for easier
+
identification within third-party archives.
+
+
Copyright [yyyy] [name of copyright owner]
+
+
Licensed under the Apache License, Version 2.0 (the "License");
+
you may not use this file except in compliance with the License.
+
You may obtain a copy of the License at
+
+
http://www.apache.org/licenses/LICENSE-2.0
+
+
Unless required by applicable law or agreed to in writing, software
+
distributed under the License is distributed on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
See the License for the specific language governing permissions and
+
limitations under the License.
+17
README.md
···
+
# Experimental Homelab
+
+
- Uses immutable, atomic OS provisioned on Proxmox VE node as base.
+
- Uses rootless Podman instead of rootful Docker.
+
- Uses Quadlets systemd-like containers instead of Docker Compose.
+
- VM can be fully removed and restored within 3 minutes, including containers autostart.
+
- Source IP is preserved using [systemd socket activation](https://github.com/eriksjolund/podman-networking-docs?tab=readme-ov-file#socket-activation-systemd-user-service) mechanism.
+
- Native network performance for the reason above.
+
- Stores Podman and applications data on dedicated iSCSI disk.
+
- Stores media and downloads on NFS share.
+
- SELinux support.
+
+
## Future plans
+
+
I would like to switch to Flatcar Linux, but for now it doesn't include `i915` kernel driver
+
which is a dealbreaker for me now. But it's [already merged](https://github.com/flatcar/scripts/pull/2349)
+
and will be soon available in Alpha channel.
+273
butane/fcos.yml.tftpl
···
+
variant: fcos
+
version: 1.5.0
+
+
passwd:
+
users:
+
- name: core
+
ssh_authorized_keys:
+
- ${ssh_key}
+
+
storage:
+
directories:
+
- path: /var/home/core/.config
+
user:
+
name: core
+
group:
+
name: core
+
+
# Quadlets dir
+
- path: /var/home/core/.config/containers
+
user:
+
name: core
+
group:
+
name: core
+
- path: /var/home/core/.config/containers/systemd
+
user:
+
name: core
+
group:
+
name: core
+
+
# Systemd user dir
+
- path: /var/home/core/.config/systemd
+
user:
+
name: core
+
group:
+
name: core
+
- path: /var/home/core/.config/systemd/user
+
user:
+
name: core
+
group:
+
name: core
+
+
links:
+
# Enable Podman socket for Traefik
+
- path: /var/home/core/.config/systemd/user/timers.target.wants/podman.socket
+
target: /usr/lib/systemd/user/podman.socket
+
user:
+
name: core
+
group:
+
name: core
+
+
# Enable http and https sockets for traefik
+
- path: /var/home/core/.config/systemd/user/timers.target.wants/http.socket
+
target: /var/home/core/.config/systemd/user/http.socket
+
user:
+
name: core
+
group:
+
name: core
+
- path: /var/home/core/.config/systemd/user/timers.target.wants/https.socket
+
target: /var/home/core/.config/systemd/user/https.socket
+
user:
+
name: core
+
group:
+
name: core
+
+
# Enable Podman auto updates
+
- path: /var/home/core/.config/systemd/user/timers.target.wants/podman-auto-update.timer
+
target: /usr/lib/systemd/user/podman-auto-update.timer
+
user:
+
name: core
+
group:
+
name: core
+
+
files:
+
# Shared network for all published services
+
- path: /var/home/core/.config/containers/systemd/reverse-proxy.network
+
contents:
+
inline: |
+
[Network]
+
user:
+
name: core
+
group:
+
name: core
+
+
# http and https sockets for traefik
+
- path: /var/home/core/.config/systemd/user/http.socket
+
contents:
+
inline: |
+
[Socket]
+
ListenStream=${ip}:8080
+
FileDescriptorName=web
+
Service=traefik.service
+
+
[Install]
+
WantedBy=sockets.target
+
user:
+
name: core
+
group:
+
name: core
+
- path: /var/home/core/.config/systemd/user/https.socket
+
contents:
+
inline: |
+
[Socket]
+
ListenStream=${ip}:8443
+
FileDescriptorName=websecure
+
Service=traefik.service
+
+
[Install]
+
WantedBy=sockets.target
+
user:
+
name: core
+
group:
+
name: core
+
+
- path: /etc/containers/storage.conf
+
contents:
+
inline: |
+
[storage]
+
driver = "overlay"
+
rootless_storage_path = "/var/mnt/docker/$USER"
+
+
# Quadlets block
+
%{ for name, content in quadlets ~}
+
- path: /var/home/core/.config/containers/systemd/${name}.container
+
contents:
+
inline: |
+
${indent(10, content)}
+
user:
+
name: core
+
group:
+
name: core
+
%{ endfor ~}
+
+
# Enable linger so containers can continue to run even after core user logouts
+
- path: /var/lib/systemd/linger/core
+
+
# Set machine hostname
+
- path: /etc/hostname
+
contents:
+
inline: ${hostname}
+
+
# Configure iSCSI target
+
- path: /etc/iscsi/iscsid.conf
+
overwrite: true
+
contents:
+
inline: |
+
node.startup = automatic
+
isns.address = ${truenas_ip}
+
isns.port = 3260
+
+
# Import Step CA root certificate
+
- path: /etc/pki/ca-trust/source/anchors/step-online-ca.pem
+
contents:
+
inline: |
+
${indent(10, root_ca)}
+
+
# Enable zram swap
+
- path: /etc/systemd/zram-generator.conf
+
contents:
+
inline: |
+
[zram0]
+
+
systemd:
+
units:
+
- name: install-additional-software.service
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Additional software installer
+
Wants=network-online.target
+
After=network-online.target
+
Before=zincati.service
+
ConditionPathExists=!/var/lib/%N.stamp
+
+
[Service]
+
Type=oneshot
+
RemainAfterExit=yes
+
ExecStart=/usr/bin/rpm-ostree install --allow-inactive --assumeyes --reboot qemu-guest-agent unzip intel-gpu-tools podman-compose
+
ExecStart=/bin/touch /var/lib/%N.stamp
+
+
[Install]
+
WantedBy=multi-user.target
+
+
- name: iscsi.service
+
enabled: true
+
+
- name: attach-iscsi-disk.service
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Attach iSCSI disk
+
ConditionFirstBoot=yes
+
Wants=network-online.target
+
After=network-online.target iscsi.service
+
+
[Service]
+
Type=oneshot
+
RemainAfterExit=yes
+
ExecStart=/usr/sbin/iscsiadm -m discovery -t sendtargets -p ${truenas_ip}
+
ExecStart=/usr/sbin/iscsiadm -m node -T ${truenas_iqn} -p ${truenas_ip} --login
+
ExecStart=/usr/sbin/lvmdevices --adddev /dev/sda
+
ExecStart=/usr/sbin/vgchange -ay
+
+
[Install]
+
WantedBy=multi-user.target
+
+
- name: var-mnt-docker.mount
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Mount docker directory
+
Before=remote-fs.target
+
Wants=network-online.target iscsi.service
+
+
[Mount]
+
What=/dev/vg0/lv0
+
Where=/var/mnt/docker
+
Type=xfs
+
Options=_netdev
+
+
[Install]
+
WantedBy=remote-fs.target
+
+
- name: podman-fix-selinux-context.service
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Fix SELinux context for Podman storage
+
Requires=var-mnt-docker.mount
+
After=var-mnt-docker.mount
+
ConditionPathExists=!/var/lib/%N.stamp
+
+
[Service]
+
Type=oneshot
+
RemainAfterExit=yes
+
ExecStart=/usr/bin/chcon -R -t container_file_t /var/mnt/docker/core
+
ExecStart=/bin/touch /var/lib/%N.stamp
+
+
[Install]
+
WantedBy=multi-user.target
+
+
- name: var-mnt-media.mount
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Mount media directory
+
Before=remote-fs.target
+
+
[Mount]
+
What=${truenas_ip}:/mnt/spool/media
+
Where=/var/mnt/media
+
Type=nfs
+
+
[Install]
+
WantedBy=remote-fs.target
+
+
- name: var-mnt-personal.mount
+
enabled: true
+
contents: |
+
[Unit]
+
Description=Mount personal directory
+
Before=remote-fs.target
+
+
[Mount]
+
What=${truenas_ip}:/mnt/spool/personal
+
Where=/var/mnt/personal
+
Type=nfs
+
+
[Install]
+
WantedBy=remote-fs.target
+
+
kernel_arguments:
+
should_exist:
+
- pcie_aspm.policy=powersupersave ip=${ip}::${gateway}:${mask}::enp6s18:none:${nameserver}
+24
fcos-stable-qcow2.tf
···
+
resource "null_resource" "fcos_qcow2" {
+
provisioner "local-exec" {
+
command = "mv $(docker run --security-opt label=disable --pull=always --rm -v .:/data -w /data quay.io/coreos/coreos-installer:release download -p qemu -f qcow2.xz -s stable -a x86_64 -d) fedora-coreos.qcow2.img"
+
interpreter = ["PowerShell", "-Command"]
+
}
+
+
provisioner "local-exec" {
+
when = destroy
+
command = "rm -f fedora-coreos.qcow2.img"
+
interpreter = ["PowerShell", "-Command"]
+
}
+
}
+
+
resource "proxmox_virtual_environment_file" "fcos_qcow2" {
+
content_type = "iso"
+
datastore_id = "local"
+
node_name = "pve"
+
+
depends_on = [null_resource.fcos_qcow2]
+
+
source_file {
+
path = "fedora-coreos.qcow2.img"
+
}
+
}
+130
fcos.tf
···
+
locals {
+
butane_config = merge(var.fcos_config, {
+
quadlets = local.quadlets
+
})
+
+
# Get a list of all files in the specified directory
+
quadlet_paths = fileset(path.module, "quadlets/*")
+
quadlets = {
+
for path in local.quadlet_paths :
+
replace(basename(path), ".container.tftpl", "") => templatefile(path, var.quadlets_config)
+
}
+
+
# Bitwarden Secret Manager Secret IDs
+
secrets = {
+
traefik-cf-dns-api-token = "e9e0f0f0-abc8-4bde-b05f-b292018179bb"
+
oauth2-proxy-cookie-secret = "289c0832-27c2-463b-97b7-b29200a8cebd"
+
oauth2-proxy-client-secret = "afdb8ef2-a3d4-4a17-b839-b29200ab6f87"
+
pocket-id-maxmind-license-key = "08c549a4-bf48-4998-8cb0-b29200ac845d"
+
actual-budget-openid-client-secret = "5754702b-d9d5-4127-b5ab-b29200abdd6a"
+
open-webui-oauth-client-secret = "b595040b-a23a-44af-8bff-b29200ad6258"
+
}
+
+
init_script_path = "${path.module}/scripts/init_fcos.sh.tftpl"
+
}
+
+
data "ct_config" "fcos_ignition" {
+
content = templatefile("${path.module}/butane/fcos.yml.tftpl", local.butane_config)
+
strict = true
+
}
+
+
resource "proxmox_virtual_environment_vm" "fcos" {
+
node_name = "pve"
+
name = "fcos"
+
description = "Managed by OpenTofu"
+
+
# Use modern platform
+
machine = "q35"
+
bios = "ovmf"
+
+
startup {
+
order = 11
+
}
+
+
cpu {
+
cores = 4
+
}
+
+
memory {
+
dedicated = 16384
+
floating = 16384
+
}
+
+
efi_disk {
+
datastore_id = "local-zfs"
+
type = "4m"
+
}
+
+
disk {
+
interface = "virtio0"
+
datastore_id = "local-zfs"
+
file_id = proxmox_virtual_environment_file.fcos_qcow2.id
+
size = 32
+
}
+
+
tpm_state {
+
datastore_id = "local-zfs"
+
}
+
+
network_device {
+
bridge = "vmbr0"
+
vlan_id = 100
+
mac_address = var.fcos_config.mac_address
+
}
+
+
# Linux 6.x
+
operating_system {
+
type = "l26"
+
}
+
+
# Intel A380 video
+
hostpci {
+
device = "hostpci0"
+
id = "0000:03:00.0"
+
pcie = true
+
rombar = true
+
}
+
+
# Intel A380 audio
+
hostpci {
+
device = "hostpci1"
+
id = "0000:04:00.0"
+
pcie = true
+
rombar = true
+
}
+
+
agent {
+
enabled = true
+
}
+
+
kvm_arguments = "-fw_cfg 'name=opt/com.coreos/config,string=${replace(data.ct_config.fcos_ignition.rendered, ",", ",,")}'"
+
}
+
+
resource "null_resource" "fcos_provision_secrets" {
+
depends_on = [proxmox_virtual_environment_vm.fcos]
+
+
triggers = {
+
checksum = sha256(file(local.init_script_path))
+
}
+
+
connection {
+
type = "ssh"
+
user = "core"
+
agent = true
+
host = var.fcos_config.ip
+
}
+
+
provisioner "file" {
+
destination = "/tmp/init.sh"
+
content = templatefile(local.init_script_path, {
+
bws_access_token: var.bws_access_token
+
quadlets: local.quadlets
+
secrets: local.secrets
+
})
+
}
+
+
provisioner "remote-exec" {
+
inline = ["sh /tmp/init.sh"]
+
on_failure = fail
+
}
+
}
+48
main.tf
···
+
terraform {
+
required_providers {
+
proxmox = {
+
source = "bpg/proxmox"
+
version = "0.71.0"
+
}
+
bitwarden = {
+
source = "maxlaverse/bitwarden"
+
version = "0.13.0"
+
}
+
ct = {
+
source = "poseidon/ct"
+
version = "0.13.0"
+
}
+
null = {
+
source = "hashicorp/null"
+
version = "3.2.3"
+
}
+
local = {
+
source = "hashicorp/local"
+
version = "2.5.2"
+
}
+
}
+
}
+
+
provider "bitwarden" {
+
access_token = var.bws_access_token
+
experimental {
+
embedded_client = true
+
}
+
}
+
+
data "bitwarden_secret" "proxmox_password" {
+
id = var.proxmox_config.password_secret_id
+
}
+
+
provider "proxmox" {
+
endpoint = var.proxmox_config.endpoint
+
insecure = true
+
+
// Unfortunately Proxmox can execute a lot of actions only under root user...
+
username = "root@pam"
+
password = data.bitwarden_secret.proxmox_password.value
+
+
ssh {
+
agent = true
+
}
+
}
+24
quadlets/actual-budget.container.tftpl
···
+
[Unit]
+
Description=Actual Budget Quadlet
+
+
[Container]
+
Image=docker.io/actualbudget/actual-server:latest
+
AutoUpdate=registry
+
ContainerName=actual-budget
+
+
User=1000:1000
+
UIDMap=+1000:@1000:1
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.actual-budget.rule=Host(`actual.${base_domain}`)"
+
+
Volume=/var/mnt/docker/app_data/actual-budget:/data:Z
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+41
quadlets/oauth2-proxy.container.tftpl
···
+
[Unit]
+
Description=OAuth2 Proxy Quadlet
+
# OAuth2 Proxy requests OIDC configuration after launch, Pocket-ID should be ready
+
Wants=pocket-id.service
+
After=pocket-id.service
+
+
[Container]
+
Image=quay.io/oauth2-proxy/oauth2-proxy:latest
+
AutoUpdate=registry
+
ContainerName=oauth2-proxy
+
+
User=1000:1000
+
+
Environment=OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
+
Environment=OAUTH2_PROXY_PROVIDER=oidc
+
Environment=OAUTH2_PROXY_OIDC_ISSUER_URL=https://id.${base_domain}
+
Environment=OAUTH2_PROXY_EMAIL_DOMAINS=*
+
Environment=OAUTH2_PROXY_CLIENT_ID=643ae98a-24a1-4c1d-9d0a-a102dd2fe38c
+
Environment=OAUTH2_PROXY_COOKIE_SECURE=true
+
Environment=OAUTH2_PROXY_REDIRECT_URL=https://oauth2-proxy.${base_domain}/oauth2/callback
+
Environment=OAUTH2_PROXY_COOKIE_DOMAINS=.${base_domain}
+
Environment=OAUTH2_PROXY_WHITELIST_DOMAINS=.${base_domain}
+
Environment=OAUTH2_PROXY_COOKIE_REFRESH=0
+
Environment=OAUTH2_PROXY_COOKIE_EXPIRE=59m
+
Environment=OAUTH2_PROXY_REVERSE_PROXY=true
+
Environment=OAUTH2_PROXY_UPSTREAMS=static://202
+
Secret=oauth2-proxy-cookie-secret,type=env,target=OAUTH2_PROXY_COOKIE_SECRET
+
Secret=oauth2-proxy-client-secret,type=env,target=OAUTH2_PROXY_CLIENT_SECRET
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.oauth2-proxy.rule=Host(`oauth2-proxy.${base_domain}`)"
+
Label="traefik.http.services.oauth2-proxy.loadbalancer.server.port=4180"
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+33
quadlets/open-webui.container.tftpl
···
+
[Unit]
+
Description=Open WebUI Quadlet
+
+
[Container]
+
Image=ghcr.io/open-webui/open-webui:main
+
AutoUpdate=registry
+
ContainerName=open-webui
+
+
User=0:0
+
+
Environment=WEBUI_URL=https://ai.${base_domain}
+
Environment=ENABLE_LOGIN_FORM=False
+
Environment=DEFAULT_LOCALE=ru
+
Environment=CORS_ALLOW_ORIGIN=https://localhost
+
Environment=ENABLE_OAUTH_SIGNUP=True
+
Environment=OAUTH_CLIENT_ID=d4979561-8290-49c9-878e-0d325f7f06a6
+
Environment=OPENID_PROVIDER_URL=https://id.${base_domain}/.well-known/openid-configuration
+
Environment=OAUTH_PROVIDER_NAME="Pocket ID"
+
Secret=open-webui-oauth-client-secret,type=env,target=OAUTH_CLIENT_SECRET
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.open-webui.rule=Host(`ai.${base_domain}`)"
+
+
Volume=/var/mnt/docker/app_data/open-webui:/app/backend/data:Z
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+31
quadlets/plex.container.tftpl
···
+
[Unit]
+
Description=Plex Quadlet
+
+
[Container]
+
Image=docker.io/plexinc/pms-docker:plexpass
+
AutoUpdate=registry
+
ContainerName=plex
+
+
Environment=PLEX_UID=1000
+
Environment=PLEX_GID=1000
+
Environment=TZ=Europe/Belgrade
+
# In my setup source IP is not preserved for local network (due to SNAT hairpinning rule)
+
Environment=ALLOWED_NETWORKS=192.168.100.1/32
+
+
Volume=/var/mnt/docker/app_data/plex:/config:Z
+
Volume=/var/mnt/media/tv_shows:/data/tv_shows:z
+
Volume=/var/mnt/media/movies:/data/movies:z
+
Volume=/var/mnt/media/music:/data/music:z
+
Tmpfs=/transcode:size=8G,rw:Z
+
+
# Host network for simplicity
+
Network=host
+
+
AddDevice=/dev/dri
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+37
quadlets/pocket-id.container.tftpl
···
+
[Unit]
+
Description=Pocket ID Quadlet
+
+
[Container]
+
Image=ghcr.io/pocket-id/pocket-id:latest
+
AutoUpdate=registry
+
ContainerName=pocket-id
+
+
User=0:0
+
UIDMap="+1000:@1000:1"
+
+
Environment=PUBLIC_APP_URL=https://id.${base_domain}
+
Environment=PUID=1000
+
Environment=PGID=1000
+
Environment=CADDY_DISABLED=true
+
Secret=pocket-id-maxmind-license-key,type=env,target=MAXMIND_LICENSE_KEY
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.pocket-id.rule=Host(`id.${base_domain}`)"
+
Label="traefik.http.routers.pocket-id.service=pocket-id"
+
Label="traefik.http.routers.pocket-id.priority=1"
+
Label="traefik.http.services.pocket-id.loadbalancer.server.port=3000"
+
Label="traefik.http.routers.pocket-id-backend.rule=Host(`id.${base_domain}`) && (PathPrefix(`/api/`) || PathPrefix(`/.well-known/`))"
+
Label="traefik.http.routers.pocket-id-backend.service=pocket-id-backend"
+
Label="traefik.http.routers.pocket-id-backend.priority=2"
+
Label="traefik.http.services.pocket-id-backend.loadbalancer.server.port=8080"
+
+
Volume=/var/mnt/docker/app_data/pocket-id:/app/backend/data:Z
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+35
quadlets/qbittorrent.container.tftpl
···
+
[Unit]
+
Description=qBittorrent Quadlet
+
+
[Container]
+
Image=lscr.io/linuxserver/qbittorrent:latest
+
AutoUpdate=registry
+
ContainerName=qbittorrent
+
+
UIDMap=+1000:@1000:1
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.qbittorrent.rule=Host(`qb.${base_domain}`)"
+
Label="traefik.http.services.qbittorrent.loadbalancer.server.port=8080"
+
Label="traefik.http.routers.qbittorrent.middlewares=oauth2-proxy@file,strip-referer"
+
Label="traefik.http.routers.qbittorrent-auth.rule=Host(`qb.${base_domain}`) && PathPrefix(`/oauth2/`)"
+
Label="traefik.http.routers.qbittorrent-auth.service=oauth2-proxy"
+
Label="traefik.http.middlewares.strip-referer.headers.customRequestHeaders.Referer="
+
+
Environment=PUID=1000
+
Environment=PGID=1000
+
Environment=TZ=Europe/Belgrade
+
Environment=WEBUI_PORT=8080
+
Environment=TORRENTING_PORT=6881
+
+
Volume=/var/mnt/docker/app_data/qbittorrent:/config:Z
+
Volume=/var/mnt/media:/media:z
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+30
quadlets/step-ca.container.tftpl
···
+
[Unit]
+
Description=Smallstep step-ca Server Quadlet
+
+
[Container]
+
Image=docker.io/smallstep/step-ca:latest
+
AutoUpdate=registry
+
ContainerName=step-ca
+
+
User=1000:1000
+
UIDMap=+1000:@1000:1
+
+
Label="traefik.enable=true"
+
Label="traefik.tcp.routers.smallstep.rule=HostSNI(`ca.${base_domain}`)"
+
Label="traefik.tcp.routers.smallstep.tls.passthrough=true"
+
Label="traefik.tcp.services.smallstep.loadbalancer.server.port=9000"
+
+
Environment=DOCKER_STEPCA_INIT_NAME=Homelab
+
Environment=DOCKER_STEPCA_INIT_DNS_NAMES=ca.${base_domain}
+
Environment=DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT=true
+
+
Volume=/var/mnt/docker/app_data/smallstep:/home/step:Z
+
+
Network=reverse-proxy.network
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
+
[Install]
+
WantedBy=multi-user.target default.target
+41
quadlets/traefik.container.tftpl
···
+
[Unit]
+
Description=Traefik Quadlet
+
Requires=http.socket https.socket
+
After=http.socket https.socket
+
+
[Container]
+
Image=docker.io/library/traefik
+
AutoUpdate=registry
+
ContainerName=traefik
+
+
User=1000:1000
+
UIDMap=+1000:@1000:1
+
+
Environment=LEGO_DISABLE_CNAME_SUPPORT=true
+
+
Label="traefik.enable=true"
+
Label="traefik.http.routers.dashboard.rule=Host(`fcos.${base_domain}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
+
Label="traefik.http.routers.dashboard.service=api@internal"
+
Label="traefik.http.routers.dashboard.middlewares=oauth2-proxy@file"
+
Label="traefik.http.routers.dashboard-auth.rule=Host(`fcos.${base_domain}`) && PathPrefix(`/oauth2/`)"
+
Label="traefik.http.routers.dashboard-auth.service=oauth2-proxy"
+
+
Secret=traefik-cf-dns-api-token,type=env,target=CF_DNS_API_TOKEN
+
+
Volume=/var/mnt/docker/app_data/traefik/traefik.yml:/etc/traefik/traefik.yml:Z
+
Volume=/var/mnt/docker/app_data/traefik/data:/data:Z
+
# requires user (!) podman.socket running
+
Volume=%t/podman/podman.sock:/var/run/docker.sock
+
+
Network=reverse-proxy.network
+
Notify=true
+
+
SecurityLabelDisable=true
+
+
[Service]
+
TimeoutStartSec=900
+
Restart=always
+
Sockets=http.socket https.socket
+
+
[Install]
+
WantedBy=multi-user.target default.target
+17
scripts/init_fcos.sh.tftpl
···
+
#!/bin/bash
+
set -e
+
+
echo "Importing secrets..."
+
# Bitwarden Secrets Manager CLI requires to save state in order to work correctly, but
+
# Fedora CoreOS has strict SELinux policies, so we need to make proper adjustments.
+
%{ for name, id in secrets ~}
+
podman run --rm -it -v /var/home/core:/home/app --user 1000:1000 --uidmap +1000:@1000:1 --security-opt=label=disable \
+
bitwarden/bws secret get --color=no --access-token=${bws_access_token} ${id} | jq -r .value | tr -d '\n' | \
+
podman secret create --replace ${name} -
+
%{ endfor ~}
+
+
echo "Starting Quadlets..."
+
# Quadlets are "enabled" using their configurations, it's enough to just start them.
+
%{ for name, _ in quadlets ~}
+
systemctl --user start ${name}.service
+
%{ endfor ~}
+39
variables.tf
···
+
variable "bws_access_token" {
+
description = "Bitwarden Secrets CLI access token"
+
type = string
+
sensitive = true
+
}
+
+
variable "proxmox_config" {
+
description = "Proxmox credentials"
+
type = object({
+
endpoint = string
+
password_secret_id = string
+
})
+
}
+
+
# Just base domain for now
+
variable "quadlets_config" {
+
description = "Shared Quadlets configuration"
+
type = object({
+
base_domain = string
+
})
+
}
+
+
variable "fcos_config" {
+
description = "Fedora CoreOS Configuration"
+
type = object({
+
hostname = string
+
ssh_key = string
+
root_ca = string
+
+
mac_address = string
+
ip = string
+
gateway = string
+
mask = string
+
nameserver = string
+
+
truenas_ip = string
+
truenas_iqn = string
+
})
+
}