<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Erdi Dogruel</title>
    <description>The latest articles on DEV Community by Erdi Dogruel (@erdid).</description>
    <link>https://dev.to/erdid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3327849%2F738478f5-444c-4be9-8249-8ca94c249af0.jpeg</url>
      <title>DEV Community: Erdi Dogruel</title>
      <link>https://dev.to/erdid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erdid"/>
    <language>en</language>
    <item>
      <title>How I Built a Full Kubernetes Platform on Hetzner for 19,90€</title>
      <dc:creator>Erdi Dogruel</dc:creator>
      <pubDate>Sun, 06 Jul 2025 11:42:42 +0000</pubDate>
      <link>https://dev.to/erdid/how-i-built-a-full-kubernetes-platform-on-hetzner-for-1990eu-53oo</link>
      <guid>https://dev.to/erdid/how-i-built-a-full-kubernetes-platform-on-hetzner-for-1990eu-53oo</guid>
      <description>&lt;p&gt;Most Kubernetes tutorials show you how to get something running – but rarely explain &lt;em&gt;why&lt;/em&gt; specific decisions are made or what pitfalls to avoid. This article walks you through building a &lt;strong&gt;production-grade Kubernetes cluster on Hetzner Cloud&lt;/strong&gt;, complete with Control Plane, Load Balancer, Persistent Volumes, Private Registry, and automated TLS certificates. You will find all scripts and yamls here: &lt;a href="https://github.com/erdiD/hetznerk8s" rel="noopener noreferrer"&gt;https://github.com/erdiD/hetznerk8s&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;We start with three Ubuntu 22.04 servers on Hetzner.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hostname&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Public IP&lt;/th&gt;
&lt;th&gt;Private IP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mykubu-1&lt;/td&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;128.140.X.X&lt;/td&gt;
&lt;td&gt;10.0.1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mykubu-2&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;91.99.X.X&lt;/td&gt;
&lt;td&gt;10.0.1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mykubu-3&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;95.217.X.X&lt;/td&gt;
&lt;td&gt;10.0.1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why private IPs?&lt;/strong&gt; Cluster traffic is more efficient and secure via private interfaces. Especially for internal storage like Longhorn.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Base Setup on All Nodes
&lt;/h2&gt;

