[{"data":1,"prerenderedAt":878},["ShallowReactive",2],{"blog-hetzner-k8s-cluster":3,"blog-post-nav":840},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"body":11,"_type":834,"_id":835,"_source":836,"_file":837,"_stem":838,"_extension":839},"/blog/hetzner-k8s-cluster","blog",false,"","Building a K3s Cluster on Hetzner with Terraform and GitOps","How I provision a four-node K3s cluster on Hetzner Cloud with Terraform, then run everything on it through ArgoCD and Helm.","2026-04-20",{"type":12,"children":13,"toc":826},"root",[14,22,29,59,75,81,94,250,263,276,282,287,381,394,407,574,586,592,614,660,688,694,716,776,804,809,815,820],{"type":15,"tag":16,"props":17,"children":18},"element","p",{},[19],{"type":20,"value":21},"text","For a while now I've been running my own Kubernetes cluster on Hetzner Cloud instead of paying for managed nodes elsewhere. The whole thing is reproducible: Terraform stands up the infrastructure, cloud-init bootstraps K3s, and ArgoCD takes over from there. This is a write-up of how it actually fits together.",{"type":15,"tag":23,"props":24,"children":26},"h2",{"id":25},"why-hetzner-and-k3s",[27],{"type":20,"value":28},"Why Hetzner and K3s",{"type":15,"tag":16,"props":30,"children":31},{},[32,34,41,43,49,51,57],{"type":20,"value":33},"Hetzner's pricing is hard to argue with. The cluster runs on ",{"type":15,"tag":35,"props":36,"children":38},"code",{"className":37},[],[39],{"type":20,"value":40},"cax21",{"type":20,"value":42}," ARM instances (4 vCPU, 8GB) for the master and three workers, plus a small ",{"type":15,"tag":35,"props":44,"children":46},{"className":45},[],[47],{"type":20,"value":48},"cax11",{"type":20,"value":50}," box dedicated to the database. The whole thing lives in the ",{"type":15,"tag":35,"props":52,"children":54},{"className":53},[],[55],{"type":20,"value":56},"nbg1",{"type":20,"value":58}," region behind a private network.",{"type":15,"tag":16,"props":60,"children":61},{},[62,64,73],{"type":20,"value":63},"I went with ",{"type":15,"tag":65,"props":66,"children":70},"a",{"href":67,"rel":68},"https://k3s.io",[69],"nofollow",[71],{"type":20,"value":72},"K3s",{"type":20,"value":74}," rather than full upstream Kubernetes because it's a single binary, it's light on resources, and it strips out the parts I don't want. The control plane is fronted by a private subnet, and the workers have no public inbound at all.",{"type":15,"tag":23,"props":76,"children":78},{"id":77},"provisioning-with-terraform",[79],{"type":20,"value":80},"Provisioning with Terraform",{"type":15,"tag":16,"props":82,"children":83},{},[84,86,92],{"type":20,"value":85},"Everything starts with the ",{"type":15,"tag":35,"props":87,"children":89},{"className":88},[],[90],{"type":20,"value":91},"hcloud",{"type":20,"value":93}," Terraform provider. I define a private network, a subnet, firewalls, and the servers themselves:",{"type":15,"tag":95,"props":96,"children":100},"pre",{"className":97,"code":98,"language":99,"meta":7,"style":7},"language-hcl shiki shiki-themes github-dark","resource \"hcloud_network\" \"private_network\" {\n  name     = \"kubernetes-cluster\"\n  ip_range = \"10.0.0.0/16\"\n}\n\nresource \"hcloud_server\" \"worker-nodes\" {\n  count       = 3\n  name        = \"worker-node-${count.index}\"\n  image       = \"ubuntu-24.04\"\n  server_type = \"cax21\"\n  location    = \"nbg1\"\n  network {\n    network_id = hcloud_network.private_network.id\n  }\n  firewall_ids = [hcloud_firewall.worker_firewall.id]\n}\n","hcl",[101],{"type":15,"tag":35,"props":102,"children":103},{"__ignoreMap":7},[104,115,124,133,142,152,161,170,179,188,197,206,215,224,233,242],{"type":15,"tag":105,"props":106,"children":109},"span",{"class":107,"line":108},"line",1,[110],{"type":15,"tag":105,"props":111,"children":112},{},[113],{"type":20,"value":114},"resource \"hcloud_network\" \"private_network\" {\n",{"type":15,"tag":105,"props":116,"children":118},{"class":107,"line":117},2,[119],{"type":15,"tag":105,"props":120,"children":121},{},[122],{"type":20,"value":123},"  name     = \"kubernetes-cluster\"\n",{"type":15,"tag":105,"props":125,"children":127},{"class":107,"line":126},3,[128],{"type":15,"tag":105,"props":129,"children":130},{},[131],{"type":20,"value":132},"  ip_range = \"10.0.0.0/16\"\n",{"type":15,"tag":105,"props":134,"children":136},{"class":107,"line":135},4,[137],{"type":15,"tag":105,"props":138,"children":139},{},[140],{"type":20,"value":141},"}\n",{"type":15,"tag":105,"props":143,"children":145},{"class":107,"line":144},5,[146],{"type":15,"tag":105,"props":147,"children":149},{"emptyLinePlaceholder":148},true,[150],{"type":20,"value":151},"\n",{"type":15,"tag":105,"props":153,"children":155},{"class":107,"line":154},6,[156],{"type":15,"tag":105,"props":157,"children":158},{},[159],{"type":20,"value":160},"resource \"hcloud_server\" \"worker-nodes\" {\n",{"type":15,"tag":105,"props":162,"children":164},{"class":107,"line":163},7,[165],{"type":15,"tag":105,"props":166,"children":167},{},[168],{"type":20,"value":169},"  count       = 3\n",{"type":15,"tag":105,"props":171,"children":173},{"class":107,"line":172},8,[174],{"type":15,"tag":105,"props":175,"children":176},{},[177],{"type":20,"value":178},"  name        = \"worker-node-${count.index}\"\n",{"type":15,"tag":105,"props":180,"children":182},{"class":107,"line":181},9,[183],{"type":15,"tag":105,"props":184,"children":185},{},[186],{"type":20,"value":187},"  image       = \"ubuntu-24.04\"\n",{"type":15,"tag":105,"props":189,"children":191},{"class":107,"line":190},10,[192],{"type":15,"tag":105,"props":193,"children":194},{},[195],{"type":20,"value":196},"  server_type = \"cax21\"\n",{"type":15,"tag":105,"props":198,"children":200},{"class":107,"line":199},11,[201],{"type":15,"tag":105,"props":202,"children":203},{},[204],{"type":20,"value":205},"  location    = \"nbg1\"\n",{"type":15,"tag":105,"props":207,"children":209},{"class":107,"line":208},12,[210],{"type":15,"tag":105,"props":211,"children":212},{},[213],{"type":20,"value":214},"  network {\n",{"type":15,"tag":105,"props":216,"children":218},{"class":107,"line":217},13,[219],{"type":15,"tag":105,"props":220,"children":221},{},[222],{"type":20,"value":223},"    network_id = hcloud_network.private_network.id\n",{"type":15,"tag":105,"props":225,"children":227},{"class":107,"line":226},14,[228],{"type":15,"tag":105,"props":229,"children":230},{},[231],{"type":20,"value":232},"  }\n",{"type":15,"tag":105,"props":234,"children":236},{"class":107,"line":235},15,[237],{"type":15,"tag":105,"props":238,"children":239},{},[240],{"type":20,"value":241},"  firewall_ids = [hcloud_firewall.worker_firewall.id]\n",{"type":15,"tag":105,"props":243,"children":245},{"class":107,"line":244},16,[246],{"type":15,"tag":105,"props":247,"children":248},{},[249],{"type":20,"value":141},{"type":15,"tag":16,"props":251,"children":252},{},[253,255,261],{"type":20,"value":254},"The firewall design is the important bit. The worker firewall has ",{"type":15,"tag":256,"props":257,"children":258},"strong",{},[259],{"type":20,"value":260},"no inbound rules at all",{"type":20,"value":262},", private network traffic between nodes bypasses the firewall automatically, so workers are completely sealed off from the public internet. The master firewall opens only SSH (22) and the Kubernetes API (6443).",{"type":15,"tag":16,"props":264,"children":265},{},[266,268,274],{"type":20,"value":267},"The master gets a static private IP of ",{"type":15,"tag":35,"props":269,"children":271},{"className":270},[],[272],{"type":20,"value":273},"10.0.1.1",{"type":20,"value":275}," so the workers always know where to find it.",{"type":15,"tag":23,"props":277,"children":279},{"id":278},"bootstrapping-k3s-with-cloud-init",[280],{"type":20,"value":281},"Bootstrapping K3s with cloud-init",{"type":15,"tag":16,"props":283,"children":284},{},[285],{"type":20,"value":286},"Rather than SSH in and run install scripts by hand, the nodes self-install K3s on first boot via cloud-init. The master installs the server with a deliberately trimmed config:",{"type":15,"tag":95,"props":288,"children":292},{"className":289,"code":290,"language":291,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark","curl https://get.k3s.io | INSTALL_K3S_EXEC=\"--disable traefik \\\n  --disable servicelb --disable-cloud-controller \\\n  --kubelet-arg cloud-provider=external \\\n  --tls-san 10.0.1.1 --flannel-iface=enp7s0\" sh -\n","bash",[293],{"type":15,"tag":35,"props":294,"children":295},{"__ignoreMap":7},[296,339,351,363],{"type":15,"tag":105,"props":297,"children":298},{"class":107,"line":108},[299,305,311,317,323,328,333],{"type":15,"tag":105,"props":300,"children":302},{"style":301},"--shiki-default:#B392F0",[303],{"type":20,"value":304},"curl",{"type":15,"tag":105,"props":306,"children":308},{"style":307},"--shiki-default:#9ECBFF",[309],{"type":20,"value":310}," https://get.k3s.io",{"type":15,"tag":105,"props":312,"children":314},{"style":313},"--shiki-default:#F97583",[315],{"type":20,"value":316}," |",{"type":15,"tag":105,"props":318,"children":320},{"style":319},"--shiki-default:#E1E4E8",[321],{"type":20,"value":322}," INSTALL_K3S_EXEC",{"type":15,"tag":105,"props":324,"children":325},{"style":313},[326],{"type":20,"value":327},"=",{"type":15,"tag":105,"props":329,"children":330},{"style":307},[331],{"type":20,"value":332},"\"--disable traefik ",{"type":15,"tag":105,"props":334,"children":336},{"style":335},"--shiki-default:#79B8FF",[337],{"type":20,"value":338},"\\\n",{"type":15,"tag":105,"props":340,"children":341},{"class":107,"line":117},[342,347],{"type":15,"tag":105,"props":343,"children":344},{"style":307},[345],{"type":20,"value":346},"  --disable servicelb --disable-cloud-controller ",{"type":15,"tag":105,"props":348,"children":349},{"style":335},[350],{"type":20,"value":338},{"type":15,"tag":105,"props":352,"children":353},{"class":107,"line":126},[354,359],{"type":15,"tag":105,"props":355,"children":356},{"style":307},[357],{"type":20,"value":358},"  --kubelet-arg cloud-provider=external ",{"type":15,"tag":105,"props":360,"children":361},{"style":335},[362],{"type":20,"value":338},{"type":15,"tag":105,"props":364,"children":365},{"class":107,"line":135},[366,371,376],{"type":15,"tag":105,"props":367,"children":368},{"style":307},[369],{"type":20,"value":370},"  --tls-san 10.0.1.1 --flannel-iface=enp7s0\"",{"type":15,"tag":105,"props":372,"children":373},{"style":301},[374],{"type":20,"value":375}," sh",{"type":15,"tag":105,"props":377,"children":378},{"style":307},[379],{"type":20,"value":380}," -\n",{"type":15,"tag":16,"props":382,"children":383},{},[384,386,392],{"type":20,"value":385},"I disable the bundled Traefik and servicelb because I bring my own ingress and load balancing through Helm. The ",{"type":15,"tag":35,"props":387,"children":389},{"className":388},[],[390],{"type":20,"value":391},"--flannel-iface=enp7s0",{"type":20,"value":393}," flag is critical: it pins the CNI to the private network interface so pod traffic never traverses the public NICs.",{"type":15,"tag":16,"props":395,"children":396},{},[397,399,405],{"type":20,"value":398},"The workers are templated through Terraform's ",{"type":15,"tag":35,"props":400,"children":402},{"className":401},[],[403],{"type":20,"value":404},"templatefile()",{"type":20,"value":406},", which injects an SSH key so each worker can pull the join token straight off the master before installing:",{"type":15,"tag":95,"props":408,"children":410},{"className":289,"code":409,"language":291,"meta":7,"style":7},"until curl -k https://10.0.1.1:6443; do sleep 5; done\nREMOTE_TOKEN=$(ssh cluster@10.0.1.1 sudo cat /var/lib/rancher/k3s/server/node-token)\ncurl -sfL https://get.k3s.io | K3S_URL=https://10.0.1.1:6443 \\\n  K3S_TOKEN=$REMOTE_TOKEN sh -\n",[411],{"type":15,"tag":35,"props":412,"children":413},{"__ignoreMap":7},[414,466,513,552],{"type":15,"tag":105,"props":415,"children":416},{"class":107,"line":108},[417,422,427,432,437,442,447,452,457,461],{"type":15,"tag":105,"props":418,"children":419},{"style":313},[420],{"type":20,"value":421},"until",{"type":15,"tag":105,"props":423,"children":424},{"style":301},[425],{"type":20,"value":426}," curl",{"type":15,"tag":105,"props":428,"children":429},{"style":335},[430],{"type":20,"value":431}," -k",{"type":15,"tag":105,"props":433,"children":434},{"style":307},[435],{"type":20,"value":436}," https://10.0.1.1:6443",{"type":15,"tag":105,"props":438,"children":439},{"style":319},[440],{"type":20,"value":441},"; ",{"type":15,"tag":105,"props":443,"children":444},{"style":313},[445],{"type":20,"value":446},"do",{"type":15,"tag":105,"props":448,"children":449},{"style":301},[450],{"type":20,"value":451}," sleep",{"type":15,"tag":105,"props":453,"children":454},{"style":335},[455],{"type":20,"value":456}," 5",{"type":15,"tag":105,"props":458,"children":459},{"style":319},[460],{"type":20,"value":441},{"type":15,"tag":105,"props":462,"children":463},{"style":313},[464],{"type":20,"value":465},"done\n",{"type":15,"tag":105,"props":467,"children":468},{"class":107,"line":117},[469,474,478,483,488,493,498,503,508],{"type":15,"tag":105,"props":470,"children":471},{"style":319},[472],{"type":20,"value":473},"REMOTE_TOKEN",{"type":15,"tag":105,"props":475,"children":476},{"style":313},[477],{"type":20,"value":327},{"type":15,"tag":105,"props":479,"children":480},{"style":319},[481],{"type":20,"value":482},"$(",{"type":15,"tag":105,"props":484,"children":485},{"style":301},[486],{"type":20,"value":487},"ssh",{"type":15,"tag":105,"props":489,"children":490},{"style":307},[491],{"type":20,"value":492}," cluster@10.0.1.1",{"type":15,"tag":105,"props":494,"children":495},{"style":307},[496],{"type":20,"value":497}," sudo",{"type":15,"tag":105,"props":499,"children":500},{"style":307},[501],{"type":20,"value":502}," cat",{"type":15,"tag":105,"props":504,"children":505},{"style":307},[506],{"type":20,"value":507}," /var/lib/rancher/k3s/server/node-token",{"type":15,"tag":105,"props":509,"children":510},{"style":319},[511],{"type":20,"value":512},")\n",{"type":15,"tag":105,"props":514,"children":515},{"class":107,"line":126},[516,520,525,529,533,538,542,547],{"type":15,"tag":105,"props":517,"children":518},{"style":301},[519],{"type":20,"value":304},{"type":15,"tag":105,"props":521,"children":522},{"style":335},[523],{"type":20,"value":524}," -sfL",{"type":15,"tag":105,"props":526,"children":527},{"style":307},[528],{"type":20,"value":310},{"type":15,"tag":105,"props":530,"children":531},{"style":313},[532],{"type":20,"value":316},{"type":15,"tag":105,"props":534,"children":535},{"style":319},[536],{"type":20,"value":537}," K3S_URL",{"type":15,"tag":105,"props":539,"children":540},{"style":313},[541],{"type":20,"value":327},{"type":15,"tag":105,"props":543,"children":544},{"style":307},[545],{"type":20,"value":546},"https://10.0.1.1:6443",{"type":15,"tag":105,"props":548,"children":549},{"style":301},[550],{"type":20,"value":551}," \\\n",{"type":15,"tag":105,"props":553,"children":554},{"class":107,"line":135},[555,560,565,570],{"type":15,"tag":105,"props":556,"children":557},{"style":307},[558],{"type":20,"value":559},"  K3S_TOKEN=",{"type":15,"tag":105,"props":561,"children":562},{"style":319},[563],{"type":20,"value":564},"$REMOTE_TOKEN ",{"type":15,"tag":105,"props":566,"children":567},{"style":307},[568],{"type":20,"value":569},"sh",{"type":15,"tag":105,"props":571,"children":572},{"style":307},[573],{"type":20,"value":380},{"type":15,"tag":16,"props":575,"children":576},{},[577,579,584],{"type":20,"value":578},"The ",{"type":15,"tag":35,"props":580,"children":582},{"className":581},[],[583],{"type":20,"value":421},{"type":20,"value":585}," loop matters more than it looks, Terraform creates resources in parallel, so workers will happily try to join before the master's API is up. The dependency chain plus the retry loop keeps the bootstrap deterministic.",{"type":15,"tag":23,"props":587,"children":589},{"id":588},"persistent-storage-on-a-storage-box",[590],{"type":20,"value":591},"Persistent storage on a Storage Box",{"type":15,"tag":16,"props":593,"children":594},{},[595,597,604,606,612],{"type":20,"value":596},"Hetzner Storage Boxes are cheap SMB/CIFS shares, so I use them for persistent volumes via the ",{"type":15,"tag":65,"props":598,"children":601},{"href":599,"rel":600},"https://github.com/kubernetes-csi/csi-driver-smb",[69],[602],{"type":20,"value":603},"SMB CSI driver",{"type":20,"value":605},", installed through the Helm Terraform provider. A ",{"type":15,"tag":35,"props":607,"children":609},{"className":608},[],[610],{"type":20,"value":611},"StorageClass",{"type":20,"value":613}," points at the share and pulls credentials from a Kubernetes secret:",{"type":15,"tag":95,"props":615,"children":617},{"className":97,"code":616,"language":99,"meta":7,"style":7},"parameters = {\n  source = \"//${var.storagebox_host}/backup\"\n  \"csi.storage.k8s.io/node-stage-secret-name\" = \"storagebox-credentials\"\n}\nmount_options = [\"dir_mode=0777\", \"file_mode=0777\", \"nobrl\"]\n",[618],{"type":15,"tag":35,"props":619,"children":620},{"__ignoreMap":7},[621,629,637,645,652],{"type":15,"tag":105,"props":622,"children":623},{"class":107,"line":108},[624],{"type":15,"tag":105,"props":625,"children":626},{},[627],{"type":20,"value":628},"parameters = {\n",{"type":15,"tag":105,"props":630,"children":631},{"class":107,"line":117},[632],{"type":15,"tag":105,"props":633,"children":634},{},[635],{"type":20,"value":636},"  source = \"//${var.storagebox_host}/backup\"\n",{"type":15,"tag":105,"props":638,"children":639},{"class":107,"line":126},[640],{"type":15,"tag":105,"props":641,"children":642},{},[643],{"type":20,"value":644},"  \"csi.storage.k8s.io/node-stage-secret-name\" = \"storagebox-credentials\"\n",{"type":15,"tag":105,"props":646,"children":647},{"class":107,"line":135},[648],{"type":15,"tag":105,"props":649,"children":650},{},[651],{"type":20,"value":141},{"type":15,"tag":105,"props":653,"children":654},{"class":107,"line":144},[655],{"type":15,"tag":105,"props":656,"children":657},{},[658],{"type":20,"value":659},"mount_options = [\"dir_mode=0777\", \"file_mode=0777\", \"nobrl\"]\n",{"type":15,"tag":16,"props":661,"children":662},{},[663,664,670,672,678,680,686],{"type":20,"value":578},{"type":15,"tag":35,"props":665,"children":667},{"className":666},[],[668],{"type":20,"value":669},"nobrl",{"type":20,"value":671}," option (disable byte-range locks) was a hard-won addition, without it, some workloads choke on SMB locking semantics. The reclaim policy is ",{"type":15,"tag":35,"props":673,"children":675},{"className":674},[],[676],{"type":20,"value":677},"Retain",{"type":20,"value":679}," so I never lose data to an accidental ",{"type":15,"tag":35,"props":681,"children":683},{"className":682},[],[684],{"type":20,"value":685},"kubectl delete pvc",{"type":20,"value":687},".",{"type":15,"tag":23,"props":689,"children":691},{"id":690},"everything-else-is-gitops",[692],{"type":20,"value":693},"Everything else is GitOps",{"type":15,"tag":16,"props":695,"children":696},{},[697,699,706,708,714],{"type":20,"value":698},"Once the cluster is up, Terraform's job is basically done. From there, ",{"type":15,"tag":65,"props":700,"children":703},{"href":701,"rel":702},"https://argo-cd.readthedocs.io",[69],[704],{"type":20,"value":705},"ArgoCD",{"type":20,"value":707}," runs the show. My Helm charts are organised by namespace under ",{"type":15,"tag":35,"props":709,"children":711},{"className":710},[],[712],{"type":20,"value":713},"charts/",{"type":20,"value":715},":",{"type":15,"tag":717,"props":718,"children":719},"ul",{},[720,732,743,754,765],{"type":15,"tag":721,"props":722,"children":723},"li",{},[724,730],{"type":15,"tag":35,"props":725,"children":727},{"className":726},[],[728],{"type":20,"value":729},"cert-manager",{"type":20,"value":731}," for TLS",{"type":15,"tag":721,"props":733,"children":734},{},[735,741],{"type":15,"tag":35,"props":736,"children":738},{"className":737},[],[739],{"type":20,"value":740},"infisical",{"type":20,"value":742}," for secret management",{"type":15,"tag":721,"props":744,"children":745},{},[746,752],{"type":15,"tag":35,"props":747,"children":749},{"className":748},[],[750],{"type":20,"value":751},"monitoring",{"type":20,"value":753}," for the Prometheus/Grafana observability stack",{"type":15,"tag":721,"props":755,"children":756},{},[757,763],{"type":15,"tag":35,"props":758,"children":760},{"className":759},[],[761],{"type":20,"value":762},"tailscale",{"type":20,"value":764}," for private access to internal services",{"type":15,"tag":721,"props":766,"children":767},{},[768,774],{"type":15,"tag":35,"props":769,"children":771},{"className":770},[],[772],{"type":20,"value":773},"default",{"type":20,"value":775}," for the actual application workloads",{"type":15,"tag":16,"props":777,"children":778},{},[779,781,787,789,795,797,802],{"type":20,"value":780},"Secrets never touch Git. Public config lives in ",{"type":15,"tag":35,"props":782,"children":784},{"className":783},[],[785],{"type":20,"value":786},"values.yaml",{"type":20,"value":788},"; anything sensitive is referenced through Infisical ",{"type":15,"tag":35,"props":790,"children":792},{"className":791},[],[793],{"type":20,"value":794},"InfisicalSecret",{"type":20,"value":796}," custom resources. The workflow is just: edit ",{"type":15,"tag":35,"props":798,"children":800},{"className":799},[],[801],{"type":20,"value":786},{"type":20,"value":803},", commit, push, ArgoCD syncs the change to the cluster automatically.",{"type":15,"tag":16,"props":805,"children":806},{},[807],{"type":20,"value":808},"The monitoring stack has grown into the most active part of the repo. I've added SNMP exporters for my router, an AdGuard Home exporter, and a handful of custom Grafana dashboards for home network health, all managed as Helm values and synced through Argo.",{"type":15,"tag":23,"props":810,"children":812},{"id":811},"what-id-tell-past-me",[813],{"type":20,"value":814},"What I'd tell past me",{"type":15,"tag":16,"props":816,"children":817},{},[818],{"type":20,"value":819},"The two things that bit me hardest were both networking: pinning Flannel to the private interface, and remembering that \"no inbound firewall rules\" is the correct, secure default for workers rather than an oversight. Get the private network right first, and the rest of the cluster is honestly the easy part.",{"type":15,"tag":821,"props":822,"children":823},"style",{},[824],{"type":20,"value":825},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":117,"depth":117,"links":827},[828,829,830,831,832,833],{"id":25,"depth":117,"text":28},{"id":77,"depth":117,"text":80},{"id":278,"depth":117,"text":281},{"id":588,"depth":117,"text":591},{"id":690,"depth":117,"text":693},{"id":811,"depth":117,"text":814},"markdown","content:blog:hetzner-k8s-cluster.md","content","blog/hetzner-k8s-cluster.md","blog/hetzner-k8s-cluster","md",[841,845,849,853,857,858,862,866,870,874],{"_path":842,"title":843,"date":844},"/blog/deploying-nuxt-to-cloudflare-workers","Deploying this Nuxt site to Cloudflare Workers","2026-06-06",{"_path":846,"title":847,"date":848},"/blog/building-forever-llm","Building forever-llm: three takes on a model that never stops","2026-05-20",{"_path":850,"title":851,"date":852},"/blog/laravel-cortex-adhd-productivity","Building Cortex: An ADHD Productivity App in Laravel + Inertia","2026-05-12",{"_path":854,"title":855,"date":856},"/blog/eink-spotify-weather-clock","Building an E-Ink Clock That Shows Spotify, Weather and the Time","2026-05-05",{"_path":4,"title":8,"date":10},{"_path":859,"title":860,"date":861},"/blog/arduino-uno-q-forza-rev-gauge","A Forza Rev Gauge on the Arduino UNO Q's LED Matrix","2026-04-15",{"_path":863,"title":864,"date":865},"/blog/modular-go-echo-gorm","A Modular Go Web App Pattern with Echo, GORM and golang-migrate","2026-04-05",{"_path":867,"title":868,"date":869},"/blog/self-hosted-ios-web-push","Self-hosting iOS push notifications with Web Push and PWAs","2026-03-25",{"_path":871,"title":872,"date":873},"/blog/lit-web-components-astro-docs","Building a framework-free web component library with Lit and Astro","2026-03-10",{"_path":875,"title":876,"date":877},"/blog/welcome-to-my-blog","About this site","2024-01-15",1781294950576]