Personal Homelab

feat: initial commit

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