&lt;p&gt;This script (You will find here: &lt;a href="https://github.com/erdiD/hetznerk8s/blob/main/setupHost.sh" rel="noopener noreferrer"&gt;https://github.com/erdiD/hetznerk8s/blob/main/setupHost.sh&lt;/a&gt;) ensures baseline settings for networking, Docker dependencies, and kernel modules. It’s the minimal groundwork for a stable kubeadm setup. It is installing everything we need for each Host to run a cluster node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./setupHost.sh
reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Control Plane Initialization
&lt;/h2&gt;

&lt;p&gt;We also need to install Helm to simplify the handling of the packages. Helm is the de-facto package manager for Kubernetes. We’ll use it for most components like ingress, cert-manager, Longhorn, etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;helm &lt;span class="nt"&gt;--classic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate Init Configuration
&lt;/h3&gt;

&lt;p&gt;Now, let Kubernetes create a initial configuration. You will find an example in the Folder &lt;code&gt;setupCluster/InitConfiguration.yaml&lt;/code&gt; or you can create it by using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm config print init-defaults &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; InitConfiguration.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This config controls things like pod networking, cert parameters, and API bindings. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Bootstrap the Cluster
&lt;/h3&gt;

&lt;p&gt;Run the Init Command and setup the kubectl command. This sets up &lt;code&gt;kubectl&lt;/code&gt; for your admin user and makes command-line usage smooth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm init &lt;span class="nt"&gt;--config&lt;/span&gt; InitConfiguration.yaml

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/kubernetes/admin.conf &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'alias k=kubectl'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Joining Worker Nodes
&lt;/h2&gt;

&lt;p&gt;On &lt;code&gt;mykubu-2&lt;/code&gt; and &lt;code&gt;mykubu-3&lt;/code&gt;: Create a JoinConfiguration from the kube. You will find an example in the Folder &lt;code&gt;setupCluster/JoinConfiguration.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm config print join-defaults &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; JoinConfiguration.yaml
kubeadm &lt;span class="nb"&gt;join&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; JoinConfiguration.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The join config ensures that tokens, endpoints, and bootstrap flows are consistent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. CNI and Node Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Label Worker Nodes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label node mykubu-2 node-role.kubernetes.io/worker&lt;span class="o"&gt;=&lt;/span&gt;worker
kubectl label node mykubu-3 node-role.kubernetes.io/worker&lt;span class="o"&gt;=&lt;/span&gt;worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;These labels help Kubernetes schedule workloads using node selectors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Allow Scheduling on Control Plane (for testing only)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl taint nodes mykubu-1 node-role.kubernetes.io/control-plane-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not recommended in production – but fine for learning or small setups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Flannel (CNI)
&lt;/h3&gt;

&lt;p&gt;Prepare your values-flannel.yaml. You will find a example in &lt;code&gt;setupCluster/values-flannel.yaml&lt;/code&gt;. You have to write your podsubnet unter 'networking.podSubnet' (line 8, you can leave it with default).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create ns kube-flannel
kubectl label &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns kube-flannel pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;privileged
helm repo add flannel https://flannel-io.github.io/flannel/
helm &lt;span class="nb"&gt;install &lt;/span&gt;flannel flannel/flannel &lt;span class="nt"&gt;--values&lt;/span&gt; values-flannel.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; kube-flannel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Flannel handles inter-pod networking. Ensure your pod subnet is correctly defined in the values file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Fix CoreDNS tolerations
&lt;/h3&gt;

&lt;p&gt;This unblocks CoreDNS which is otherwise blocked by the cloud-controller tolerations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system patch deployment coredns &lt;span class="nt"&gt;--type&lt;/span&gt; json &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'[{"op":"add","path":"/spec/template/spec/tolerations/-","value":{"key":"node.cloudprovider.kubernetes.io/uninitialized","value":"true","effect":"NoSchedule"}}]'&lt;/span&gt;

kubectl taint nodes mykubu-1 node.cloudprovider.kubernetes.io/uninitialized&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;:NoSchedule-
kubectl taint nodes mykubu-2 node.cloudprovider.kubernetes.io/uninitialized&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;:NoSchedule-
kubectl taint nodes mykubu-3 node.cloudprovider.kubernetes.io/uninitialized&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;:NoSchedule-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Hetzner Cloud Controller Manager (HCCM)
&lt;/h2&gt;

&lt;p&gt;HCCM bridges Kubernetes with Hetzner’s native APIs: it manages load balancers, floating IPs, and metadata. You have to add a API Token from Hetzner and your private Networkid. The API Token can you get from the Hetzner Console under Security and then &lt;code&gt;API-Tokens&lt;/code&gt;. The Networkid is under Networks section. Here you can extract it from the url, i.e. &lt;code&gt;https://console.hetzner.com/projects/11111111/networks/&amp;lt;HETZNER_NETWORK_ID&amp;gt;/resources&lt;/code&gt;. Here is a Example of the Screen where you can get your API-Key. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe51kzu2shjh2qoxzs8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe51kzu2shjh2qoxzs8x.png" alt="api-key" width="800" height="243"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system create secret generic hcloud &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TOKEN&amp;gt; &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;NETWORK_ID&amp;gt;
helm repo add hcloud https://charts.hetzner.cloud
helm &lt;span class="nb"&gt;install &lt;/span&gt;hccm hcloud/hcloud-cloud-controller-manager &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. NGINX Ingress Controller
&lt;/h2&gt;

&lt;p&gt;You need a ingress-value.yaml. You will find it under &lt;code&gt;setupCluster/values-ingress.yaml&lt;/code&gt;. I will highlight the changes you have to make. It has around 1300 lines. I will only highlight the lines you have to change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dnsPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterFirstWithHostNet&lt;/span&gt;
  &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DaemonSet&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;load-balancer.hetzner.cloud/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k8s-lb"&lt;/span&gt;
      &lt;span class="na"&gt;load-balancer.hetzner.cloud/location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fsn1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This setup exposes the ingress via Hetzner Load Balancer through host networking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Install NGINX Helm Chart&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm &lt;span class="nb"&gt;install &lt;/span&gt;ingress-nginx-controller ingress-nginx/ingress-nginx &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;After installation, go to Hetzner Cloud UI and manually attach the LB to the private network and nodes. HCCM doesn’t do this reliably yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Connect to your Hetzner Cloud UI and select your loadbalancer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to the network tab and add the loadbalancer to the private network.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkzzkav5j1559kuprnar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkzzkav5j1559kuprnar.png" alt="network-screen" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the target tab and add all hosts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj60icuqedddnzwki6bzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj60icuqedddnzwki6bzy.png" alt="target-screen" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. TLS with Cert-Manager &amp;amp; Let’s Encrypt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add jetstack https://charts.jetstack.io
helm &lt;span class="nb"&gt;install &lt;/span&gt;cert-manager jetstack/cert-manager &lt;span class="nt"&gt;--namespace&lt;/span&gt; cert-manager &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; v1.18.0 &lt;span class="nt"&gt;--set&lt;/span&gt; crds.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Cert-Manager manages ACME challenges and TLS certificate lifecycles automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Define a ClusterIssuer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIssuer&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://acme-v02.api.letsencrypt.org/directory&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your@email.com&lt;/span&gt;
    &lt;span class="na"&gt;privateKeySecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
    &lt;span class="na"&gt;solvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cluster-issuer.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Longhorn for Persistent Storage
&lt;/h2&gt;

&lt;p&gt;Create a Secret to get at least a basic auth. You can fill out the namespaces  and &lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;ADMIN&amp;gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;PASSWORD&amp;gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl passwd &lt;span class="nt"&gt;-stdin&lt;/span&gt; &lt;span class="nt"&gt;-apr1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; auth

kubectl create ns longhorn-system

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; longhorn-system create secret generic basic-auth &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auth
&lt;span class="nb"&gt;rm &lt;/span&gt;auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Helm Chart&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add longhorn https://charts.longhorn.io
helm repo update
helm &lt;span class="nb"&gt;install &lt;/span&gt;longhorn longhorn/longhorn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; longhorn-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; persistence.defaultClassReplicaCount&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also add a Ingress&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-system&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/auth-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;basic&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/auth-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;basic-auth&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/auth-realm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authentication&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Required&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;longhorn.dogruel.me&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-tls&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn.dogruel.me&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
        &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-frontend&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 9: Install Harbor
&lt;/h2&gt;

&lt;p&gt;Please fill out the  with a PW of your choice. The user will be &lt;code&gt;admin&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add harbor https://helm.goharbor.io
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;harbor harbor/harbor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; harbor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; expose.ingress.hosts.core&lt;span class="o"&gt;=&lt;/span&gt;artifactory.dogruel.me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;externalURL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://artifactory.dogruel.me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; expose.ingress.className&lt;span class="o"&gt;=&lt;/span&gt;nginx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; expose.tls.secretName&lt;span class="o"&gt;=&lt;/span&gt;harbor-tls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; expose.ingress.annotations.&lt;span class="s2"&gt;"cert-manager&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;io/cluster-issuer"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;letsencrypt-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; persistence.persistentVolumeClaim.registry.size&lt;span class="o"&gt;=&lt;/span&gt;5Gi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; persistence.persistentVolumeClaim.database.size&lt;span class="o"&gt;=&lt;/span&gt;5Gi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; persistence.persistentVolumeClaim.jobservice.size&lt;span class="o"&gt;=&lt;/span&gt;2Gi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;harborAdminPassword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;ADMINPW&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This manual setup gives you full control and deep understanding of how Kubernetes interacts with a cloud provider like Hetzner. You now have a cluster with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load-balanced ingress via NGINX&lt;/li&gt;
&lt;li&gt;Persistent storage with Longhorn&lt;/li&gt;
&lt;li&gt;Private registry with Harbor&lt;/li&gt;
&lt;li&gt;Fully automated TLS with Let’s Encrypt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next part will be coming soon. If you have any questions let me know at &lt;a href="https://www.linkedin.com/in/erdidogruel/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/erdidogruel/&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>longhorn</category>
      <category>devops</category>
      <category>harbor</category>
    </item>
  </channel>
</rss>
