<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://egel.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://egel.github.io/" rel="alternate" type="text/html" /><updated>2026-02-14T01:11:22+00:00</updated><id>https://egel.github.io/feed.xml</id><title type="html">Maciej Sypien</title><subtitle>Personal blog about life as the software engineer.</subtitle><author><name>Maciej Sypien</name></author><entry><title type="html">Setup GitLab runner on Home K8s Cluster with Helm</title><link href="https://egel.github.io/2026/01/23/installing-gitlab-runner-on-home-k8s-cluster-with-helm.html" rel="alternate" type="text/html" title="Setup GitLab runner on Home K8s Cluster with Helm" /><published>2026-01-23T00:00:00+00:00</published><updated>2026-01-23T00:00:00+00:00</updated><id>https://egel.github.io/2026/01/23/installing-gitlab-runner-on-home-k8s-cluster-with-helm</id><content type="html" xml:base="https://egel.github.io/2026/01/23/installing-gitlab-runner-on-home-k8s-cluster-with-helm.html"><![CDATA[<p>I’ve been exploring Kubernetes and GitLab for quite some time. Until recently, I’ve relied on GitHub’s free runners or GitLab runners hosted on private instances. I’ve often using manually installed executables directly on production servers to automate my deployment processes. However, this approach presented a few challenges.</p>

<p>One concern with free runners is the lack of privacy. Pipeline job details are publicly visible, which could expose sensitive information if building critical infrastructure components. For example, someone could observe the steps involved in creating a sensitive environment. Clearly, that’s not a desirable situation.</p>

<p>Deploying runners directly on production servers, especially if those servers are also hosting other services, wastes valuable resources that should be dedicated to running the primary application. This isn’t an ideal solution either.</p>

<p>Another option is to rent a dedicated server, like a VPS, for a modest cost. While seemingly straightforward, this comes with the risk of losing everything if you fail to pay the rental fee. Your VPS, and the entire setup you’ve invested time in, could disappear. For just a little more money, you could purchase your own private server, granting you complete control and flexibility. Imagine taking a three-month vacation, shutting down your home lab, and returning to find everything exactly as you left it, without ongoing rental fees. Great, isn’t it?</p>

<p>These factors ultimately led me to build a home Kubernetes cluster to fully leverage its capabilities and run my own GitLab runners within my home lab.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>Installing GitLab requires a few additional things you might need before attempting a pure installation.</p>

<ul>
  <li>First, you’ll need a GitLab account, either a private one or one from the official gitlab.com domain (I used the latter).</li>
  <li>Second, you’ll need a prepared Kubernetes cluster to begin the installation. If you’re interested, I’ve written an article on how to <a href="/2024/08/26/home-k8s-cluster.html">set up your own home Kubernetes cluster</a>.</li>
</ul>

<h2 id="architecture">Architecture</h2>

<p>In this post, I will guide you through installing the latest GitLab Runner on a Kubernetes cluster using GitLab’s official Helm charts.</p>

<p>Below is an overview of how the home cluster and main GitLab instance will look in our example.</p>

<p><img alt="Architecture" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/architecture.excalidraw.svg" width="100%" height="auto" /></p>

<h3 id="pros-and-cons-of-using-different-executors">Pros and cons of using different executors</h3>

<p>Before diving into the installation process, let’s weigh the pros and cons of using different executors. It’s important to understand why the Kubernetes executor might be preferable or less useful depending on the specific situation, compared to for example a simpler Docker executor.</p>

<h4 id="shell-executor">Shell Executor</h4>

<table>
  <thead>
    <tr>
      <th>🛑 cons</th>
      <th>✅ pros</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>hard to scale</td>
      <td>setup is very simple</td>
    </tr>
    <tr>
      <td>no isolation between jobs</td>
      <td>just works on local pc</td>
    </tr>
    <tr>
      <td>works only on local machine</td>
      <td>job starts quickly</td>
    </tr>
    <tr>
      <td>the runner host is vulnerable</td>
      <td>great for basic, short tasks</td>
    </tr>
    <tr>
      <td>environment consistency is a problem</td>
      <td> </td>
    </tr>
    <tr>
      <td>requires maintaining host environment</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>The Shell executor appears to offer a fast and straightforward setup for testing. It seems well-suited for quickly testing very basic, small tasks. However, for more complex operations, it’s likely not the best choice.</p>

<h4 id="docker-executor">Docker executor</h4>

<table>
  <thead>
    <tr>
      <th>🛑 cons</th>
      <th>✅ pros</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>difficult to scale</td>
      <td>relative easy to setup</td>
    </tr>
    <tr>
      <td>must use docker daemon (not other alternatives)</td>
      <td>clean environment per job</td>
    </tr>
    <tr>
      <td>resources works for entire docker daemon</td>
      <td>allow to define resources for docker daemon</td>
    </tr>
    <tr>
      <td> </td>
      <td>just works on local pc</td>
    </tr>
  </tbody>
</table>

<p>The Docker executor presents a reasonable solution, providing a good balance of features. It offers relatively easy setup and a clean environment per job. However, it’s not without limitations, as scaling can be challenging if you have a large number of tasks to execute. Resource specifications apply to the overall daemon service, which lacks the granularity found in Kubernetes.</p>

<h4 id="kubernetes-executor">Kubernetes executor</h4>

<table>
  <thead>
    <tr>
      <th>🛑 cons</th>
      <th>✅ pros</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>setup that could be complex</td>
      <td>offer excellent scalability</td>
    </tr>
    <tr>
      <td>works usually in distributed setup</td>
      <td>full job isolation</td>
    </tr>
    <tr>
      <td>requires a Kubernetes cluster</td>
      <td>full automation for jobs and cleanup</td>
    </tr>
    <tr>
      <td>maintenance require more knowledge</td>
      <td>allow to define specific resources management</td>
    </tr>
    <tr>
      <td>Kubernetes has a steep learning curve</td>
      <td>adding new instances integrates quickly with Kubernetes infrastructure</td>
    </tr>
    <tr>
      <td>network latency can be a problem</td>
      <td>high availability by defining multiple runners</td>
    </tr>
  </tbody>
</table>

<p>It would be disingenuous to claim the Kubernetes executor is the absolute best option simply by comparing pros and cons. It’s a truly impressive solution, but it appears most suitable for more advanced users or frequent, complex scenarios. A key consideration is the maintenance overhead and the specialized knowledge required to set it up. While GitLab Runner configuration is straightforward, managing a Kubernetes cluster alongside it can represent a significant workload.</p>

<p>However, if you already have a Kubernetes cluster in your local environment or in the cloud, leveraging those resources to offload job execution from your local machine, which is anyway often busy with other tasks like running your IDEs, local Docker images, Browsers or utilizing LLMs models. You’d typically need a very powerful machine to handle all of that. Delegating this task to a pool of dedicated resources sounds like a great, win-win approach.</p>

<h3 id="job-flow-execution">Job Flow Execution</h3>

<p>Let’s explore how the job execution flow works. We’ll consider a typical, straightforward execution: when a developer pushes code to a pull request (in GitLab, this is called a Merge Request), it’s checked by the runners (using <code class="language-plaintext highlighter-rouge">Dockerfile</code>, <code class="language-plaintext highlighter-rouge">shellcheck</code>, and linting tests).</p>

<p>How this works:</p>

<ol>
  <li>We define our tasks file <code class="language-plaintext highlighter-rouge">gitlab-ci.yaml</code> in the repository.</li>
  <li>A developer pushes code to a branch.</li>
  <li>GitLab detects new commits and triggers the pipeline based on the job definition.</li>
  <li>Runners assigned to the project, for specific tags or as general runners, periodically check for jobs to execute and retrieve them.</li>
  <li>The job is executed on the runner and the results are returned to the main GitLab instance.</li>
  <li>The results can be viewed in the job execution summary.</li>
</ol>

<p>As you can see, the job flow architecture is quite simple. It consists of the main code host (GitLab) and the runners (GitLab Runners). This architecture allows us to easily deploy code to a single instance and specify multiple runners, each serving a different purpose (production, staging, team 1, team 2, etc.).</p>

<h2 id="installing-gitlab-runner-on-kubernetes-cluster">Installing gitlab-runner on Kubernetes cluster</h2>

<h3 id="installation">Installation</h3>

<p>Make sure you have access to your k8s cluster (master node, or control plane). You should get similar message as below.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl cluster-info
Kubernetes control plane is running at https://192.168.178.200:6443
CoreDNS is running at https://192.168.178.200:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use <span class="s1">'kubectl cluster-info dump'</span><span class="nb">.</span>
</code></pre></div></div>

<p>Further, confirm you have a <code class="language-plaintext highlighter-rouge">helm</code> installed (best from version 3.0 above.)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>helm version
version.BuildInfo<span class="o">{</span>Version:<span class="s2">"v3.18.3"</span>, GitCommit:<span class="s2">"6838ebcf265a3842d1433956e8a622e3290cf324"</span>, GitTreeState:<span class="s2">"clean"</span>, GoVersion:<span class="s2">"go1.24.4"</span><span class="o">}</span>
</code></pre></div></div>

<h4 id="get-token-for-gitlab-runner">Get token for gitlab runner</h4>

<ol>
  <li>Login to gitlab</li>
  <li>
    <p>Go to your project and enter the left sidebar menu <strong>Settings</strong> -&gt; <strong>CI-CD</strong></p>

    <p><img alt="Gitlab CI-CD page" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-project-settings-cicd.png" width="100%" height="auto" /></p>
  </li>
  <li>
    <p>Next open Runners section and click <code class="language-plaintext highlighter-rouge">Create project runner</code>.</p>

    <p><img alt="Gitlab Runners page" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-project-settings-cicd-runners.png" width="100%" height="auto" /></p>
  </li>
  <li>Fill the data to create new project and define following things:
    <ul>
      <li><strong>Tags</strong>: In the example I used following: <code class="language-plaintext highlighter-rouge">k8s</code>, <code class="language-plaintext highlighter-rouge">kubernetes</code>, <code class="language-plaintext highlighter-rouge">docker</code>, <code class="language-plaintext highlighter-rouge">linux</code>, <code class="language-plaintext highlighter-rouge">homecluster</code>. Tags give you a lever to execute a job on a specific runner.</li>
      <li><strong>Run untagged jobs</strong>: This option allows you to use pick by this runner any job assigned to this project.</li>
      <li><strong>Description</strong> (optional): Mostly to orientate yourself which runner it describes.</li>
    </ul>

    <p><img alt="Gitlab Create new project runner" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-project-settings-cicd-runners-new-project-runner.png" width="100%" height="auto" /></p>
  </li>
  <li>
    <p><strong>Write down the token and store it save place!</strong> - we will need this token in later steps.</p>

    <p><img alt="Gitlab Create new project runner token" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-project-settings-cicd-runners-new-project-runner-project-token.png" width="100%" height="auto" /></p>

    <blockquote>
      <p><strong>Notice</strong>: If case you wonder, <em>for the security reason, the token I left on screenshot is already invalid</em> 😉. I wanted to leave it on screenshot, to document it and present you as much possible also with how the real token looks like.</p>
    </blockquote>
  </li>
</ol>

<h4 id="install-gitlab-runner-helm-chart">Install gitlab runner Helm Chart</h4>

<p>Add gitlab Helm repo.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add gitlab https://charts.gitlab.io
helm repo update
</code></pre></div></div>

<p>Create new namespace for the gitlab runner.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create namespace gitlab
</code></pre></div></div>

<p>Verify and checkout to new namespace.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe namespace gitlab
Name:         gitlab
Labels:       kubernetes.io/metadata.name<span class="o">=</span>gitlab
Annotations:  &lt;none&gt;
Status:       Active

No resource quota.

No LimitRange resource.

<span class="c"># set current context to gitlab namespace</span>
<span class="nv">$ </span>kubectl config set-context <span class="nt">--current</span> <span class="nt">--namespace</span><span class="o">=</span>gitlab
</code></pre></div></div>

<h4 id="configure-the-gitlab-runner">Configure the gitlab-runner</h4>

<p>In this section, we’ll use Helm to install the GitLab Runner. This involves configuring a YAML file with the necessary settings. You can find a complete example in my public GitHub repository: <a href="https://github.com/egel/k8s-gitlab-runner">https://github.com/egel/k8s-gitlab-runner</a>.</p>

<p>To illustrate the configuration, I’m using my personal home cluster, which consists of three nodes. I’ve allocated two nodes specifically for the Runners. The goal is to set up two active Runners with a total of four concurrent jobs, resulting in eight concurrent jobs overall – a setup that should be suitable for most use cases.</p>

<p>Each node in the cluster has 2 CPUs and 16GB of RAM. For building and pushing multiple final Docker images to a registry, I’ve provisioned 4GB of memory per job, which I believe is sufficient.</p>

<p>Enough with the setup details, let’s dive into defining the <code class="language-plaintext highlighter-rouge">gitlab-runner-values.yaml</code> file. I’ll break down each part in the following steps to provide a comprehensive understanding.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="c1"># GitLab connection</span>
<span class="na">gitlabUrl</span><span class="pi">:</span> <span class="s">https://gitlab.com/</span> <span class="c1"># or your self-hosted URL</span>
<span class="na">runnerToken</span><span class="pi">:</span> <span class="s2">"</span><span class="s">PLACE</span><span class="nv"> </span><span class="s">YOUR</span><span class="nv"> </span><span class="s">GITLAB</span><span class="nv"> </span><span class="s">RUNNER</span><span class="nv"> </span><span class="s">TOKEN</span><span class="nv"> </span><span class="s">HERE"</span> <span class="c1"># paste your token from previous steps or better use a secret (see desc below)</span>

<span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>

<span class="c1"># Runner image</span>
<span class="na">image</span><span class="pi">:</span>
  <span class="na">registry</span><span class="pi">:</span> <span class="s">docker.io</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">gitlab/gitlab-runner</span>
  <span class="na">tag</span><span class="pi">:</span> <span class="s">ubuntu</span>

<span class="c1"># Scaling</span>
<span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span> <span class="c1"># 2 manager pods for HA</span>
<span class="na">concurrent</span><span class="pi">:</span> <span class="m">4</span> <span class="c1"># each manager handles 4 jobs max</span>

<span class="na">checkInterval</span><span class="pi">:</span> <span class="m">15</span> <span class="c1"># define in sec how often the runner checks for new jobs</span>

<span class="c1"># Security - in some helm/kubernetes collections there is problem with permissions</span>
<span class="c1"># like "mkdir: cannot create directory '/home/gitlab-runner': Permission denied".</span>
<span class="c1"># See: https://docs.gitlab.com/runner/install/kubernetes_helm_chart_configuration/#switch-to-the-ubuntu-based-gitlab-runner-docker-image)</span>
<span class="na">securityContext</span><span class="pi">:</span>
  <span class="na">fsGroup</span><span class="pi">:</span> <span class="m">999</span>
  <span class="na">runAsUser</span><span class="pi">:</span> <span class="m">999</span>

<span class="na">serviceAccount</span><span class="pi">:</span>
  <span class="na">create</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">rbac</span><span class="pi">:</span>
  <span class="na">create</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">clusterWideAccess</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">events"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">list"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">watch"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">namespaces"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">delete"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">pods"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">delete"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">get"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
      <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">pods/attach"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pods/exec"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">create"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">patch"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">delete"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">"</span><span class="pi">]</span>
      <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">pods/log"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">list"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">secrets"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">delete"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">get"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">update"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">serviceaccounts"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">get"</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">services"</span><span class="pi">]</span>
      <span class="na">verbs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">create"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">get"</span><span class="pi">]</span>

<span class="c1"># Kubernetes executor config</span>
<span class="na">runners</span><span class="pi">:</span>
  <span class="na">config</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">[[runners]]</span>
      <span class="s"># Fix request concurrency for multiple runners</span>
      <span class="s">request_concurrency = 3  # Default: only 3 request at a time</span>
      <span class="s">limit = 10               # Can handle 10 jobs, but only 3 request slot</span>
    <span class="no">  </span>
      <span class="s">[runners.kubernetes]</span>
        <span class="s">namespace = ""</span>
        <span class="s">image = "alpine:latest"</span>

        <span class="s"># Resource limits (per job pod)</span>
        <span class="s">cpu_limit = "1"</span>
        <span class="s">memory_limit = "4Gi"</span>
        <span class="s">cpu_request = "500m"</span>
        <span class="s">memory_request = "512Mi"</span>

        <span class="s"># Service containers (e.g., DinD)</span>
        <span class="s">service_cpu_limit = "1"</span>
        <span class="s">service_memory_limit = "4Gi"</span>

        <span class="s"># Enable privileged mode (needed for Docker-in-Docker)</span>
        <span class="s">privileged = true</span>
</code></pre></div></div>

<p>The configuration is pretty straightforward. First, we define credentials for connecting to your GitLab instance. I strongly recommend storing these tokens within a Kubernetes secret object for best practices. Refer to the official documentation for details on setting up the <code class="language-plaintext highlighter-rouge">certsSecretName</code>. You can certainly use plain text tokens initially and revisit this later once everything is up and running.</p>

<p>Next, you specify the Docker image your Runner will use. I personally used <code class="language-plaintext highlighter-rouge">ubuntu</code> as a base image.</p>

<blockquote>
  <p><strong>Pro Tip:</strong> This is a great place to save time and resources! You can build a custom Docker image based on your chosen base image and pre-install all the software you commonly use when working on jobs. This avoids repetitive installations and reduces bandwidth consumption. This approach leverages the “Docker-in-Docker” (DinD) concept. I created a custom image specifically for this purpose that you might find useful. It’s available on Docker Hub: <a href="https://hub.docker.com/r/egel/docker-for-gitlab-ci">https://hub.docker.com/r/egel/docker-for-gitlab-ci</a>. While it might not always be the absolute latest base image, it’s a solid starting point. Feel free to explore, modify, and adapt it to your needs – it’s all public!</p>
</blockquote>

<p>In the security section, we’ll explore the necessary permissions for the Runner to execute jobs. I found it essential to set up the <code class="language-plaintext highlighter-rouge">securityContext</code> with a value of <code class="language-plaintext highlighter-rouge">999</code> during my setup. You can find more information on this in the official documentation: <a href="https://docs.gitlab.com/runner/install/kubernetes_helm_chart_configuration/#switch-to-the-ubuntu-based-gitlab-runner-docker-image">https://docs.gitlab.com/runner/install/kubernetes_helm_chart_configuration/#switch-to-the-ubuntu-based-gitlab-runner-docker-image</a>.</p>

<p>For better organization and easier maintenance, I recommend creating a dedicated service account. Implementing the recommended RBAC rules from the official documentation is also a good idea. See the docs here <a href="https://docs.gitlab.com/runner/executors/kubernetes/#configure-runner-api-permissions">https://docs.gitlab.com/runner/executors/kubernetes/#configure-runner-api-permissions</a> or extract them from sample chart values with:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show values gitlab/gitlab-runner

<span class="c"># or save the output to sample file for convenient usage</span>
helm show values gitlab/gitlab-runner <span class="o">&gt;</span> default-values.yaml
</code></pre></div></div>

<p>Finally, if you’re using a multi-tenant configuration, pay attention to adjusting the <code class="language-plaintext highlighter-rouge">requestConcurrency</code> and <code class="language-plaintext highlighter-rouge">limit</code> settings. More details are available in the official documentation: <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration/#configuration-warnings">https://docs.gitlab.com/runner/configuration/advanced-configuration/#configuration-warnings</a>. Otherwise, you might encounter a warning in your logs during the final setup stages like this one below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WARNING: CONFIGURATION: Long polling issues detected.
Issues found:
  - Request bottleneck: 1 runners have request_concurrency=1, causing job delays during long polling
This can cause job delays matching your GitLab instance's long polling timeout.
Recommended solutions:
  1. Increase 'request_concurrency' to 2-4 for 1 runners currently using request_concurrency=1
Note: The 'FF_USE_ADAPTIVE_REQUEST_CONCURRENCY' feature flag can help automatically adjust request_concurrency based on workload.
This message will be printed each time the configuration is reloaded if the issues persist.
See documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#long-polling-issues  builds=0 max_builds=4
</code></pre></div></div>

<h4 id="install-gitlab-runner-from-the-helm-chart">Install gitlab-runner from the Helm chart</h4>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># check the version of gitlab-runner helm chart and pick one</span>
helm search repo <span class="nt">-l</span> gitlab/gitlab-runner

<span class="c"># install the runner (I manually pick the latest version, but dropping it will install latest as well)</span>
helm <span class="nb">install </span>gitlab-runner <span class="nt">--namespace</span> gitlab <span class="nt">--version</span> 0.84.2 <span class="nt">-f</span> gitlab-runner-values.yaml gitlab/gitlab-runner
</code></pre></div></div>

<p>the runner should schedule all objects we need. Now let’s watch what we have got back in k8s namespace:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get pods <span class="nt">--namespace</span> gitlab
NAME                           READY   STATUS    RESTARTS   AGE
gitlab-runner-fcd689ff-hh8nb   1/1     Running   0          73m
gitlab-runner-fcd689ff-kwpgm   1/1     Running   0          73m
</code></pre></div></div>

<p>Now, let’s check what logs says about our installation to be sure all works as expected:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs <span class="nt">-n</span> gitlab <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>gitlab-runner <span class="nt">--tail</span><span class="o">=</span>20
</code></pre></div></div>

<p>If you see not errors that’s amazing! That will be all for the runner configuration. Now we can setup your project’s <code class="language-plaintext highlighter-rouge">gitlab-ci.yaml</code> file, to trigger our pipeline and test the project.</p>

<h3 id="gitlab-ci-yaml">Gitlab CI yaml</h3>

<p>As mentioned shorly before, for this part we will need to setup the <code class="language-plaintext highlighter-rouge">gitlab-ci.yaml</code> for our project. Let’s take a very simple example and define few jobs that will be started at our first stage called <code class="language-plaintext highlighter-rouge">test</code> (other stages I left commented out and as they are not part of this, but you might like to explore them later).</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">docker:29-dind</span> <span class="c1"># or use my image https://hub.docker.com/repository/docker/egel/docker-for-gitlab-ci</span>

<span class="na">variables</span><span class="pi">:</span>
  <span class="na">NODE_IMAGE</span><span class="pi">:</span> <span class="s">node:24-alpine3.23</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">docker:dind</span>

<span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">test</span>
  <span class="c1"># - build   # this stage is usually there but not part of this tutorial</span>
  <span class="c1"># - deploy  # this stage is usually there but not part of this tutorial</span>

<span class="c1">####################################</span>
<span class="c1"># JOBS</span>
<span class="c1">####################################</span>

<span class="c1">#</span>
<span class="c1"># Shell scripts</span>
<span class="c1">#</span>
<span class="s">test:shellscripts:</span>
  <span class="s">image</span><span class="err">:</span> <span class="s">koalaman/shellcheck-alpine:v0.10.0</span>
  <span class="s">stage</span><span class="err">:</span> <span class="s">test</span>
  <span class="s">interruptible</span><span class="err">:</span> <span class="no">true</span>
  <span class="s">script</span><span class="err">:</span>
    <span class="pi">-</span> <span class="s">find . -not -path "*node_modules*" -name "*.sh" | while IFS= read -r fpath; do shellcheck -x "${fpath}"; done</span>

<span class="c1">#</span>
<span class="c1"># Dockerfile</span>
<span class="c1">#</span>
<span class="s">test:dockerfile:</span>
  <span class="s">stage</span><span class="err">:</span> <span class="s">test</span>
  <span class="s">image</span><span class="err">:</span> <span class="s">hadolint/hadolint:v2.12.0-debian</span>
  <span class="s">interruptible</span><span class="err">:</span> <span class="no">true</span>
  <span class="s">script</span><span class="err">:</span>
    <span class="pi">-</span> <span class="s">find . -not \( -path *node_modules* -prune \) -name "Dockerfile*" -print0 | xargs -0 hadolint --config .hadolint.yml</span>

<span class="c1">#</span>
<span class="c1"># webapp lint</span>
<span class="c1">#</span>
<span class="s">test:webapp:lint:</span>
  <span class="s">stage</span><span class="err">:</span> <span class="s">test</span>
  <span class="s">image</span><span class="err">:</span> <span class="s">$NODE_IMAGE</span>
  <span class="s">interruptible</span><span class="err">:</span> <span class="no">true</span>
  <span class="s">variables</span><span class="err">:</span>
    <span class="na">PUPPETEER_SKIP_DOWNLOAD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="err">  </span><span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">npm ci --cache .npm --prefer-offline</span>
    <span class="pi">-</span> <span class="s">npm run lint</span>
</code></pre></div></div>

<p>If we create a file, commit it to the repository, and then create a new branch and push the changes (ideally, also create a merge request from that branch), we should see an example pipeline similar to those shown in the screenshots below.</p>

<p><img alt="" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-sample-pipeline-tests.png" width="100%" height="auto" /></p>

<p><img alt="" src="/assets/posts/installing-gitlab-runner-on-home-k8s-cluster-with-helm/gitlab-com-sample-pipeline-tests-single-job.png" width="100%" height="auto" /></p>

<h2 id="conclusion">Conclusion</h2>

<p>Overall, I think this is a really excellent and cost-effective solution for a solo developer. If you’re already running servers for your projects, spinning up a cluster and leveraging their resources is a smart move. Running a GitLab Runner doesn’t necessitate a static IP address, thanks to the pull principle. So, you don’t <em>need</em> one.</p>

<p>Of course, there are scenarios where running everything on a home server might not be the best approach compared to dedicated servers with constant internet connectivity. This is especially true when using public runners, as you can’t guarantee access if something goes wrong. I’ve found that having a private runner for a solo developer is a fantastic solution. Being able to utilize the power of your own small computers and gain hands-on experience while working on your own projects is truly invaluable.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://charts.gitlab.io/">https://charts.gitlab.io/</a></li>
  <li><a href="https://docs.gitlab.com/runner/install/kubernetes_helm_chart_configuration/">https://docs.gitlab.com/runner/install/kubernetes_helm_chart_configuration/</a></li>
  <li><a href="https://docs.gitlab.com/runner/executors/kubernetes/#configure-runner-api-permissions.">https://docs.gitlab.com/runner/executors/kubernetes/#configure-runner-api-permissions.</a></li>
  <li><a href="https://medium.com/@guqung6/gitlab-runner-on-kubernetes-a-complete-guide-22f4db87218b">https://medium.com/@guqung6/gitlab-runner-on-kubernetes-a-complete-guide-22f4db87218b</a></li>
</ul>]]></content><author><name>Maciej Sypien</name></author><category term="linux" /><category term="debian" /><category term="gitlab" /><category term="gitlab-runner" /><category term="kubernetes" /><category term="k8s" /><category term="helm" /><category term="cicd" /><category term="devops" /><category term="docker" /><summary type="html"><![CDATA[I’ve been exploring Kubernetes and GitLab for quite some time. Until recently, I’ve relied on GitHub’s free runners or GitLab runners hosted on private instances. I’ve often using manually installed executables directly on production servers to automate my deployment processes. However, this approach presented a few challenges.]]></summary></entry><entry><title type="html">Installing the Latest Neovim on Debian 12</title><link href="https://egel.github.io/2025/09/17/nvim-complete-install-on-debian-12.html" rel="alternate" type="text/html" title="Installing the Latest Neovim on Debian 12" /><published>2025-09-17T00:00:00+00:00</published><updated>2025-09-17T00:00:00+00:00</updated><id>https://egel.github.io/2025/09/17/nvim-complete-install-on-debian-12</id><content type="html" xml:base="https://egel.github.io/2025/09/17/nvim-complete-install-on-debian-12.html"><![CDATA[<p>Sometime ago, I was installing new servers with Debian and encountered a well-known dilemma for Debian users: having older versions of some programs in the standard repository. This isn’t necessarily a bad thing. It’s a characteristic of Debian’s commitment to stability over having the absolute latest package versions. However, in this case, I wanted a more up-to-date version of Neovim for development, local debugging, and efficient administration.</p>

<p>This post will guide you through installing the latest version of Neovim on Debian 12 for the <code class="language-plaintext highlighter-rouge">amd64</code> architecture using the <code class="language-plaintext highlighter-rouge">apt</code> package manager.</p>

<p>At the time of writing, the version of Neovim in Debian 12 is <code class="language-plaintext highlighter-rouge">v0.7.2-7</code>, which is significantly behind the latest stable version, <code class="language-plaintext highlighter-rouge">v0.11.5</code>. By the time you read this, it might be even newer.</p>

<p>The installation process is straightforward but requires installing additional software to compile Neovim from source. Don’t panic! Compiling from source isn’t rocket science, and I’ll guide you through each step. Plus, you’ll learn how to update your Neovim version quickly, allowing you to keep your system up-to-date.</p>

<h2 id="before-you-start">Before You Start</h2>

<p>Before we begin, remove any existing Neovim installation from your system. This is essential because attempting to install from source with Neovim already installed can result in errors like “nvim is already installed.” This typically happens when Neovim was previously installed using <code class="language-plaintext highlighter-rouge">apt</code>, <code class="language-plaintext highlighter-rouge">apt-get</code>, or another package manager. If so, you <em>must</em> uninstall it before compiling from source.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check if you have Neovim already installed</span>
which nvim

<span class="c"># Uninstall the current Neovim</span>
<span class="nb">sudo </span>apt remove neovim
<span class="nb">sudo </span>apt autoremove <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt clean
</code></pre></div></div>

<h2 id="installation-from-source">Installation from Source</h2>

<p>Installing from source is relatively simple. We need the source files and additional packages required for compilation.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install required packages</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>ninja-build gettext cmake curl build-essential git

<span class="c"># Optional:  Useful for copying text to the system clipboard</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>xclip
</code></pre></div></div>

<p>Next, clone the source files into a local directory. I prefer cloning for better repository management, but you can also download the zip file and unpack it in a directory (e.g., ~/Downloads/neovim).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone the source files</span>
<span class="nb">cd</span> ~/Downloads
git clone https://github.com/neovim/neovim
<span class="nb">cd </span>neovim
git checkout stable
</code></pre></div></div>

<p>Now for the core part. We’ll prepare the repository for building and create a Debian package directly from the source files. This will create a final package ready for installation.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make <span class="nv">CMAKE_BUILD_TYPE</span><span class="o">=</span>Release
<span class="nb">cd </span>build <span class="o">&amp;&amp;</span> cpack <span class="nt">-G</span> DEB
<span class="nb">sudo </span>dpkg <span class="nt">-i</span> nvim-linux-x86_64.deb
</code></pre></div></div>

<p>That’s it! You can now enjoy your newly installed Neovim editor.</p>

<h2 id="appendix-additional-languages-and-packages">Appendix: Additional Languages and Packages</h2>

<p>When working with Vim or Neovim, I find certain packages exceptionally useful. Here are some categories and programs I frequently use with my Neovim configuration (https://github.com/egel/dotfiles/tree/main/configuration/.config/nvim).</p>

<h3 id="fuzzing">Fuzzing</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>fzf
<span class="nb">sudo </span>apt <span class="nb">install </span>ripgrep <span class="c"># for search with fzf</span>
</code></pre></div></div>

<h3 id="lua">Lua</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>lua5.4
<span class="nb">sudo </span>apt <span class="nb">install </span>luarocks
</code></pre></div></div>

<h3 id="golang">Golang</h3>

<p>The easiest way to install the latest version of Golang is to use the precompiled binary. I’m using version v1.25.1, although this might be outdated when you read this. Check https://go.dev/dl/ for the latest version.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Update system packages</span>
<span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt upgrade

<span class="c"># Download and unpack the binary</span>
<span class="nb">cd</span> ~/Downloads
wget https://golang.org/dl/go1.25.1.linux-amd64.tar.gz
<span class="nb">sudo tar</span> <span class="nt">-C</span> /usr/local <span class="nt">-xzf</span> go1.25.1.linux-amd64.tar.gz

<span class="c"># Add to .profile or .zshrc</span>
<span class="nb">echo</span> <span class="s2">"export PATH=/usr/local/go/bin:</span><span class="k">${</span><span class="nv">PATH</span><span class="k">}</span><span class="s2">"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> <span class="nv">$HOME</span>/.profile

<span class="c"># Reload profile to activate the changes</span>
<span class="nb">source</span> <span class="nv">$HOME</span>/.profile
</code></pre></div></div>

<h3 id="install-rust-and-cargo-with-tree-sitter">Install Rust and Cargo (with tree-sitter)</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://sh.rustup.rs <span class="nt">-sSf</span> | sh

<span class="c"># Install tree-sitter</span>
cargo <span class="nb">install </span>tree-sitter-cli
</code></pre></div></div>

<h3 id="node--typescript">Node + Typescript</h3>

<p>Typescript is used by tree-sitter for installing additional syntax highlighting. To install Typescript, we need Node. I use nvm to manage Node versions. Version 22 is the LTS version at the time of writing.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-o-</span> https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm <span class="nb">install </span>22
nvm <span class="nb">alias </span>default 22

<span class="c"># Update npm to the latest version</span>
npm <span class="nb">install</span> <span class="nt">-g</span> npm@latest

<span class="c"># Install Typescript</span>
npm <span class="nb">install</span> <span class="nt">-g</span> typescript
</code></pre></div></div>

<p>The full configuration for <code class="language-plaintext highlighter-rouge">nvm</code> variables resides in my <code class="language-plaintext highlighter-rouge">.zshrc</code> file.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test in nvim
:echo exepath("node")
:echo exepath("npm")
</code></pre></div></div>

<h3 id="python">Python</h3>

<p>Install a global version at the system level.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>python3 pipx
</code></pre></div></div>

<p>I also install <code class="language-plaintext highlighter-rouge">pyenv</code> for better Python version management.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://pyenv.run | bash

<span class="c"># Required when later compiling Python versions from source</span>
<span class="nb">sudo </span>apt-get <span class="nb">install </span>libbz2-dev liblzma-dev

<span class="c"># Update pyenv</span>
pyenv update

<span class="c"># List all available Python versions to install</span>
pyenv <span class="nb">install</span> <span class="nt">--list</span>

<span class="c"># Install a Python binary</span>
pyenv <span class="nb">install </span>3.12.11

<span class="c"># Set the global version</span>
pyenv global 3.12.11

<span class="c"># Verify the global version is set correctly</span>
<span class="nv">$ </span>pyenv global
3.12.11

<span class="c"># Verify the Python version in use</span>
<span class="nv">$ </span>which python
/Users/johndoe/.pyenv/shims/python

<span class="nv">$ </span>python <span class="nt">--version</span>
Python 3.12.11
</code></pre></div></div>

<p>And that’s the end of the article. I hope you enjoyed it! If you have any questions or comments, feel free to leave them down below.</p>]]></content><author><name>Maciej Sypien</name></author><category term="linux" /><category term="debian" /><category term="nvim" /><summary type="html"><![CDATA[Sometime ago, I was installing new servers with Debian and encountered a well-known dilemma for Debian users: having older versions of some programs in the standard repository. This isn’t necessarily a bad thing. It’s a characteristic of Debian’s commitment to stability over having the absolute latest package versions. However, in this case, I wanted a more up-to-date version of Neovim for development, local debugging, and efficient administration.]]></summary></entry><entry><title type="html">Complete guide to install and configure Debian 12</title><link href="https://egel.github.io/2025/06/11/complete-guide-to-install-and-configure-debian-12.html" rel="alternate" type="text/html" title="Complete guide to install and configure Debian 12" /><published>2025-06-11T00:00:00+00:00</published><updated>2025-06-11T00:00:00+00:00</updated><id>https://egel.github.io/2025/06/11/complete-guide-to-install-and-configure-debian-12</id><content type="html" xml:base="https://egel.github.io/2025/06/11/complete-guide-to-install-and-configure-debian-12.html"><![CDATA[<p>In one of my previous posts, I created a <a href="/2024/08/26/home-k8s-cluster.html">Home Kubernetes Cluster</a>. The experience from that event gave me valuable insights to try something different from the most popular distributions like Ubuntu. My goal remains simple: ease of operation, high performance, and stability without many problems. After considering various options, I decided to switch to <strong>Debian</strong>.</p>

<p>In this article, I will walk you through a full, fresh installation and preparation of Debian on my HP EliteDesk 800 G3, which I’ll refer to as Elitedesk or PC interchangeably. This groundwork will provide a solid foundation for our home Kubernetes cluster <a href="/2024/08/26/home-k8s-cluster.html">Home Kubernetes Cluster</a></p>

<h2 id="prerequisites">Prerequisites</h2>

<p><strong>Hardware</strong>:</p>

<ul>
  <li>USB stick with minimum 4GB or more</li>
  <li>PC (I used one from spec below):
    <ul>
      <li>Model: HP Elitedesk 800 G3 DM 35W</li>
      <li>CPU: Intel(R) Core(TM) i5-6500T CPU @ 2.50GHz</li>
      <li>Mem: 16384 MB</li>
    </ul>
  </li>
  <li>Additional screen</li>
  <li>Additional keyboard</li>
</ul>

<p><strong>Software</strong>:</p>

<ul>
  <li>Etcher (to flash the ISO image)</li>
  <li>Debian ISO image</li>
</ul>

<h2 id="prepare-the-usb-stick">Prepare the USB stick</h2>

<h3 id="step-1">Step 1</h3>

<p>Download the Debian ISO image from the official <a href="https://www.debian.org/distrib/">Debian download</a> page. At the time of writing this article, I used the <strong>Debian 12.11.0 Bookworm</strong> Iso-DVD image.</p>

<p>Choose a mirror closest to your location and download the image accordingly. Here you can <a href="https://www.debian.org/CD/http-ftp/#mirrors">select the mirror</a> closest to your location and download image.</p>

<p>I consider downloading the <code class="language-plaintext highlighter-rouge">amd64</code> architecture image since my Elitedesk 800 has an <a href="https://www.intel.com/content/www/us/en/products/sku/88183/intel-core-i56500t-processor-6m-cache-up-to-3-10-ghz/specifications.html">Intel i5 6500T</a> chip with 64-bit instructions set.</p>

<p>Since my Elitedesk 800 is has chip on board and runs with 64-bit instructions set, therefore I chose image for <code class="language-plaintext highlighter-rouge">amd64</code> architecture.</p>

<p>For reference, you can check out this sample link: <a href="https://ftp.uni-hannover.de/debian/debian-cd/12.11.0/amd64/iso-dvd/">debian-12.11.0-amd64-DVD-1.iso</a></p>

<h3 id="step-2">Step 2</h3>

<p>Using <a href="https://etcher.balena.io/">Etcher</a> to install the downloaded ISO image on your USB stick. This process may take several minutes depending on your USB stick’s quality and speed.</p>

<p>If this is done, your ready for next part.</p>

<h2 id="installation">Installation</h2>

<h3 id="step-1-prepeare-server-for-installation">Step 1: Prepeare server for installation</h3>

<p>Connect your server (in my case it’s Elitedesk 800) to power, add an external monitor, keyboard, and USB stick containing the Debian ISO image.</p>

<p>Power on the PC and while booting start constatnly pressing <code class="language-plaintext highlighter-rouge">F10</code> until you will enter the BIOS menu (if you use different PC, check the manual by searching for example: <em>“How to enter BIOS on X”</em>).</p>

<h3 id="step-2-configure-bios">Step 2: Configure BIOS</h3>

<p>After entering BIOS you should get something like this.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/hp-bios-main-menu.png" alt="HP BIOS Welcome screen" /></p>

<p>Using arrow keys on your keyboard, go to <strong>Advanced</strong> section and enter <strong>Boot Options</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/hp-bios-advanced-menu.png" alt="HP BIOS Advanced menu" /></p>

<p>Select the <strong>USB Storage boot</strong> option, so you can boot up from the USB stick you installed.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/hp-bios-boot-options-menu.png" alt="HP BIOS Boot options menu" /></p>

<p>Go to back to <strong>Main</strong> and <strong>Save Changes and Exit</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/hp-bios-main-menu-save-and-exit.png" alt="HP BIOS Main Menu - Save and exit" /></p>

<p>While the PC will start booting again, similar song like before, while booting start constatnly pressing <code class="language-plaintext highlighter-rouge">F9</code> until you will enter to <strong>quick boot menu</strong>, then select our new stick <strong>UEFI - Generic Mass Storage</strong> (this name might be different depend on what kind of USB stick you will use).</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/hp-boot-menu-select-usb.png" alt="HP Boot menu - Select USB" /></p>

<h3 id="step-3-debian-installation">Step 3: Debian installation</h3>

<p>Instalator will greet us with a nice menu. I selected <strong>Install</strong> which is standard TUI (Terminal User Interface) and continue.</p>

<blockquote>
  <p>Although if you prefer, you can also choose more modern, <strong>Graphical install</strong>.</p>
</blockquote>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-main-menu.png" alt="Debian installer - Main Menu" /></p>

<p><strong>Select system language</strong>. I simply go with <strong>English</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-select-language.png" alt="" /></p>

<p><strong>Select location</strong>. I go with <strong>Other</strong> &gt; <strong>Europe</strong> &gt; <strong>Germany</strong>.
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-select-location.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-select-location-2.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-select-location-3.png" alt="" /></p>

<p><strong>Select locales</strong>. I go with <strong>United States en_US.UTF-8</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-configure-locales.png" alt="" /></p>

<p>Setup <strong>root user</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password-2.png" alt="" /></p>

<p>Setup <strong>regular user</strong> (in my case I will go with <code class="language-plaintext highlighter-rouge">maciej</code>).</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password-3.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password-4.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password-5.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-setup-user-and-password-6.png" alt="" /></p>

<p>Let’s jump to next section and establish new <strong>discs partition</strong>.</p>

<p>I will simplify the process to going with the <strong>Guided - use entire disc</strong> option and its defaults - as of the purpose of this article this will be sufficient.</p>

<div class="alert alert-success">
Although if we would not go with server for Kubernetes, the best practice would be to use more space for the <code>swap</code> partition. In this case, the minimum would be the RAM size, and it's recommended to double the RAM size for optimal performance.
</div>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition-2.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition-3.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition-4.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition-5.png" alt="" />
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-disc-partition-6.png" alt="" /></p>

<p><strong>Configure package manager</strong>. Now we get question <strong>Continue without a network mirror?</strong> and for now I go with <strong>Yes</strong>. We will handle this later.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-configure-package-manager.png" alt="" /></p>

<p><strong>Configure popularity contest</strong>. I’ll choose <strong>No</strong>, although for the authors selecting this option would be benefitial to collect some metric data available at <a href="https://popcon.debian.org">https://popcon.debian.org</a>. We can change the mind later by running <code class="language-plaintext highlighter-rouge">dpkg-reconfigure popularity-contest</code>.
<img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-configure-popularity-contest.png" alt="" /></p>

<p><strong>Software selection</strong>. Since we’re focusing on server configuration, we don’t need a graphical interface.</p>

<p>Important packages to select that will make our life easier are:</p>

<ul>
  <li><strong>SSH server</strong> - helpful for easily establishing the first connection</li>
  <li><strong>Standard system utilities</strong> - pack of default programs</li>
</ul>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-software-selection.png" alt="" />
We are now boot up again and we are ready for configuration.</p>

<p><strong>Finish</strong>. Let’s now complete the installation. At this point we should <strong>take out our USB stick</strong> and then press <strong>Continue</strong>. After this the system will boot up and we are ready for next section - configuration.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-installer-finish.png" alt="" /></p>

<p>Finally we got the Debian login screen. Great!</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/debian-login-screen.png" alt="" /></p>

<h2 id="configuration">Configuration</h2>

<p>We come along come a long way already. Since we installed our system with preselected option to install <strong>SSH server</strong> we can login as our new user and can check the IP given by DHCP server with <code class="language-plaintext highlighter-rouge">ip addr</code>.
At this point technically if we know IP we can try using your laptop and SSH directly to it via <code class="language-plaintext highlighter-rouge">ssh maciej@192.168.178.X</code> and provide the password.</p>

<p>If not you can still use currently connected setup (monitor and keyboad) until we establish fixed IP adress.</p>

<h3 id="setup-ip-address">Setup IP address</h3>

<p>Depend on your resources there are 2 ways we want to setup IP address.</p>

<ul>
  <li><strong>Dynamic</strong>, via assigning static IP via router DHCP settings (you may need MAC address of your device)</li>
  <li><strong>Static</strong> , via old school assigning static IP address</li>
</ul>

<p>In this tutorial I will go with <strong>dynamic</strong> assign, although will show both ways if you decide going with static IP addresses.</p>

<h4 id="dynamic-via-router-static-ip">Dynamic via router static IP</h4>

<p>Using FritzBox, login to your router admin panel <a href="http://fritz.box/">http://fritz.box/</a></p>

<p>At first you might get error with “Your connection is not private”. This is expected with local router configuration. Simply accept by click “Advanced”, and continue “Proceed to fritz.box (unsafe)”.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/fritzbox-connection-is-not-private.png" alt="" /></p>

<p>Let’s jump to configuration:</p>

<p>Go to <strong>Home Network</strong> and find and open <code class="language-plaintext highlighter-rouge">cplane1</code> device.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/fritzbox-network-devicelist.png" alt="" /></p>

<p>In tab <strong>Network</strong>, select desired IP, I used <code class="language-plaintext highlighter-rouge">XXX.YYY.ZZZ.200</code> and enable <strong>Assign permanent IPv4 address</strong>.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/fritzbox-device-homenetwork-permament-ipv4.png" alt="" /></p>

<p>If you cannot find it as have many devices you can check the MAC address after logging in with command <code class="language-plaintext highlighter-rouge">ip addr</code> (usually it come after word <code class="language-plaintext highlighter-rouge">ether</code> in some networks).</p>

<h4 id="static-ip-address">Static IP address</h4>

<p>If you did not choose dynamic IP via router, you are must setup static IP address directly on the machine.</p>

<ol>
  <li>Create backup <code class="language-plaintext highlighter-rouge">/etc/network/interfaces</code> file running <code class="language-plaintext highlighter-rouge">sudo cp /etc/network/interfaces /etc/network/interfaces.bak</code></li>
  <li>Edit the <code class="language-plaintext highlighter-rouge">sudo vi /etc/network/interfaces</code></li>
  <li>Configure static IP address for <code class="language-plaintext highlighter-rouge">enp0s5</code> Ethernet interface: address <code class="language-plaintext highlighter-rouge">192.168.178.249</code></li>
  <li>Add subnet mask: <code class="language-plaintext highlighter-rouge">netmask 255.255.255.0</code></li>
  <li>Set up default gateway IP: <code class="language-plaintext highlighter-rouge">gateway 192.168.2.254</code></li>
  <li>Finally add DNS resolver IP: <code class="language-plaintext highlighter-rouge">dns-nameservers 192.168.2.254 8.8.8.8 8.8.4.4</code></li>
</ol>

<h3 id="ssh-service">SSH service</h3>

<p>During system installation we explicitly select an option to install <strong>SSH server</strong>, so after the installation, the PC should be reachable via SSH.</p>

<p>Let’s test it and login with our regular user using external laptop:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># from your laptop (or other control PC)</span>
ssh maciej@192.168.178.200
</code></pre></div></div>

<p>If you can reach it, great this part is done and you can detach monitor and keyboard, as we can connect to PC via remote connection. Let’s jump to next section <strong>Update broken update</strong>.</p>

<h4 id="enable-ssh">Enable SSH</h4>

<p>First thing we want to do is enable the remote connection via SSH. We should do this first because it will allow us to use our laptop’s terminal to connect to server, rather than using attached peripherals (monitors, keyboard).</p>

<p>At this point, our system is quite bare-bones. We probably don’t even have the sudo program installed. But no worries; we’ll take care of that later. Let’s first enable SSH. During installation, we explicitly select to install the SSH server, so the service should be working or we should be able to start it.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># after login to system, switch user to be root</span>
su -

<span class="c"># check SSH status</span>
systemctl status ssh

<span class="c"># enable SSH service</span>
systemctl start ssh

<span class="nb">logout</span>
</code></pre></div></div>

<p>At this point we should be ready to login via SSH from our laptop. The port is <code class="language-plaintext highlighter-rouge">22</code> as we not change it and we may adapt this later.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># from your laptop (or other control PC)</span>
ssh maciej@192.168.178.200
</code></pre></div></div>

<p>Success! You can now disconnect the monitor and keyboard, and focus on your laptop.</p>

<p>At this point, if you have more servers to configure, do it. You can reproduce same steps at this moment also for other machines but keep using different IP addresses . Later all together configuration can be perform on all machines at the same time with <a href="https://docs.ansible.com/">Ansible</a>.</p>

<h3 id="update-broken-update">Update broken update</h3>

<p>Using the ISO image have some downside in our approach, as the confiruation is meant to be pulled from mirror. We skipped this, but now will be much easier to enter respective entires (simply by pasting them. In the terminal that would not be possible)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># regular user does not have sudo permissions so change to root</span>
<span class="nv">$ </span>su -

root@cplane1:~# apt update
Ign:1 cdrom://[Debian GNU/Linux 12.11.0 _Bookworm_ - Official amd64 DVD Binary-1 with firmware 20250517-09:52] bookworm InRelease
Err:2 cdrom://[Debian GNU/Linux 12.11.0 _Bookworm_ - Official amd64 DVD Binary-1 with firmware 20250517-09:52] bookworm Release
  Please use apt-cdrom to make this CD-ROM recognized by APT. apt-get update cannot be used to add new CD-ROMs
Reading package lists... Done
E: The repository <span class="s1">'cdrom://[Debian GNU/Linux 12.11.0 _Bookworm_ - Official amd64 DVD Binary-1 with firmware 20250517-09:52] bookworm Release'</span> does not have a Release file.
N: Updating from such a repository can<span class="s1">'t be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
</span></code></pre></div></div>

<p>If you see this above, most likely you must update you source list from <code class="language-plaintext highlighter-rouge">/etc/apt/source.list</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@cplane1:~# <span class="nb">cat</span> /etc/apt/sources.list
deb cdrom:[Debian GNU/Linux 12.11.0 _Bookworm_ - Official amd64 DVD Binary-1 with firmware 20250517-09:52]/ bookworm contrib main non-free-firmware
</code></pre></div></div>

<p>Edit file <code class="language-plaintext highlighter-rouge">vi /etc/apt/sources.list</code>, remove all, and paste something like this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@cplane1:~# <span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">' | tee /etc/apt/sources.list
deb https://ftp.debian.org/debian/ bookworm contrib main non-free non-free-firmware
# deb-src https://ftp.debian.org/debian/ bookworm contrib main non-free non-free-firmware

deb https://ftp.debian.org/debian/ bookworm-updates contrib main non-free non-free-firmware
# deb-src https://ftp.debian.org/debian/ bookworm-updates contrib main non-free non-free-firmware

deb https://ftp.debian.org/debian/ bookworm-proposed-updates contrib main non-free non-free-firmware
# deb-src https://ftp.debian.org/debian/ bookworm-proposed-updates contrib main non-free non-free-firmware

deb https://ftp.debian.org/debian/ bookworm-backports contrib main non-free non-free-firmware
# deb-src https://ftp.debian.org/debian/ bookworm-backports contrib main non-free non-free-firmware

deb https://security.debian.org/debian-security/ bookworm-security contrib main non-free non-free-firmware
# deb-src https://security.debian.org/debian-security/ bookworm-security contrib main non-free non-free-firmware
</span><span class="no">EOF
</span></code></pre></div></div>

<p>Now, we can update and ready to install software</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@cplane1:~# apt update

<span class="c"># upgrade existing packages</span>
root@cplane1:~# apt upgrade <span class="nt">-y</span>
</code></pre></div></div>

<h3 id="configure-sudo-and-privileges-to-user">Configure sudo and privileges to User</h3>

<p>In order to</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@cplane1:~# apt update

<span class="c"># install sudo</span>
root@cplane1:~# apt <span class="nb">install sudo</span> <span class="nt">-y</span>

<span class="c"># add regular user 'maciej' to sudoers</span>
root@cplane1:~# usermod <span class="nt">-aG</span> <span class="nb">sudo </span>maciej

<span class="c"># test if assigned</span>
root@cplane1:~# <span class="nb">groups </span>maciej
maciej : maciej cdrom floppy <span class="nb">sudo </span>audio dip video plugdev <span class="nb">users </span>netdev

root@cplane1:~# <span class="nb">exit</span>

<span class="nv">$ </span><span class="nb">logout</span>
</code></pre></div></div>

<p>If you are already logged as regular use (in my case is <code class="language-plaintext highlighter-rouge">maciej</code>) you have to logout and login again, so user sudoers group is properly picked.</p>

<p>Last thing we may want to do, is to <strong>include log sudo usage</strong>. Open <code class="language-plaintext highlighter-rouge">sudo visudo</code> and ensure this line is in the file. This will make sure to log all sudo commands to <code class="language-plaintext highlighter-rouge">/var/log/sudo.log</code> file.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Defaults        logfile="/var/log/sudo.log"
</code></pre></div></div>

<p>From now on you can perform all actions, as regular user (<code class="language-plaintext highlighter-rouge">maciej</code>) with sudo privilages. When there will be a need to use particular <code class="language-plaintext highlighter-rouge">root</code> user I will show it with prompt for example <code class="language-plaintext highlighter-rouge">root@cplane1:~#</code>.</p>

<h3 id="configure-hostname-and-timezone">Configure Hostname and Timezone</h3>

<p>In installation we setup this although to make this tutorial complete lets check it:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># test showing host</span>
hostnamectl

<span class="c"># test showing time and date</span>
timedatectl
</code></pre></div></div>

<p>If you anyway want to change it, you can do it with:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>hostnamectl set-hostname cplane1

<span class="nb">sudo </span>timedatectl set-timezone Europe/Berlin
</code></pre></div></div>

<h3 id="enable-automatic-updates">Enable automatic updates</h3>

<p>Depend on who is reading and how hardcore security freak might be. In this tutorial we want that our server perform some security patches automatically.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>unattended-upgrades

<span class="c"># prompt to enable automatic updates</span>
<span class="nb">sudo </span>dpkg-reconfigure <span class="nt">--priority</span><span class="o">=</span>low unattended-upgrades
</code></pre></div></div>

<h3 id="secure-root-account">Secure root account</h3>

<p>Best practice is to disable login as <code class="language-plaintext highlighter-rouge">root</code> user, directly via SSH.</p>

<div class="alert alert-warning">
Before making this change, <b>make sure your regular user you choose has sudo privilages</b>! Otherwise you will need to <a href="https://linuxconfig.org/recover-reset-forgotten-linux-root-password">reset your root password</a>.
</div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># check your regular user (with sudo privilages)</span>
<span class="nv">$ </span><span class="nb">groups </span>maciej

<span class="c"># diable login as root by removing password</span>
<span class="nb">sudo </span>passwd <span class="nt">-l</span> root
</code></pre></div></div>

<h3 id="update-default-ssh-port">Update default SSH port</h3>

<p>Changing the default port for SSH can reduce risk for automated attacks. To do so, open <code class="language-plaintext highlighter-rouge">vi /etc/ssh/sshd_config</code> and update port to desired e.g.: <code class="language-plaintext highlighter-rouge">2222</code>, but use different one if you prefer.</p>

<p>Restart SSH service:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart ssh
</code></pre></div></div>

<h3 id="activate-ssh-log">Activate SSH log</h3>

<p>We usually want to monitor all activity with logging to our server:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install rsyslog</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>rsyslog <span class="nt">-y</span>

<span class="c"># check if service is running</span>
systemctl status rsyslog

<span class="c"># edit config</span>
<span class="nb">sudo </span>vi /etc/rsyslog.confsudo vi /etc/rsyslog.conf
</code></pre></div></div>

<p>Make sure section below is uncommented:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auth,authpriv.*                 /var/log/auth.log
</code></pre></div></div>

<p>Restart service:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart rsyslog
</code></pre></div></div>

<h3 id="setup-firewall">Setup Firewall</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>ufw

<span class="c"># allow SSH connections (if you choose different please use it here)</span>
<span class="nb">sudo </span>ufw allow 2222/tcp

<span class="c"># enable firewall</span>
<span class="nb">sudo </span>ufw <span class="nb">enable</span>

<span class="c"># test status</span>
<span class="nb">sudo </span>ufw status
</code></pre></div></div>

<h4 id="configure-additional-firewall-services">Configure additional firewall services</h4>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># allow HTTP / HTTPS</span>
<span class="nb">sudo </span>ufw allow http
<span class="nb">sudo </span>ufw allow https

<span class="c"># deny Telnet</span>
<span class="nb">sudo </span>ufw deny 23

<span class="c"># allow Rsyslog</span>
ufw allow 514/tcp
ufw allow 514/udp
</code></pre></div></div>

<h3 id="ids---intrusion-detection-systems">IDS - Intrusion detection systems</h3>

<h4 id="fail2ban">Fail2Ban</h4>

<p><a href="https://github.com/fail2ban/fail2ban">Fail2Ban</a> will project your server from the brute-force attacks by banning IP addresses after specified number of failed attempts.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>fail2ban <span class="nt">-y</span>

<span class="c"># copy config as local version</span>
<span class="nb">sudo cp</span> /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

<span class="c"># edit configuration</span>
<span class="nb">sudo </span>vi /etc/fail2ban/jail.local
</code></pre></div></div>

<p>Fragment of <code class="language-plaintext highlighter-rouge">jail.local</code></p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[sshd]</span>
<span class="py">enabled</span>  <span class="p">=</span> <span class="s">true</span>
<span class="py">mode</span>     <span class="p">=</span> <span class="s">normal</span>
<span class="py">port</span>     <span class="p">=</span> <span class="s">ssh</span>
<span class="py">logpath</span>  <span class="p">=</span> <span class="s">%(sshd_log)s</span>
<span class="py">backend</span>  <span class="p">=</span> <span class="s">%(sshd_backend)s</span>
<span class="py">maxretry</span> <span class="p">=</span> <span class="s">5</span>
</code></pre></div></div>

<p>Restart service:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart fail2ban
</code></pre></div></div>

<h4 id="aide">AIDE</h4>

<p>AIDE stands for <em>Advanced Intrusion Detection Environment</em> and it monitors file system changes.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>aide <span class="nt">-y</span>

<span class="c"># initialize</span>
<span class="nb">sudo </span>aideinit
</code></pre></div></div>

<p>It also should create a daily cron job to check system. You can examine it at <code class="language-plaintext highlighter-rouge">/etc/cron.daily/aide</code>.</p>

<h3 id="configure-apparmor">Configure AppArmor</h3>

<p><a href="https://www.apparmor.net/">AppArmor</a> is a security program that restrict programs capabilities.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>apparmor apparmor-profiles apparmor-utils <span class="nt">-y</span>

<span class="c"># enable</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>apparmor
<span class="nb">sudo </span>systemctl start apparmor

<span class="c"># test status</span>
<span class="nb">sudo </span>apparmor_status
</code></pre></div></div>

<h3 id="secure-nginx">Secure Nginx</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>nginx <span class="nt">-y</span>
</code></pre></div></div>

<p>Edit <code class="language-plaintext highlighter-rouge">sudo vi /etc/nginx/nginx.conf</code> and disable showing server name and version</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server_tokens off;
</code></pre></div></div>

<p>Install Certbot for Let’s Encrypt</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>certbot python3-certbot-nginx <span class="nt">-y</span>

<span class="c"># obtain SSL certificate</span>
<span class="nb">sudo </span>certbot <span class="nt">--nginx</span>
</code></pre></div></div>

<h3 id="monitor-logs">Monitor logs</h3>

<p>We also may want to regularly review logs from suspicious activity. Let’s use <code class="language-plaintext highlighter-rouge">logwatch</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>logwatch <span class="nt">-y</span>

<span class="c"># edit cron job to mail</span>
<span class="nb">sudo </span>vi /etc/cron.daily/00logwatch
</code></pre></div></div>

<p>Sample edited file <code class="language-plaintext highlighter-rouge">/etc/cron.daily/00logwatch</code> and replace the email you want.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c">#Check if removed-but-not-purged</span>
<span class="nb">test</span> <span class="nt">-x</span> /usr/share/logwatch/scripts/logwatch.pl <span class="o">||</span> <span class="nb">exit </span>0

<span class="c">#execute</span>
/usr/sbin/logwatch <span class="nt">--output</span> mail <span class="nt">--mailto</span> john.doe@example.com <span class="nt">--detail</span> high
</code></pre></div></div>

<h3 id="custom-motd-when-login">Custom MOTD when login</h3>

<p>MOTD stands for “message of the day”. When we log to the system, by default Debian will greet us with something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Linux cplane1 6.1.0-35-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.137-1 (2025-05-07) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Jun 12 16:48:47 2025 from 192.168.178.162
</code></pre></div></div>

<p>We can do a bit better and provide bit more initial information for us especially if we log in after being some time away. Let’s use <code class="language-plaintext highlighter-rouge">neofetch</code> and <code class="language-plaintext highlighter-rouge">inxi</code> to help us display few things.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>neofetch inxi <span class="nt">-y</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># list of available files</span>
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-al</span> /etc/update-motd.d/
total 16
drwxr-xr-x  2 root root 4096 Jun 12 17:00 <span class="nb">.</span>
drwxr-xr-x 81 root root 4096 Jun 12 19:23 ..
<span class="nt">-rwxr-xr-x</span>  1 root root   23 Apr  4  2017 10-uname
<span class="nt">-rwxr-xr-x</span>  1 root root  165 Dec 31  2022 92-unattended-upgrades

<span class="c"># file 10-uname is not interesting, so let's remove it and create our file</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/update-motd.d/10-uname

<span class="c"># create new file 01-custom</span>
<span class="nb">sudo tee</span> /etc/update-motd.d/01-custom <span class="o">&gt;</span>/dev/null <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#!/bin/sh
printf "</span><span class="se">\n</span><span class="sh">"
/usr/bin/neofetch

inxi -D
printf "</span><span class="se">\n</span><span class="sh">"
</span><span class="no">EOF

</span><span class="c"># apply executing privileges to script</span>
<span class="nb">sudo chmod</span> +x /etc/update-motd.d/01-custom
</code></pre></div></div>

<p>With that we finally have nice looking and informative start screen.</p>

<p><img src="/assets/posts/complete-guide-to-install-and-configure-debian-12/neofetch-inxi-welcome-screen.png" alt="" /></p>

<h2 id="update-default-editor">Update default editor</h2>

<p>The default editor in Debian 12 is <code class="language-plaintext highlighter-rouge">nano</code>. I much more prefer vim so I will set it up as default
system editor.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># show current editor</span>
<span class="nb">sudo </span>update-alternatives <span class="nt">--display</span> editor

<span class="c"># install vim</span>
<span class="nb">sudo </span>apt <span class="nt">-y</span> update
<span class="nb">sudo </span>apt <span class="nt">-y</span> <span class="nb">install </span>vim

<span class="c"># update to vim</span>
<span class="nb">sudo </span>update-alternatives <span class="nt">--set</span> editor /usr/bin/vim.basic
</code></pre></div></div>

<h2 id="ansible">Ansible</h2>

<p>To configure many futher actions easier, we need to install <code class="language-plaintext highlighter-rouge">ansible</code>. Debian 121 has all packges in standard repository simply do:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install ansible</span>
apt <span class="nb">install </span>ansible sshpass <span class="nt">-y</span>
</code></pre></div></div>

<h2 id="shell">Shell</h2>

<p>Change shell can be also a productive boost for you. I like to use zsh therefore here will also
drop recommendation to install it:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install zsh</span>
<span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nb">install </span>zsh

<span class="c"># change the default shell to zsh</span>
chsh <span class="nt">-s</span> <span class="si">$(</span>which zsh<span class="si">)</span>
</code></pre></div></div>

<h3 id="oh-my-zsh">oh-my-zsh</h3>

<p>When talking about zsh I have to suggest you also installing <a href="https://ohmyz.sh/#install">oh-my-zsh</a></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh <span class="nt">-c</span> <span class="s2">"</span><span class="si">$(</span>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<h2 id="finalization">Finalization</h2>

<p>Woooohooo! This was a long ride but it was worth it! Now you have system Hope you enjoy the tutorial will leave a comment what you like and what see improved. Thank you for staying with me and as alwyas until next time.</p>

<h2 id="reference">Reference</h2>

<ul>
  <li>https://sec-tech.org/how-to-harden-a-freshly-installed-debian-server-a-comprehensive-step-by-step-guide/</li>
  <li>https://thelinuxcode.com/partition-disks-while-installing-debian-12-bookworm/</li>
  <li>https://medium.com/@zehan9211/how-to-fix-debian-12-bookworm-update-error-395a3d6d4ab7</li>
  <li>https://linuxways.net/debian/fix-sudo-command-not-found-debian-12/#post-24005-bookmark=id.x2pttty01825</li>
  <li>https://cloudybarz.com/custom-motd-in-linux-debian/</li>
  <li>https://www.putorius.net/custom-motd-login-screen-linux.html</li>
</ul>]]></content><author><name>Maciej Sypien</name></author><category term="linux" /><category term="hardware" /><category term="security" /><category term="server" /><summary type="html"><![CDATA[In one of my previous posts, I created a Home Kubernetes Cluster. The experience from that event gave me valuable insights to try something different from the most popular distributions like Ubuntu. My goal remains simple: ease of operation, high performance, and stability without many problems. After considering various options, I decided to switch to Debian.]]></summary></entry><entry><title type="html">Useful terminal commands</title><link href="https://egel.github.io/2025/05/27/useful-terminal-commands.html" rel="alternate" type="text/html" title="Useful terminal commands" /><published>2025-05-27T00:00:00+00:00</published><updated>2025-05-27T00:00:00+00:00</updated><id>https://egel.github.io/2025/05/27/useful-terminal-commands</id><content type="html" xml:base="https://egel.github.io/2025/05/27/useful-terminal-commands.html"><![CDATA[<p>This post will be different from the others. Instead of presenting a comprehensive tutorial or in-depth explanation, it will function more like a personal repository – a collection of useful programs, terminal commands, and solutions that have worked well for me over time. Think of it as a ‘notes to self’ section, where I can quickly reference and revisit these gems whenever needed.</p>

<h2 id="searching">Searching</h2>

<h3 id="search-phrase-inside-pdf-files">Search phrase inside PDF files</h3>

<blockquote>
  <p>install <code class="language-plaintext highlighter-rouge">pdfgrep</code></p>
</blockquote>

<p>Search for <code class="language-plaintext highlighter-rouge">&lt;phrase&gt;</code> in all PDF files (case insensitive, e.g.: <code class="language-plaintext highlighter-rouge">.pdf</code>, <code class="language-plaintext highlighter-rouge">.PDF</code>) in the current directory (<code class="language-plaintext highlighter-rouge">.</code>).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find <span class="nb">.</span> <span class="nt">-iname</span> <span class="s2">"*.pdf"</span> <span class="nt">-exec</span> pdfgrep <span class="s2">"&lt;phrase&gt;"</span> <span class="o">{}</span> +
</code></pre></div></div>

<h3 id="check-open-port-on-given-ip-address">Check open port on given IP address</h3>

<p>Check if on IP <code class="language-plaintext highlighter-rouge">145.23.12.30</code> there is open port <code class="language-plaintext highlighter-rouge">8080</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-p</span> 8080 145.23.12.30
</code></pre></div></div>

<h2 id="listing">Listing</h2>

<h3 id="show-all-directories-and-their-real-sizes-with-sort">Show all directories and their real sizes (with sort)</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">du</span> <span class="nt">-sh</span> <span class="k">*</span>/ | <span class="nb">sort</span> <span class="nt">-hr</span>
</code></pre></div></div>

<h2 id="copying">Copying</h2>

<h3 id="copy-all-files-of-a-given-type-to-specific-directory">Copy all files of a given type to specific directory</h3>

<p>Take all <code class="language-plaintext highlighter-rouge">mp4</code> files from current directory (<code class="language-plaintext highlighter-rouge">.</code>) and paste into <code class="language-plaintext highlighter-rouge">~/Music</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find <span class="nb">.</span> <span class="nt">-name</span> <span class="s1">'*.mp4'</span> <span class="nt">-type</span> f | xargs <span class="nt">-I</span> <span class="s1">'{}'</span> <span class="nb">mv</span> <span class="s1">'{}'</span> ~/Music
</code></pre></div></div>

<h2 id="updating">Updating</h2>

<p>TBD</p>

<h2 id="testing">Testing</h2>

<h3 id="benchmark-server-effectiveness">Benchmark server effectiveness</h3>

<blockquote>
  <p>install <code class="language-plaintext highlighter-rouge">wrk</code></p>
</blockquote>

<p>Spawn 12 threads with 400 simultaneous connections for 10s to <code class="language-plaintext highlighter-rouge">http://127.0.0.1:9000</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wrk <span class="nt">-t12</span> <span class="nt">-c400</span> <span class="nt">-d10s</span> http://127.0.0.1:9000
</code></pre></div></div>

<h3 id="reaching-internet-connection-macos">Reaching internet connection (macOS)</h3>

<p>Sometimes my laptop loose internet connection. Then I usually check reaching sever <code class="language-plaintext highlighter-rouge">8.8.8.8</code> and when it’s back (and I did not notice) I want to be informed by “beep” sound.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do
  if </span>ping <span class="nt">-c</span> 1 8.8.8.8 &amp;&gt; /dev/null<span class="p">;</span> <span class="k">then
    </span><span class="nb">printf</span> <span class="s2">"%s %s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">date</span> <span class="nt">-u</span> +<span class="s2">"%Y-%m-%dT%H:%M:%SZ"</span><span class="si">)</span><span class="s2">"</span> success <span class="o">&amp;&amp;</span> afplay /System/Library/Sounds/Funk.aiff
  <span class="k">else
    </span><span class="nb">printf</span> <span class="s2">"%s %s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">date</span> <span class="nt">-u</span> +<span class="s2">"%Y-%m-%dT%H:%M:%SZ"</span><span class="si">)</span><span class="s2">"</span> fail
  <span class="k">fi</span><span class="p">;</span>
  <span class="nb">sleep </span>5<span class="p">;</span>
<span class="k">done</span>
</code></pre></div></div>

<h2 id="saving">Saving</h2>

<h3 id="saving-file-with-vim-without-sudo-privileges">Saving file with vim without sudo privileges</h3>

<p>Imagine situation when you open file, do lot of important work, and then while trying to save the file, your vim tells you: <code class="language-plaintext highlighter-rouge">E45: 'readonly' option is set (add ! to override)</code>. Next you, try again, and again, and nothing… Then you remember about “force” and check saving with <code class="language-plaintext highlighter-rouge">w!</code>, but this also not work…</p>

<p>If this is the case you have encounter, try this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:w !sudo tee %
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">w</code> - save</li>
  <li><code class="language-plaintext highlighter-rouge">!</code> - execute</li>
  <li><code class="language-plaintext highlighter-rouge">sudo tee</code> - programs</li>
  <li><code class="language-plaintext highlighter-rouge">%</code> - current buffer</li>
</ul>

<p>In short: it will save your current file , by wrapping it up with <code class="language-plaintext highlighter-rouge">sudo tee</code> which enable a proper saving privileges for sudo user.</p>

<h2 id="generating">Generating</h2>

<h3 id="random-passwords">Random passwords</h3>

<ul>
  <li>
    <p>Option 1: Using <code class="language-plaintext highlighter-rouge">openssl</code> and generate random 12 characters password.</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl rand <span class="nt">-base64</span> 12
</code></pre></div>    </div>
  </li>
  <li>
    <p>Option 2: Generate chars <code class="language-plaintext highlighter-rouge">[a-z0-9]</code> (no special chars)</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">LC_ALL</span><span class="o">=</span>C bash <span class="nt">-c</span> <span class="s1">'head /dev/urandom | tr -dc a-z0-9 | head -c30'</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="secure-password-file-for-users">Secure password file for users</h3>

<ul>
  <li>
    <p>Option 3: Using <code class="language-plaintext highlighter-rouge">htpasswd</code> and generate given sample password <code class="language-plaintext highlighter-rouge">123</code> and save to <code class="language-plaintext highlighter-rouge">auth</code> file with <code class="language-plaintext highlighter-rouge">john</code> key name.</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>htpasswd <span class="nt">-c</span> auth john

<span class="nv">$ </span><span class="nb">cat </span>auth
john:<span class="nv">$apr1$VqXa9gLG$pAIL</span>.jm6Ts48QH4RKlX6k0
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="git">Git</h2>

<h3 id="update-all-git-repositories-from-current-path">Update all git repositories from current path</h3>

<p>Take current directory (<code class="language-plaintext highlighter-rouge">.</code>) and search for all git repositories underneath (not nested) and fetch the latest changes from the origin.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find <span class="nb">.</span> <span class="nt">-name</span> .git <span class="nt">-type</span> d <span class="nt">-prune</span> | xargs <span class="nt">-I</span> <span class="o">{}</span> sh <span class="nt">-c</span> <span class="s1">'cd {} &amp;&amp; cd .. &amp;&amp; printf "Repo: %s\n\n" $(realpath) &amp;&amp; git fetch'</span>
</code></pre></div></div>

<h3 id="change-master-branch-to-main">Change master branch to main</h3>

<p>Change <code class="language-plaintext highlighter-rouge">master</code> to <code class="language-plaintext highlighter-rouge">main</code> and push to origin.</p>

<blockquote>
  <p>(Optional) update the default branch in repo settings.</p>
</blockquote>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout master
git branch <span class="nt">-m</span> master main
git push <span class="nt">-u</span> origin main
git fetch origin
git branch <span class="nt">-u</span> origin/main main
git remote set-head origin <span class="nt">-a</span>
</code></pre></div></div>

<h2 id="docker">Docker</h2>

<p>Sometimes we have to check what’s inside prepared docker image, but it might be hard debug it. Nothing far from that.</p>

<p>Here is little solution. To explain:</p>

<ul>
  <li>(optional): <code class="language-plaintext highlighter-rouge">--name podman1</code> runs under unique name <code class="language-plaintext highlighter-rouge">podman1</code>. This is handy if you need easy reference to the container</li>
  <li>(optional): <code class="language-plaintext highlighter-rouge">--rm</code> remove and cleanup container after closing. This is handy if you do not want dangling containers around</li>
  <li><code class="language-plaintext highlighter-rouge">--entrypoint=""</code> override image’s entrypoint. This is handy to cleanup if the container has custom entrypoint</li>
  <li><code class="language-plaintext highlighter-rouge">-it</code> to start interactive session</li>
  <li>use the container <code class="language-plaintext highlighter-rouge">egel/podman:latest</code></li>
  <li>jump to <code class="language-plaintext highlighter-rouge">bash</code> shell</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--name</span> podman1 <span class="nt">--rm</span> <span class="nt">--entrypoint</span><span class="o">=</span><span class="s2">""</span> <span class="nt">-it</span> egel/podman:latest bash
</code></pre></div></div>]]></content><author><name>Maciej Sypien</name></author><category term="macos" /><category term="unix" /><category term="linux" /><summary type="html"><![CDATA[This post will be different from the others. Instead of presenting a comprehensive tutorial or in-depth explanation, it will function more like a personal repository – a collection of useful programs, terminal commands, and solutions that have worked well for me over time. Think of it as a ‘notes to self’ section, where I can quickly reference and revisit these gems whenever needed.]]></summary></entry><entry><title type="html">Building offline docs</title><link href="https://egel.github.io/2025/02/03/building-offline-docs.html" rel="alternate" type="text/html" title="Building offline docs" /><published>2025-02-03T00:00:00+00:00</published><updated>2025-02-03T00:00:00+00:00</updated><id>https://egel.github.io/2025/02/03/building-offline-docs</id><content type="html" xml:base="https://egel.github.io/2025/02/03/building-offline-docs.html"><![CDATA[<p>In the last month, I had a serious problem with my home internet connection for a few weeks. People don’t appreciating what they have until they loose it. Essentially, same happened to me when I lost the internet for a couple of days. I immediately begin appreciating this luxury of having stable connection while downloading every single kilobyte.</p>

<h2 id="web-with-limited-access">Web with limited access</h2>

<p>One major challenge in developer daily work, apart from regular online meetings and constant communication, is access to required documentation of services often working with. Many of documentation project are usually easily available online, so normally nobody even think about use them without internet. But what if you would have a very limited internet access or even not at all?</p>

<p>This situation I’d been get into, encouraged me to quickly find a solution in order to my limited mobile bandwidth and not use all at once. I started by finding all the documentation for the services I mist working with and putting them into single lists for later download.</p>

<p>I get the list and promptly reached the place with the nearest stable internet connection. I get started pulling all docs with the neat plan to compile them and serve them on my local machine using docker containers.</p>

<p>Some fragment of the documentation list you can find below:</p>

<ul>
  <li><a href="https://github.com/jsdoc/jsdoc.github.io">jsdoc.github.io</a></li>
  <li><a href="https://jakearchibald.github.io/svgomg/">SVGOGM</a> a missing GUI for [svgo]</li>
  <li><a href="https://github.com/tailwindlabs/tailwindcss">tailwind</a> for <a href="https://v3.tailwindcss.com/">v3</a> (and <a href="https://tailwindcss.com">v4</a>)</li>
  <li><a href="https://github.com/mui/material-ui">material UI</a></li>
  <li><a href="https://react-hook-form.com/">react-hook-form</a></li>
  <li><a href="https://react-icons.github.io/react-icons/">react-icons</a></li>
</ul>

<div class="alert alert-info">
    Psst... All Docker containers that I've created for myself are ready to use and you can find them all at the end of this article with info how to run them.
</div>

<p>As I mentioned before, I started with downloading all of them and testing if I could run them offline. However, the funny thing after downloading some of the docs, I realized that wasn’t the end of pulling new things. Usually, the files are just source files without any dependencies. Usually later you need to pull another bunch of dependencies required for each project to start. And to make it even more complex, each project might use a different way of installing its own dependencies. Moreover, some projects have dependencies that have their own dependencies, and then things can get complicated.</p>

<p>FUN FACT: After realizing this, I figured out that <strong>it would be actually pretty cool if every major project had an offline documentation image</strong> that could be easily pulled with one click. That would be very helpful for a consumer like me, who finds themself in the situation of being temporary offline.</p>

<p>The project dependencies were one aspect of the problem. The most challenging part was discovering a proper way to compile and build all those documentation projects. Each had its own unique approach, but some stood out as particularly difficult. The <a href="https://react-icons.github.io/react-icons/">react-icons</a> project proved especially troublesome due to its multi-step compilation process, which often resulted in errors. <a href="https://github.com/tailwindlabs/tailwindcss">Tailwind</a> posed another significant challenge, not due to the compilation process itself, but rather its substantial build size, which frequently required me to free up disk space after multiple failed attempts.</p>

<h2 id="dockerization-process">Dockerization process</h2>

<p>Let’s take a look at the first example with the JS docs, available at <a href="https://github.com/jsdoc/jsdoc.github.io">jsdoc.github.io</a>. This repository is actually very simple and easy to dockerize, so I will start with this one as an example. It will be easy to demonstrate how we can achieve our goal.</p>

<p>Also, before dive deeper into example, it’s worth mentioning that for creating <code class="language-plaintext highlighter-rouge">Dockerfiles</code> (or actually, in the end, Docker images), there are two major ways of doing this:</p>

<ul>
  <li>The faster way, but less elegant and sometimes faulty - usually done by utilizing a spinning dev server inside the container. Those images are relatively much larger due to containing all of the dependencies required for running the server.</li>
  <li>The slower way, although producing smaller and more efficient images - by compiling source files and serving them statically with reliable servers like Nginx, for example.</li>
</ul>

<p>I will start with the first way, also known as the “quick and dirty” method, to get some fast results. Later, I will do another example using a more efficient way of compiling and serving the content with previously mentioned Nginx.</p>

<h3 id="big-and-dirty-image">Big and dirty image</h3>

<p>As I mentioned, this way of creating containers is fast, although big and dirty as well. <strong>I would not recommend using it in production environments</strong>. Usually, the images are very large and inappropriate for end-consumer users. Although, for our purposes to serve documentation locally, it’s totally fine.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>workspace
git clone git@github.com:jsdoc/jsdoc.github.io.git

<span class="nb">cd </span>jsdoc.github.io

<span class="c"># create our own Dockerfile</span>
<span class="nb">touch </span>Dockerfile
</code></pre></div></div>

<p>Now that we’ve pulled the project, we can open a freshly created <code class="language-plaintext highlighter-rouge">Dockerfile</code> which will help us dockerize our documentation.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:22</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> ./package.json /app/package.json</span>
<span class="k">COPY</span><span class="s"> ./package-lock.json /app/package-lock.json</span>

<span class="k">RUN </span>npm <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">EXPOSE</span><span class="s"> 8080</span>
<span class="k">CMD</span><span class="s"> ["npm", "run", "serve", "--", "--port", "8080"]</span>
</code></pre></div></div>

<blockquote>
  <p>PRO TIP: When creating Dockerfiles, I strongly recommend using <a href="https://github.com/hadolint/hadolint">hadolint</a> linter. This library will greatly help you enforce best practices for creating great Dockerfiles. Please don’t be surprised if many of your current practices are discouraged or rejected – but hey, it’s all about creating files that are best for team work and future maintenance, right?</p>
</blockquote>

<p>In here it would be worth to mention that for building Docker images that does not contain anything unwanted, very helpful is to create additional <code class="language-plaintext highlighter-rouge">.dockerignore</code> file. The purpose of this file is to block files from its list into the Docker container. Simple but very useful technique to make container “relatively” lightweight.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.git</span>
<span class="err">scripts</span>
<span class="err">*Dockerfile*</span>
<span class="err">node_modules</span>
<span class="err">.env</span>
</code></pre></div></div>

<p>Now build the documentation and start a Docker container on port <code class="language-plaintext highlighter-rouge">9001</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-f</span> Dockerfile <span class="nt">-t</span> jsdoc:latest <span class="nb">.</span> <span class="o">&amp;&amp;</span> docker run <span class="nt">--name</span> jsdoc-offline <span class="nt">-p</span> 9001:8080 jsdoc:latest
</code></pre></div></div>

<p>That’s all for “the fast and dirty” creation of docker image. Let’s jump to the next section of creating more effective Docker image.</p>

<h3 id="the-effective-way">The effective way</h3>

<p>Actually, I think it would be even more great to recreate the same documentation using a more effective technique and compare the results together. I’d be most excited about seeing the final sizes of the Docker images. We could also test the performance, although that would be rather an objective test of the servers running under the hood than a measuring of our benefits - a runnable image that we as clients just want to use.</p>

<p>So let’s start by recreating the previous solution using a more effective way.</p>

<p>In this process, we usually have a few steps:</p>

<ol>
  <li>In the first step, we create a container where we actually pull all dependencies needed for the process of compiling the documentation.</li>
  <li>The second step involves preparing the final server - small and fast, containing only the necessary programs or libraries to run the compiled files.</li>
</ol>

<p>As in the previous example, let’s create another Docker file <code class="language-plaintext highlighter-rouge">Dockerfile_v2</code> and start coding.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">###################################</span>
<span class="c"># build compilation image</span>
<span class="c">###################################</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">node:22</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> ./package.json /app/package.json</span>
<span class="k">COPY</span><span class="s"> ./package-lock.json /app/package-lock.json</span>

<span class="k">RUN </span>npm <span class="nb">install</span>

<span class="c"># copy rest of files and build</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>npm run build
<span class="k">RUN </span><span class="nb">ls</span> <span class="nt">-al</span> /app/_site


<span class="c">###################################</span>
<span class="c"># Building final image</span>
<span class="c">###################################</span>
<span class="k">FROM</span><span class="s"> nginx:1-alpine3.20</span>

<span class="k">RUN </span><span class="nb">mkdir</span> <span class="nt">-p</span> /usr/share/nginx/html
<span class="k">COPY</span><span class="s"> --from=build /app/_site /usr/share/nginx/html</span>
<span class="k">RUN </span><span class="nb">ls</span> <span class="nt">-al</span> /usr/share/nginx/html

<span class="k">COPY</span><span class="s"> nginx.conf /etc/nginx/nginx.conf</span>

<span class="k">EXPOSE</span><span class="s"> 80/tcp</span>

<span class="k">CMD</span><span class="s"> ["/usr/sbin/nginx", "-g", "daemon off;"]</span>

<span class="c"># vi: ft=dockerfile</span>
</code></pre></div></div>

<p>So here we created the first build compilation where we execute our build. And right after, we’re building a final image, and only that last build will be used in the final image.</p>

<p>One last thing that we still need to add for our configuration. Do you see what’s missing? Yes, it’s our new nginx configuration without which our server wouldn’t be able to serve our content properly.</p>

<p>Let’s create the last <code class="language-plaintext highlighter-rouge">nginx.conf</code> file for our jsdocs. The full configuration can also be found below.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">user</span> <span class="s">nginx</span><span class="p">;</span>
<span class="k">worker_processes</span> <span class="s">auto</span><span class="p">;</span>

<span class="k">events</span> <span class="p">{</span>
    <span class="kn">worker_connections</span>  <span class="mi">1024</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">http</span> <span class="p">{</span>
    <span class="kn">include</span> <span class="s">mime.types</span><span class="p">;</span>

    <span class="c1"># remove nginx version from server header</span>
    <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span>

    <span class="kn">access_log</span> <span class="n">/dev/stdout</span><span class="p">;</span>
    <span class="kn">error_log</span> <span class="n">/dev/stdout</span> <span class="s">info</span><span class="p">;</span>

    <span class="c1"># opt file nginx async filehandling</span>
    <span class="kn">sendfile</span> <span class="no">on</span><span class="p">;</span>
    <span class="kn">keepalive_timeout</span>  <span class="mi">65</span><span class="p">;</span>

    <span class="kn">server</span> <span class="p">{</span>
        <span class="kn">listen</span> <span class="mi">80</span> <span class="s">default_server</span><span class="p">;</span>

        <span class="c1"># location of static files to serve</span>
        <span class="kn">root</span> <span class="n">/usr/share/nginx/html</span><span class="p">;</span>

        <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
            <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="nv">$uri</span><span class="s">.html</span> <span class="nv">$uri</span><span class="n">/index.html</span> <span class="n">/index.html</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All done together and now we can get into the last part, the build of the final Docker image.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">rm </span>jsdoc-offline<span class="p">;</span> docker build <span class="nt">-f</span> Dockerfile_v2 <span class="nt">-t</span> jsdoc:latest <span class="nb">.</span> <span class="o">&amp;&amp;</span> docker run <span class="nt">--name</span> jsdoc-offline <span class="nt">--rm</span> <span class="nt">-p</span> 9002:80 jsdoc:latest
</code></pre></div></div>

<h2 id="comparing-images-and-final-conclusion">Comparing images and final conclusion</h2>

<p>So now let’s compare what we actually gain from using a bit more complex way rather than the fast and dirty method. I am actually quite curious if you can estimate what will be the size of both images without knowing it yet.</p>

<p>But let’s not prolong this, and the final results you can find below for my local builds (M3).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>→ docker images jsdoc-dirty:latest <span class="nt">--format</span> <span class="s2">"{{.Repository}}:{{.Tag}} -&gt; {{.Size}}"</span>

jsdoc-dirty:latest -&gt; 1.85GB

→ docker images jsdoc:latest <span class="nt">--format</span> <span class="s2">"{{.Repository}}:{{.Tag}} -&gt; {{.Size}}"</span>

jsdoc:latest -&gt; 77.6MB
</code></pre></div></div>

<p>As you can see, the fast and dirty image is quite big, and to be precise it is 2.384% larger than the more efficient version. So now you see why the efficient way is much better for end customers like us, especially when downloading bandwidth matters the most.</p>

<p>The results could be quite shocking for those who might not have realized before how big a dev server can be - especially those who run on npm and node_modules. I apologize, I couldn’t rest, but you may see for yourself downloading some of the images that must running with node servers (I am looking at tailwind and mui!).</p>

<p><img src="/assets/posts/building-offline-docs/node_modules-heaviest-object-universe.jpg" alt="node_modules" /></p>

<p>Thank you for your time, staying with me and reading until the very end. Hope you find this enjoyable. See you until the next one.</p>

<h2 id="offline-docs">Offline docs</h2>

<p>Here is also a little hint (for me in future) how to quickly setup and build images for multiple architectures on Mac M-serie.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create new builder</span>
docker buildx create <span class="nt">--name</span> mybuilder <span class="nt">--use</span> <span class="nt">--bootstrap</span>

<span class="c"># should list new mybuilder image</span>
docker buildx <span class="nb">ls</span>

<span class="c"># build images and push to repository</span>
<span class="c"># INFO: progress plain is optional. This option give some additional debug options while building images</span>
<span class="c"># INFO: if build take a while, then build separately and then push with with</span>
<span class="c">#       all platform set at once, otherwise one will override another</span>
docker buildx build <span class="nt">--push</span> <span class="nt">--platform</span> linux/amd64,linux/arm64 <span class="nt">--tag</span> &lt;docker-user&gt;/&lt;tag-name&gt;:&lt;build-version&gt; <span class="nt">--file</span> Dockerfile <span class="nt">--progress</span><span class="o">=</span>plain <span class="nb">.</span>
</code></pre></div></div>

<h3 id="final-images-to-download-and-use">Final images to download and use</h3>

<p>I compiled the images for two architectures: <a href="https://hub.docker.com/u/arm64v8/">arm64v8</a> and <a href="https://hub.docker.com/u/amd64/">amd64</a>. Those are sufficient for most users, although if need more, just let me know in the comments.</p>

<ol>
  <li>
    <p><a href="https://github.com/jsdoc/jsdoc.github.io">jsdoc</a> (eleventy, npm) ~19MB compressed</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/jsdoc/Dockerfile">Dockerfile</a></li>
      <li><a href="/assets/posts/building-offline-docs/jsdoc/nginx.conf">nginx.conf</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9001:80 egel/jsdoc:latest
</code></pre></div>    </div>
  </li>
  <li>
    <p><a href="https://jakearchibald.github.io/svgomg/">svgomg</a> (gulp, npm) ~20MB</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/svgomg/Dockerfile">Dockerfile</a></li>
      <li><a href="/assets/posts/building-offline-docs/svgomg/nginx.conf">nginx.conf</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9002:80 egel/svgomg:latest
</code></pre></div>    </div>
  </li>
  <li>
    <p><a href="https://github.com/tailwindlabs/tailwindcss">tailwind</a> (nextjs, npm) ~4GB</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/tailwind/Dockerfile">Dockerfile</a></li>
      <li><a href="/assets/posts/building-offline-docs/tailwind/dockerignore">.dockerignore</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9003:8080 egel/tailwind:v3
</code></pre></div>    </div>
  </li>
  <li>
    <p><a href="https://github.com/mui/material-ui">material-ui</a> (node:22, react, pnpm) ~1.68GB</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/mui/Dockerfile">Dockerfile</a></li>
      <li><a href="/assets/posts/building-offline-docs/mui/dockerignore">.dockerignore</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9005:8080 egel/mui-docs:latest
</code></pre></div>    </div>
  </li>
  <li>
    <p><a href="https://react-hook-form.com/">react hook form</a> (node:22, nextjs, pnpm) ~600MB</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/react-hook-form/Dockerfile">Dockerfile</a></li>
      <li><a href="/assets/posts/building-offline-docs/react-hook-form/dockerignore">.dockerignore</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9006:8080 egel/react-hook-form-docs:latest
</code></pre></div>    </div>
  </li>
  <li>
    <p><a href="https://react-icons.github.io/react-icons/">react icons</a> (node:22-alpine3.21, astro, yarn) ~1.14GB</p>

    <ul>
      <li><a href="/assets/posts/building-offline-docs/react-icons/Dockerfile">Dockerfile</a></li>
    </ul>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> docker run <span class="nt">-d</span> <span class="nt">-p</span> 9007:8080 egel/react-icons:v5.4.0
</code></pre></div>    </div>
  </li>
</ol>]]></content><author><name>Maciej Sypien</name></author><category term="macos" /><category term="docker" /><category term="nginx" /><category term="npm" /><category term="yarn" /><category term="pnpm" /><category term="gulp" /><category term="eleventy" /><summary type="html"><![CDATA[In the last month, I had a serious problem with my home internet connection for a few weeks. People don’t appreciating what they have until they loose it. Essentially, same happened to me when I lost the internet for a couple of days. I immediately begin appreciating this luxury of having stable connection while downloading every single kilobyte.]]></summary></entry><entry><title type="html">Creating Home Kubernetes Cluster Lab</title><link href="https://egel.github.io/2024/08/26/home-k8s-cluster.html" rel="alternate" type="text/html" title="Creating Home Kubernetes Cluster Lab" /><published>2024-08-26T00:00:00+00:00</published><updated>2024-08-26T00:00:00+00:00</updated><id>https://egel.github.io/2024/08/26/home-k8s-cluster</id><content type="html" xml:base="https://egel.github.io/2024/08/26/home-k8s-cluster.html"><![CDATA[<p>From my earliest memories, I’ve always been fascinated by building my own Kubernetes cluster at home. Recently, I made it a reality by assembling a small lab using affordable PCs. In this post, I’ll share the insights and lessons learned from my first attempt to set up a home Kubernetes cluster. Whether you’re a seasoned admin or just starting out, I hope to provide valuable information on the hardware, software, and configurations that worked for me.</p>

<p>Throughout the article, you may come across snippets with additional output to illustrate key concepts and provide a deeper understanding of the installation process. These extra outputs are intended to help first-time users compare their results with mine, ensuring they can achieve similar success in setting up their own Kubernetes cluster.</p>

<h2 id="hardware">Hardware</h2>

<ul>
  <li>3 x HP Elitedesk 800 G3 i5-6500T 2.5GHz 16GB 128GB SSD</li>
  <li>3 x Network cable 50cm</li>
  <li>1 x Network cable 5m</li>
  <li>1 x Switch (5 ports is enough)</li>
  <li>1 x Router (best with function to assign static IP) - I used my FritzBox router</li>
</ul>

<h2 id="software">Software</h2>

<ul>
  <li>Kubernetes 1.31</li>
  <li>ssh, ssh-copy-id</li>
</ul>

<h2 id="before-you-start">Before you start</h2>

<ul>
  <li>assemble your machines to together
    <ul>
      <li>3 times network cables to switch</li>
      <li>switch to router</li>
    </ul>
  </li>
</ul>

<h2 id="start-setup-cluster">Start setup cluster</h2>

<p>I assume you…</p>

<ul>
  <li>
    <p>install ubuntu 24.02</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>lsb_release <span class="nt">-a</span>
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:        24.04
Codename:       nobles
</code></pre></div>    </div>
  </li>
</ul>

<div class="alert alert-info">
    <p><strong>Hey there! It's me from the future:</strong> In this tutorial, I'll be using Ubuntu as my primary operating system. While it's a great choice for beginners like you, if you're looking for a more stable and low-maintenance option, I'd recommend exploring the latest, stable Debian release.</p>
    <p>The reason behind my choice lies in Debian's excellent maintenance quality, its focus on server stability and performance. In the case of running a long-term Kubernetes cluster, I firmly believe that Debian is the better-suited distribution for such tasks.</p>
</div>

<h3 id="set-up-proper-hostnames">Set up proper hostnames</h3>

<p>At this point I assume you have all machines up and running connected to your local router so you
can access them.</p>

<p>in my case:</p>

<ul>
  <li>
    <p>Master</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>hostnamectl set-hostname cplane1
</code></pre></div>    </div>
  </li>
  <li>
    <p>Workers</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>hostnamectl set-hostname worker1 <span class="c"># worker 1</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>Add to <code class="language-plaintext highlighter-rouge">/ect/hosts</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>192.168.178.200     cplane1
192.168.178.201     worker1
</code></pre></div></div>

<p>Test from each machine, example of reaching <code class="language-plaintext highlighter-rouge">cplane1</code> &amp; <code class="language-plaintext highlighter-rouge">worker1</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>maciej@cplane1:~<span class="nv">$ </span>ping worker1 <span class="nt">-c3</span>
PING worker1 <span class="o">(</span>192.168.178.201<span class="o">)</span> 56<span class="o">(</span>84<span class="o">)</span> bytes of data.
64 bytes from worker1 <span class="o">(</span>192.168.178.201<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>1 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.853 ms
64 bytes from worker1 <span class="o">(</span>192.168.178.201<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>2 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.586 ms
64 bytes from worker1 <span class="o">(</span>192.168.178.201<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>3 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.581 ms
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>maciej@worker1:~<span class="nv">$ </span>ping cplane1 <span class="nt">-c3</span>
PING cplane1 <span class="o">(</span>192.168.178.200<span class="o">)</span> 56<span class="o">(</span>84<span class="o">)</span> bytes of data.
64 bytes from cplane1 <span class="o">(</span>192.168.178.200<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>1 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.606 ms
64 bytes from cplane1 <span class="o">(</span>192.168.178.200<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>2 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.602 ms
64 bytes from cplane1 <span class="o">(</span>192.168.178.200<span class="o">)</span>: <span class="nv">icmp_seq</span><span class="o">=</span>3 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.607 ms
</code></pre></div></div>

<h3 id="quick-and-secure-ssh-connection">Quick and secure SSH connection</h3>

<p>If you want to have easy, quick and secure connection to your machine from your machine where you connect to cluster (laptop) you can add your public SSH key to your <code class="language-plaintext highlighter-rouge">cplane1</code>.</p>

<blockquote>
  <p>There is no need to add this to workers machines as only cplane1 is used for communication with
cluster, but will not stop you if you want to do it anyway ;)</p>
</blockquote>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ssh-copy-id <span class="nt">-i</span> ~/.ssh/id_ed25519.pub maciej@192.168.178.200

/usr/bin/ssh-copy-id: INFO: Source of key<span class="o">(</span>s<span class="o">)</span> to be installed: <span class="s2">"/Users/maciej/.ssh/id_ed25519.pub"</span>
/usr/bin/ssh-copy-id: INFO: attempting to log <span class="k">in </span>with the new key<span class="o">(</span>s<span class="o">)</span>, to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key<span class="o">(</span>s<span class="o">)</span> remain to be installed <span class="nt">--</span> <span class="k">if </span>you are prompted now it is to <span class="nb">install </span>the new keys
maciej@192.168.178.200<span class="s1">'s password:

Number of key(s) added:        1

Now try logging into the machine, with: "ssh -i /Users/maciej/.ssh/id_ed25519 '</span>maciej@192.168.178.200<span class="s1">'"
and check to make sure that only the key(s) you wanted were added.
</span></code></pre></div></div>

<p>Then on same extertal machine add new entry to your <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> like the one following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host cplane1
    HostName 192.168.178.200
    User maciej
    Port 22  # adapt if you use different port
    IdentityFile ~/.ssh/id_ed25519
    PreferredAuthentications publickey
</code></pre></div></div>

<p>Now, try to ssh like pro <code class="language-plaintext highlighter-rouge">ssh cplane1</code>.</p>

<h3 id="enabling-firewall">Enabling firewall</h3>

<div class="alert alert-warning">

<p>Important information, especially for those who want to create a home cluster:</p>

<ul>
<li>Learn to Kubernetes course (CKAD)</li>
<li>Or/and, if you're just beginning your journey with exploring Kubernetes (maybe Linux and command-line as well) for the first time.</li>
<li>Or/and start using it in your local network.</li>
</ul>

<p>I would strongly recommend disabling the firewall completely, unless you know what you're doing. Both for master and worker nodes.</p>
<p>However, I'm aware this is not a secure approach, and I do not recommend it for production clusters. If you're just starting out learning Kubernetes, focus on understanding this new technology rather than spending too much time finding the perfect installation by creating obstacles from the start.</p>
<p>Disabling the firewall can remove many problems with local networking and accessing cluster resources.</p>

</div>

<ul>
  <li>
    <p>Master</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Protocol  Direction Port Range  Purpose Used By
-----------------------------------------------
TCP       Inbound   6443        Kubernetes API server All
TCP       Inbound   2379-2380   etcd server client API  kube-apiserver, etcd
TCP       Inbound   10250       Kubelet API Self, Control plane
TCP       Inbound   10259       kube-scheduler  Self
TCP       Inbound   10257       kube-controller-manager Self
</code></pre></div>    </div>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw allow <span class="s2">"OpenSSH"</span>
<span class="nb">sudo </span>ufw <span class="nb">enable</span>

<span class="c"># Test</span>
<span class="nb">sudo </span>ufw status

<span class="nb">sudo </span>ufw allow 6443/tcp
<span class="nb">sudo </span>ufw allow 2379:2380/tcp
<span class="nb">sudo </span>ufw allow 10250/tcp
<span class="nb">sudo </span>ufw allow 10259/tcp
<span class="nb">sudo </span>ufw allow 10257/tcp

<span class="c"># Test</span>
<span class="nv">$ </span><span class="nb">sudo </span>ufw status
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>maciej:
Status: active

To                         Action      From
<span class="nt">--</span>                         <span class="nt">------</span>      <span class="nt">----</span>
OpenSSH                    ALLOW       Anywhere
6443/tcp                   ALLOW       Anywhere
2379:2380/tcp              ALLOW       Anywhere
10250/tcp                  ALLOW       Anywhere
10259/tcp                  ALLOW       Anywhere
10257/tcp                  ALLOW       Anywhere
OpenSSH <span class="o">(</span>v6<span class="o">)</span>               ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
6443/tcp <span class="o">(</span>v6<span class="o">)</span>              ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
2379:2380/tcp <span class="o">(</span>v6<span class="o">)</span>         ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
10250/tcp <span class="o">(</span>v6<span class="o">)</span>             ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
10259/tcp <span class="o">(</span>v6<span class="o">)</span>             ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
10257/tcp <span class="o">(</span>v6<span class="o">)</span>             ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Workers</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw allow <span class="s2">"OpenSSH"</span>
<span class="nb">sudo </span>ufw <span class="nb">enable

sudo </span>ufw allow 10250/tcp
<span class="nb">sudo </span>ufw allow 30000:32767/tcp

<span class="c"># Test</span>
<span class="nv">$ </span><span class="nb">sudo </span>ufw status
<span class="o">[</span><span class="nb">sudo</span><span class="o">]</span> password <span class="k">for </span>maciej:
Status: active

To                         Action      From
<span class="nt">--</span>                         <span class="nt">------</span>      <span class="nt">----</span>
10250/tcp                  ALLOW       Anywhere
30000:32767/tcp            ALLOW       Anywhere
OpenSSH                    ALLOW       Anywhere
10250/tcp <span class="o">(</span>v6<span class="o">)</span>             ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
30000:32767/tcp <span class="o">(</span>v6<span class="o">)</span>       ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
OpenSSH <span class="o">(</span>v6<span class="o">)</span>               ALLOW       Anywhere <span class="o">(</span>v6<span class="o">)</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="enable-kernel-modules--disable-swap">Enable kernel modules &amp; disable swap</h3>

<p>Enable kernel modules and add them to file for permament effect after reboot.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>modprobe overlay
<span class="nv">$ </span><span class="nb">sudo </span>modprobe br_netfilter
<span class="nv">$ </span><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
</span><span class="no">EOF
</span></code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># add kernel parametes to load with k8s configuration</span>
<span class="nv">$ </span><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
</span><span class="no">EOF

</span><span class="c"># load modules above</span>
<span class="nv">$ </span><span class="nb">sudo </span>sysctl <span class="nt">--system</span>
</code></pre></div></div>

<h3 id="disable-swap">Disable swap</h3>

<p>To not have hicckups in our cluster it’s recommended to disable the swap.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># disable with command</span>
<span class="nv">$ </span><span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'/ swap / s/^\(.*\)$/#\1/g'</span> /etc/fstab
<span class="c"># or comment the line manually</span>
<span class="nv">$ </span><span class="nb">sudo </span>vim /etc/fstab <span class="c"># disable /swap</span>


<span class="nv">$ </span><span class="nb">sudo </span>swapoff <span class="nt">-a</span>
<span class="nv">$ </span>free <span class="nt">-m</span>
               total        used        free      shared  buff/cache   available
Mem:           15783         261       15085           1         436       15255
Swap:              0           0           0
</code></pre></div></div>

<p>Now, repeat same for the other worker(s).</p>

<h3 id="installing-containerd">Installing Containerd</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nt">-y</span> update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nt">-y</span> upgrade
Hit:1 http://de.archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://de.archive.ubuntu.com/ubuntu noble-updates InRelease
Hit:3 http://de.archive.ubuntu.com/ubuntu noble-backports InRelease
Hit:4 http://security.ubuntu.com/ubuntu noble-security InRelease
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.31/deb  InRelease <span class="o">[</span>1,189 B]
Err:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.31/deb  InRelease
  The following signatures were invalid: EXPKEYSIG 234654DA9A296436 isv:kubernetes OBS Project &lt;isv:kubernetes@build.opensuse.org&gt;
Fetched 1,189 B <span class="k">in </span>1s <span class="o">(</span>1,819 B/s<span class="o">)</span>
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.31/deb  InRelease: The following signatures were invalid: EXPKEYSIG 234654DA9A296436 isv:kubernetes OBS Project &lt;isv:kubernetes@build.opensuse.org&gt;
W: Failed to fetch https://pkgs.k8s.io/core:/stable:/v1.31/deb/InRelease  The following signatures were invalid: EXPKEYSIG 234654DA9A296436 isv:kubernetes OBS Project &lt;isv:kubernetes@build.opensuse.org&gt;
W: Some index files failed to download. They have been ignored, or old ones used instead.
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

<span class="c"># if having info about outdated gpg do following</span>
<span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmour</span> <span class="nt">-o</span> /etc/apt/trusted.gpg.d/containerd.gpg
<span class="nb">sudo </span>apt <span class="nt">-y</span> update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nt">-y</span> upgrade

<span class="c"># Prepare for new repos</span>
<span class="nb">sudo </span>apt <span class="nt">-y</span> <span class="nb">install </span>apt-transport-https ca-certificates software-properties-common curl gnupg

<span class="c"># Add Docker’s official GPG key:</span>
<span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /etc/apt/keyrings/docker.gpg
<span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.gpg

<span class="c"># Use the following command to set up the repository:</span>
<span class="nb">echo</span> <span class="se">\</span>
  <span class="s2">"deb [arch="</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2">" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
  "</span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$VERSION_CODENAME</span><span class="s2">"</span><span class="si">)</span><span class="s2">" stable"</span> | <span class="se">\</span>
  <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null

<span class="nb">sudo </span>apt <span class="nt">-y</span> update

<span class="nb">sudo </span>apt <span class="nb">install </span>containerd.io <span class="c"># docker-ce docker-ce-cli docker-buildx-plugin docker-compose-plugin</span>

<span class="c"># stop containers</span>
<span class="nb">sudo </span>systemctl stop containerd

<span class="c"># copy orginal configuration</span>
<span class="nb">sudo mv</span> /etc/containerd/config.toml /etc/containerd/config.toml.orig
<span class="nb">sudo </span>containerd config default <span class="o">&gt;</span> /etc/containerd/config.toml
<span class="nt">-bash</span>: /etc/containerd/config.toml: Permission denied

<span class="nb">sudo </span>su
root@worker1:/home/maciej# <span class="nb">sudo </span>containerd config default <span class="o">&gt;</span> /etc/containerd/config.toml
<span class="nb">exit</span>

<span class="c"># replace "SystemdCgroup = false" to "SystemdCgroup = true".</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/SystemdCgroup \= false/SystemdCgroup \= true/g'</span> /etc/containerd/config.toml
<span class="c"># or do it manually</span>
<span class="nb">sudo </span>vim /etc/containerd/config.toml

<span class="c"># start</span>
<span class="nb">sudo </span>systemctl start containerd

<span class="nv">$ </span><span class="nb">sudo </span>systemctl status containerd
● containerd.service - containerd container runtime
     Loaded: loaded <span class="o">(</span>/lib/systemd/system/containerd.service<span class="p">;</span> enabled<span class="p">;</span> vendor preset: enabled<span class="o">)</span>
     Active: active <span class="o">(</span>running<span class="o">)</span> since Wed 2023-04-19 23:05:59 UTC<span class="p">;</span> 1min 12s ago
       Docs: https://containerd.io
    Process: 2285 <span class="nv">ExecStartPre</span><span class="o">=</span>/sbin/modprobe overlay <span class="o">(</span><span class="nv">code</span><span class="o">=</span>exited, <span class="nv">status</span><span class="o">=</span>0/SUCCESS<span class="o">)</span>
   Main PID: 2286 <span class="o">(</span>containerd<span class="o">)</span>
      Tasks: 10
     Memory: 12.7M
        CPU: 248ms
     CGroup: /system.slice/containerd.service
             └─2286 /usr/bin/containerd

Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.574566257Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span>serving... <span class="nv">address</span><span class="o">=</span>/run/container&gt;
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.574626153Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span>serving... <span class="nv">address</span><span class="o">=</span>/run/container&gt;
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.574685614Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span><span class="s2">"containerd successfully booted i&gt;
Apr 19 23:05:59 worker1 systemd[1]: Started containerd container runtime.
Apr 19 23:05:59 worker1 containerd[2286]: time="</span>2023-04-19T23:05:59.574660458Z<span class="s2">" level=info msg="</span>Start subscribing containerd eve&gt;
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.576694989Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span><span class="s2">"Start recovering state"</span>
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.576767152Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span><span class="s2">"Start event monitor"</span>
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.576792721Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span><span class="s2">"Start snapshots syncer"</span>
Apr 19 23:05:59 worker1 containerd[2286]: <span class="nb">time</span><span class="o">=</span><span class="s2">"2023-04-19T23:05:59.576807855Z"</span> <span class="nv">level</span><span class="o">=</span>info <span class="nv">msg</span><span class="o">=</span><span class="s2">"Start cni network conf syncer fo&gt;
Apr 19 23:05:59 worker1 containerd[2286]: time="</span>2023-04-19T23:05:59.576816067Z<span class="s2">" level=info msg="</span>Start streaming server<span class="s2">"

</span><span class="nv">$ </span><span class="s2">sudo systemctl is-enabled containerd
</span></code></pre></div></div>

<h3 id="install-kubernetes-kubeadm-kubelet-kubectl">Install Kubernetes (kubeadm, kubelet, kubectl)</h3>

<p><code class="language-plaintext highlighter-rouge">kubeadm</code> requirements:</p>

<ul>
  <li>RAM: min 2GB</li>
  <li>CPU: min 2</li>
  <li>disable swap</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsb_release <span class="nt">-c</span>
No LSB modules are available.
Codename:       noble

<span class="c"># add / refresh gpg key</span>
<span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmour</span> <span class="nt">-o</span> /etc/apt/trusted.gpg.d/docker.gpg
<span class="nb">sudo </span>add-apt-repository <span class="s2">"deb [arch=amd64] https://download.docker.com/linux/ubuntu </span><span class="si">$(</span>lsb_release <span class="nt">-cs</span><span class="si">)</span><span class="s2"> stable"</span>

<span class="nv">$ </span><span class="nb">sudo </span>apt update

<span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>kubelet kubeadm kubectl

<span class="c"># check installed versions</span>
<span class="nv">$ </span>kubelet <span class="nt">--version</span>
Kubernetes v1.31.6

<span class="nv">$ </span>kubectl version
Client Version: v1.31.6
Kustomize Version: v5.4.2
The connection to the server localhost:8080 was refused - did you specify the right host or port?

<span class="nv">$ </span>kubeadm version
kubeadm version: &amp;version.Info<span class="o">{</span>Major:<span class="s2">"1"</span>, Minor:<span class="s2">"31"</span>, GitVersion:<span class="s2">"v1.31.6"</span>, GitCommit:<span class="s2">"6b3560758b37680cb713dfc71da03c04cadd657c"</span>, GitTreeState:<span class="s2">"clean"</span>, BuildDate:<span class="s2">"2025-02-12T21:31:09Z"</span>, GoVersion:<span class="s2">"go1.22.12"</span>, Compiler:<span class="s2">"gc"</span>, Platform:<span class="s2">"linux/amd64"</span><span class="o">}</span>

<span class="c"># Prevent kubernetes packages from being upgraded between versions</span>
<span class="nv">$ </span><span class="nb">sudo </span>apt-mark hold kubelet kubeadm kubectl
kubelet <span class="nb">set </span>on hold.
kubeadm <span class="nb">set </span>on hold.
kubectl <span class="nb">set </span>on hold.
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># check is kubelet is running</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> kubelet
</code></pre></div></div>

<h2 id="installing-cni-plugin">Installing CNI Plugin</h2>

<p>Install Container Network Interface (CNI).</p>

<div class="alert alert-info">
<p>When I for the first time Install Kubernetes I use flannel CNI. It was good, but it didn't work with the <code>NetworkPolicy</code>. Therefore, I could not accomplish some of the exercises I with it.</p>
<p>If you're setting up the Kubernetes cluster also for learning for the CKAD, I encourage you to use Calico&nbsp;CNI.</p>
</div>

<ul>
  <li>
    <p><a href="https://github.com/flannel-io/flannel">Flannel</a> (simple, although <code class="language-plaintext highlighter-rouge">NetworkPolicy</code> is not working with it)</p>

    <p>If you want to use flannel, then you you have to install <code class="language-plaintext highlighter-rouge">flanneld</code> program</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /opt/bin/
<span class="nb">sudo </span>curl <span class="nt">-fsSLo</span> /opt/bin/flanneld https://github.com/flannel-io/flannel/releases/download/v0.25.5/flanneld-amd64

<span class="nb">sudo chmod</span> +x /opt/bin/flanneld
</code></pre></div>    </div>
  </li>
  <li>
    <p>Calico (advance, recommended for K8s course)</p>

    <p>Setup will find later in the article.</p>
  </li>
</ul>

<h2 id="installing-kubernetes">Installing kubernetes</h2>

<p>Now setup <code class="language-plaintext highlighter-rouge">kubeadm init</code> and later finish with running CNI</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubeadm init
<span class="o">[</span>init] Using Kubernetes version: v1.27.1
<span class="o">[</span>preflight] Running pre-flight checks
error execution phase preflight: <span class="o">[</span>preflight] Some fatal errors occurred:
	<span class="o">[</span>ERROR IsPrivilegedUser]: user is not running as root
<span class="o">[</span>preflight] If you know what you are doing, you can make a check non-fatal with <span class="sb">`</span><span class="nt">--ignore-preflight-errors</span><span class="o">=</span>...<span class="sb">`</span>
To see the stack trace of this error execute with <span class="nt">--v</span><span class="o">=</span>5 or higher

<span class="c"># become a root</span>
maciej@cplane1:~<span class="nv">$ </span><span class="nb">sudo </span>su

<span class="c"># pull images before</span>
root@cplane1:/home/root# kubeadm config images pull
<span class="o">[</span>config/images] Pulled registry.k8s.io/kube-apiserver:v1.31.0
<span class="o">[</span>config/images] Pulled registry.k8s.io/kube-controller-manager:v1.31.0
<span class="o">[</span>config/images] Pulled registry.k8s.io/kube-scheduler:v1.31.0
<span class="o">[</span>config/images] Pulled registry.k8s.io/kube-proxy:v1.31.0
<span class="o">[</span>config/images] Pulled registry.k8s.io/coredns/coredns:v1.11.1
<span class="o">[</span>config/images] Pulled registry.k8s.io/pause:3.10
<span class="o">[</span>config/images] Pulled registry.k8s.io/etcd:3.5.15-0

<span class="c"># kubeadm and save the output text for later use (thanks to tee)</span>
root@cplane1:/home/root# kubeadm init <span class="nt">--pod-network-cidr</span><span class="o">=</span>10.244.0.0/16 <span class="nt">--apiserver-advertise-address</span><span class="o">=</span>192.168.178.200 <span class="nt">--cri-socket</span><span class="o">=</span>unix:///run/containerd/containerd.sock | <span class="nb">tee </span>kubeadm_init_store.txt
<span class="o">[</span>init] Using Kubernetes version: v1.31.0
<span class="o">[</span>preflight] Running pre-flight checks
<span class="o">[</span>preflight] Pulling images required <span class="k">for </span>setting up a Kubernetes cluster
<span class="o">[</span>preflight] This might take a minute or two, depending on the speed of your internet connection
<span class="o">[</span>preflight] You can also perform this action beforehand using <span class="s1">'kubeadm config images pull'</span>
W0818 20:50:23.203697   11745 checks.go:846] detected that the sandbox image <span class="s2">"registry.k8s.io/pause:3.8"</span> of the container runtime is inconsistent with that used by kubeadm.It is recommended to use <span class="s2">"registry.k8s.io/pause:3.10"</span> as the CRI sandbox image.
<span class="o">[</span>certs] Using certificateDir folder <span class="s2">"/etc/kubernetes/pki"</span>
<span class="o">[</span>certs] Generating <span class="s2">"ca"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"apiserver"</span> certificate and key
<span class="o">[</span>certs] apiserver serving cert is signed <span class="k">for </span>DNS names <span class="o">[</span>cplane1 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs <span class="o">[</span>10.96.0.1 192.168.178.200]
<span class="o">[</span>certs] Generating <span class="s2">"apiserver-kubelet-client"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"front-proxy-ca"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"front-proxy-client"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"etcd/ca"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"etcd/server"</span> certificate and key
<span class="o">[</span>certs] etcd/server serving cert is signed <span class="k">for </span>DNS names <span class="o">[</span>cplane1 localhost] and IPs <span class="o">[</span>192.168.178.200 127.0.0.1 ::1]
<span class="o">[</span>certs] Generating <span class="s2">"etcd/peer"</span> certificate and key
<span class="o">[</span>certs] etcd/peer serving cert is signed <span class="k">for </span>DNS names <span class="o">[</span>cplane1 localhost] and IPs <span class="o">[</span>192.168.178.200 127.0.0.1 ::1]
<span class="o">[</span>certs] Generating <span class="s2">"etcd/healthcheck-client"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"apiserver-etcd-client"</span> certificate and key
<span class="o">[</span>certs] Generating <span class="s2">"sa"</span> key and public key
<span class="o">[</span>kubeconfig] Using kubeconfig folder <span class="s2">"/etc/kubernetes"</span>
<span class="o">[</span>kubeconfig] Writing <span class="s2">"admin.conf"</span> kubeconfig file
<span class="o">[</span>kubeconfig] Writing <span class="s2">"super-admin.conf"</span> kubeconfig file
<span class="o">[</span>kubeconfig] Writing <span class="s2">"kubelet.conf"</span> kubeconfig file
<span class="o">[</span>kubeconfig] Writing <span class="s2">"controller-manager.conf"</span> kubeconfig file
<span class="o">[</span>kubeconfig] Writing <span class="s2">"scheduler.conf"</span> kubeconfig file
<span class="o">[</span>etcd] Creating static Pod manifest <span class="k">for </span><span class="nb">local </span>etcd <span class="k">in</span> <span class="s2">"/etc/kubernetes/manifests"</span>
<span class="o">[</span>control-plane] Using manifest folder <span class="s2">"/etc/kubernetes/manifests"</span>
<span class="o">[</span>control-plane] Creating static Pod manifest <span class="k">for</span> <span class="s2">"kube-apiserver"</span>
<span class="o">[</span>control-plane] Creating static Pod manifest <span class="k">for</span> <span class="s2">"kube-controller-manager"</span>
<span class="o">[</span>control-plane] Creating static Pod manifest <span class="k">for</span> <span class="s2">"kube-scheduler"</span>
<span class="o">[</span>kubelet-start] Writing kubelet environment file with flags to file <span class="s2">"/var/lib/kubelet/kubeadm-flags.env"</span>
<span class="o">[</span>kubelet-start] Writing kubelet configuration to file <span class="s2">"/var/lib/kubelet/config.yaml"</span>
<span class="o">[</span>kubelet-start] Starting the kubelet
<span class="o">[</span>wait-control-plane] Waiting <span class="k">for </span>the kubelet to boot up the control plane as static Pods from directory <span class="s2">"/etc/kubernetes/manifests"</span>
<span class="o">[</span>kubelet-check] Waiting <span class="k">for </span>a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
<span class="o">[</span>kubelet-check] The kubelet is healthy after 1.001949593s
<span class="o">[</span>api-check] Waiting <span class="k">for </span>a healthy API server. This can take up to 4m0s
<span class="o">[</span>api-check] The API server is healthy after 4.002920718s
<span class="o">[</span>upload-config] Storing the configuration used <span class="k">in </span>ConfigMap <span class="s2">"kubeadm-config"</span> <span class="k">in </span>the <span class="s2">"kube-system"</span> Namespace
<span class="o">[</span>kubelet] Creating a ConfigMap <span class="s2">"kubelet-config"</span> <span class="k">in </span>namespace kube-system with the configuration <span class="k">for </span>the kubelets <span class="k">in </span>the cluster
<span class="o">[</span>upload-certs] Skipping phase. Please see <span class="nt">--upload-certs</span>
<span class="o">[</span>mark-control-plane] Marking the node cplane1 as control-plane by adding the labels: <span class="o">[</span>node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
<span class="o">[</span>mark-control-plane] Marking the node cplane1 as control-plane by adding the taints <span class="o">[</span>node-role.kubernetes.io/control-plane:NoSchedule]
<span class="o">[</span>bootstrap-token] Using token: 8o34w9.jyhtv2av6l2ozv5d
<span class="o">[</span>bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
<span class="o">[</span>bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
<span class="o">[</span>bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs <span class="k">in </span>order <span class="k">for </span>nodes to get long term certificate credentials
<span class="o">[</span>bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
<span class="o">[</span>bootstrap-token] Configured RBAC rules to allow certificate rotation <span class="k">for </span>all node client certificates <span class="k">in </span>the cluster
<span class="o">[</span>bootstrap-token] Creating the <span class="s2">"cluster-info"</span> ConfigMap <span class="k">in </span>the <span class="s2">"kube-public"</span> namespace
<span class="o">[</span>kubelet-finalize] Updating <span class="s2">"/etc/kubernetes/kubelet.conf"</span> to point to a rotatable kubelet client certificate and key
<span class="o">[</span>addons] Applied essential addon: CoreDNS
<span class="o">[</span>addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
  <span class="nb">sudo cp</span> <span class="nt">-i</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
  <span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config

Alternatively, <span class="k">if </span>you are the root user, you can run:

  <span class="nb">export </span><span class="nv">KUBECONFIG</span><span class="o">=</span>/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run <span class="s2">"kubectl apply -f [podnetwork].yaml"</span> with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can <span class="nb">join </span>any number of worker nodes by running the following on each as root:

kubeadm <span class="nb">join </span>192.168.178.200:6443 <span class="nt">--token</span> 8o34w9.jyhtv2av6l2ozv5d <span class="se">\</span>
        <span class="nt">--discovery-token-ca-cert-hash</span> sha256:ab1a9a94e63fd993ba5b4959af0cea371cbb2339637bc1662de5f8121803bcbd
</code></pre></div></div>

<h3 id="configure-cni">configure CNI</h3>

<ul>
  <li>
    <p>Flannel</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@cplane1:/home/root# <span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | tee /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.0/16
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
</span><span class="no">EOF

</span>root@cplane1:/home/root# kubectl apply <span class="nt">-f</span> https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
namespace/kube-flannel created
serviceaccount/flannel created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created
</code></pre></div>    </div>
  </li>
  <li>
    <p>Calico
configure NetworkManager</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/tigera-operator.yaml
namespace/tigera-operator created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpfilters.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/tiers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/adminnetworkpolicies.policy.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/apiservers.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/imagesets.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/installations.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/tigerastatuses.operator.tigera.io created
serviceaccount/tigera-operator created
clusterrole.rbac.authorization.k8s.io/tigera-operator created
clusterrolebinding.rbac.authorization.k8s.io/tigera-operator created
deployment.apps/tigera-operator created

<span class="c"># Install Calico by creating the necessary custom resource. For more information on</span>
<span class="c"># configuration options available in this manifest, see the installation reference.</span>
<span class="c"># https://docs.tigera.io/calico/latest/reference/installation/api</span>

<span class="c"># I used differnt cidr and need to be replaced</span>
<span class="c"># 10.244.0.0/16</span>
<span class="nv">$ </span>curl https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/custom-resources.yaml <span class="o">&gt;&gt;</span> custom-resources.yaml
<span class="nv">$ </span><span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/192.168.0.0\/16/10.244.0.0\/16/g'</span> custom-resources.yaml

<span class="c"># apply modified config</span>
kubectl create <span class="nt">-f</span> custom-resources.yaml

<span class="c"># check if working</span>
watch kubectl get pods <span class="nt">-n</span> calico-system
Every 2.0s: kubectl get pods <span class="nt">-n</span> calico-system                                                                                                                                                                                cplane1: Tue Feb 25 15:43:40 2025

NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-56bf547c9f-nrn42   1/1     Running   0          40s
calico-node-mf56l                          1/1     Running   0          41s
calico-typha-6854bfcb4d-6bhh5              1/1     Running   0          41s
csi-node-driver-mbqcl                      2/2     Running   0          40s

<span class="c"># check taint</span>
<span class="nv">$ </span>kubectl describe nodes | <span class="nb">grep</span> <span class="nt">-i</span> taint
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Taints: &lt;none&gt;

<span class="c"># untained control plane</span>
kubectl taint nodes <span class="nt">--all</span> node-role.kubernetes.io/control-plane-
node/cplane1 untainted

<span class="c"># recheck taint</span>
<span class="nv">$ </span>kubectl describe nodes | <span class="nb">grep</span> <span class="nt">-i</span> taint
Taints:             &lt;none&gt;
Taints:             &lt;none&gt;

<span class="c"># check nodes</span>
kubectl get nodes <span class="nt">-o</span> wide
</code></pre></div>    </div>

    <p>calicoctl</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-L</span> https://github.com/projectcalico/calico/releases/download/v3.29.2/calicoctl-linux-amd64 <span class="nt">-o</span> calicoctl
<span class="nb">chmod</span> +x ./calicoctl

<span class="c"># move calicoctl to accessible on system path</span>
<span class="nb">mv </span>calicoctl /usr/local/bin/
</code></pre></div>    </div>
  </li>
</ul>

<hr />

<p>As you may already noticed, we got some small errors with containerd images.</p>

<blockquote>
  <p>W0818 20:50:23.203697 11745 checks.go:846] detected that the sandbox image “registry.k8s.io/pause:3.8” of the container runtime is inconsistent with that used by kubeadm.It is recommended to use “registry.k8s.io/pause:3.10” as the CRI sandbox image.</p>
</blockquote>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># open containerd config and update the pause image to desired version 3.10</span>
vim /etc/containerd/config.toml

<span class="c"># after restart service to apply</span>
systemctl restart containerd
</code></pre></div></div>

<h3 id="installing-useful-tools">Installing useful tools</h3>

<p><code class="language-plaintext highlighter-rouge">kubens</code>, <code class="language-plaintext highlighter-rouge">kubectx</code>, <code class="language-plaintext highlighter-rouge">k9s</code></p>

<p>Since we’re running on Ubuntu 24, there is a more convenient way to install our software then via <code class="language-plaintext highlighter-rouge">/etc/apt/source.list.d/</code></p>

<p>We will utilize <code class="language-plaintext highlighter-rouge">snap</code> ubuntu package manager.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>snapd

<span class="c"># installing programs</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>kubectx <span class="nt">--classic</span> <span class="c"># contains kubens</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>k9s
</code></pre></div></div>

<h2 id="end">End</h2>

<p>Thank you your time and staying until the end. Hope you had as much fun reading as I had while preparing and documenting the steps in this article.</p>

<p>Stay save and until the next time.</p>

<h3 id="resources">Resources</h3>

<ul>
  <li><a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a></li>
  <li><a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/">https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</a></li>
  <li><a href="https://www.baeldung.com/ops/kubernetes-uninstall">https://www.baeldung.com/ops/kubernetes-uninstall</a></li>
  <li><a href="https://www.howtoforge.com/how-to-setup-kubernetes-cluster-with-kubeadm-on-ubuntu-22-04/">https://www.howtoforge.com/how-to-setup-kubernetes-cluster-with-kubeadm-on-ubuntu-22-04/</a></li>
  <li><a href="https://www.linuxtechi.com/install-kubernetes-on-ubuntu-22-04/">https://www.linuxtechi.com/install-kubernetes-on-ubuntu-22-04/</a></li>
  <li><a href="https://www.servethehome.com/introducing-project-tinyminimicro-home-lab-revolution/hp-elitedesk-705-g3-kubernetes/">https://www.servethehome.com/introducing-project-tinyminimicro-home-lab-revolution/hp-elitedesk-705-g3-kubernetes/</a></li>
</ul>

<h2 id="faq">FAQ</h2>

<h3 id="have-you-made-a-mistake-and-need-reset">Have you made a mistake and need reset?</h3>

<p>After that you can do <code class="language-plaintext highlighter-rouge">kubeadm reset</code> and repeat seteps (recommend)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># delete calico</span>
kubectl delete <span class="nt">-f</span> https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/custom-resources.yaml
kubectk delete deploy tigera-operator <span class="nt">-n</span> tigera-operator

<span class="c"># reset cplane and workers</span>
kubeadm reset

<span class="nb">sudo </span>apt-get purge kubeadm kubectl kubelet kubernetes-cni
<span class="nb">sudo </span>apt-get autoremove

<span class="c"># Remove Related Files and Directories</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> ~/.kube
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/cni /etc/kubernetes <span class="nb">rm</span> <span class="nt">-f</span> /etc/apparmor.d/docker /etc/systemd/system/etcd<span class="k">*</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/dockershim /var/lib/etcd /var/lib/kubelet <span class="se">\</span>
         /var/lib/etcd2/ /var/run/kubernetes

<span class="c"># Clear out the Firewall Tables and Rules (for some must be root user)</span>
<span class="c">#</span>
<span class="c"># flushing and deleting the filter table</span>
<span class="nb">sudo </span>iptables <span class="nt">-F</span> <span class="o">&amp;&amp;</span> iptables <span class="nt">-X</span>
<span class="c"># lush and delete the NAT (Network Address Translation) table</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-F</span> <span class="o">&amp;&amp;</span> iptables <span class="nt">-t</span> nat <span class="nt">-X</span>
<span class="c"># flush and remove the chains and rules in the raw table</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> raw <span class="nt">-F</span> <span class="o">&amp;&amp;</span> iptables <span class="nt">-t</span> raw <span class="nt">-X</span>
<span class="c"># remove the chains and rules in the mangle table</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> mangle <span class="nt">-F</span> <span class="o">&amp;&amp;</span> iptables <span class="nt">-t</span> mangle <span class="nt">-X</span>

<span class="c"># optional: remove containerd</span>
<span class="nb">sudo </span>apt-get remove containerd.io
<span class="nb">sudo </span>apt-get purge <span class="nt">--auto-remove</span> containerd

<span class="c"># if cplane continue to init</span>
kubeadm init ... <span class="c"># and rest of flags</span>
<span class="c"># if worker continue to join</span>
<span class="nb">sudo </span>apt-get purge <span class="nt">--auto-remove</span> containerd
kubeadm <span class="nb">join</span> ... <span class="c"># and rest from kubeadm init</span>
</code></pre></div></div>

<h3 id="after-shutting-down-nodes">After shutting down Nodes</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>maciej@cplane1:~<span class="nv">$ </span>kubectl get pv
E1225 20:37:45.232733    7857 memcache.go:265] <span class="s2">"Unhandled Error"</span> <span class="nv">err</span><span class="o">=</span><span class="s2">"couldn't get current server API group list: Get </span><span class="se">\"</span><span class="s2">https://192.168.178.200:6443/api?timeout=32s</span><span class="se">\"</span><span class="s2">: dial tcp 192.168.178.200:6443: connect: connection refused"</span>
E1225 20:37:45.234160    7857 memcache.go:265] <span class="s2">"Unhandled Error"</span> <span class="nv">err</span><span class="o">=</span><span class="s2">"couldn't get current server API group list: Get </span><span class="se">\"</span><span class="s2">https://192.168.178.200:6443/api?timeout=32s</span><span class="se">\"</span><span class="s2">: dial tcp 192.168.178.200:6443: connect: connection refused"</span>
E1225 20:37:45.235670    7857 memcache.go:265] <span class="s2">"Unhandled Error"</span> <span class="nv">err</span><span class="o">=</span><span class="s2">"couldn't get current server API group list: Get </span><span class="se">\"</span><span class="s2">https://192.168.178.200:6443/api?timeout=32s</span><span class="se">\"</span><span class="s2">: dial tcp 192.168.178.200:6443: connect: connection refused"</span>
E1225 20:37:45.237148    7857 memcache.go:265] <span class="s2">"Unhandled Error"</span> <span class="nv">err</span><span class="o">=</span><span class="s2">"couldn't get current server API group list: Get </span><span class="se">\"</span><span class="s2">https://192.168.178.200:6443/api?timeout=32s</span><span class="se">\"</span><span class="s2">: dial tcp 192.168.178.200:6443: connect: connection refused"</span>
E1225 20:37:45.238673    7857 memcache.go:265] <span class="s2">"Unhandled Error"</span> <span class="nv">err</span><span class="o">=</span><span class="s2">"couldn't get current server API group list: Get </span><span class="se">\"</span><span class="s2">https://192.168.178.200:6443/api?timeout=32s</span><span class="se">\"</span><span class="s2">: dial tcp 192.168.178.200:6443: connect: connection refused"</span>
The connection to the server 192.168.178.200:6443 was refused - did you specify the right host or port?
</code></pre></div></div>

<p>Solution:</p>

<p>Just wait a moment until system will start and connect.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>maciej@cplane1:~<span class="nv">$ </span>k get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
vol1   1Gi        RWO            Retain           Bound    default/registry-claim0                  &lt;<span class="nb">unset</span><span class="o">&gt;</span>                          117d
vol2   200Mi      RWO            Retain           Bound    default/nginx-claim0                     &lt;<span class="nb">unset</span><span class="o">&gt;</span>                          117d
</code></pre></div></div>]]></content><author><name>Maciej Sypien</name></author><category term="macos" /><category term="linux" /><category term="k8s" /><category term="ckad" /><summary type="html"><![CDATA[From my earliest memories, I’ve always been fascinated by building my own Kubernetes cluster at home. Recently, I made it a reality by assembling a small lab using affordable PCs. In this post, I’ll share the insights and lessons learned from my first attempt to set up a home Kubernetes cluster. Whether you’re a seasoned admin or just starting out, I hope to provide valuable information on the hardware, software, and configurations that worked for me.]]></summary></entry><entry><title type="html">Use shell only for small to medium size programs</title><link href="https://egel.github.io/2024/03/26/use-shell-only-for-small-to-medium-size-programs.html" rel="alternate" type="text/html" title="Use shell only for small to medium size programs" /><published>2024-03-26T00:00:00+00:00</published><updated>2024-03-26T00:00:00+00:00</updated><id>https://egel.github.io/2024/03/26/use-shell-only-for-small-to-medium-size-programs</id><content type="html" xml:base="https://egel.github.io/2024/03/26/use-shell-only-for-small-to-medium-size-programs.html"><![CDATA[<p>I have seen, written, and edited in may carrier many small, medium, and large-size shell programs, both in pure <code class="language-plaintext highlighter-rouge">sh</code> and <code class="language-plaintext highlighter-rouge">bash</code>. The major thing I have learned over this period of time with shell-scripting are the following patterns:</p>

<ul>
  <li>use shell to write only small scripts</li>
  <li>use shell to write small-to-medium size scripts ONLY when used by a singular operating system, or when frequently executed in pipeline</li>
  <li>if your shell program becomes larger and more complex consider using some more verbose language with good testing capabilities</li>
  <li>if you are writing scripts that MUST BE cross-compatible to different systems, consider using compiled languages that can produce you easy-to-download binaries for your users</li>
</ul>

<h2 id="reasoning">Reasoning</h2>

<p>In following sections I will try to summarize for you my thoughts in which situations shell-scripting is usefull, and when it could be worth consider replacing it different solution.</p>

<p>Although I have to honestly say, there is no right or wrong solution. This case is no different, and every situation could have own advantages and disadvantages. Therefore there are some cases where let’s say, building a lagre shell script was a better solution then the following other altervative way.</p>

<p>If you ask any resonable IT person, will tell you “it depends” as the ultimate answer to almost any IT questions. And in most cases they will be right. Weird! …but true.</p>

<h3 id="small-size">Small size</h3>

<p>Writing shell small and impactful scripts is awesome experiance. With just a few lines of code that work out of the box on almost any Linux or macOS systems, is very easy and get the job done fast. You basically perfectly covering the famous 80/20 rule.</p>

<p>In this moment you have all the benefits of automatizing your idea in almost the blink of an eye, and same time be able to execute it with almost no dependencies. Sometimes you must download a few additional programs to make it work, but this is a relatively very small cost to the effort you put in. Additionally, there is an enormous amount of great CLI tools that can be easily downloaded and used.</p>

<h3 id="medium">Medium</h3>

<p>Over time your script gets bigger and grows to MEDIUM size. In this moment you discover for example that:</p>

<ul>
  <li>I need to add a few flags to enhance program capabilities</li>
  <li>or I will need my script to be used by users on different Operating Systems</li>
  <li>or my script is already quite big (~500 lines of code), and there are no tests, so further expansion will no longer quickly guarantee that any modifications will be without errors.</li>
</ul>

<p>When this is happening, you should consider revisiting “time &amp; efforts” to the “cost &amp; future expanding”.
This is totally valid to ask yourself “What I am aiming for with this script”. If you see the end of the expansion, and there will be no need for further modification, finish it and be done with it.</p>

<p>However, if you know that this is just the next chapter, and your script will naturally grow performing more and more tasks. Then this case I most often consider as the last time to provide mandatory test coverage, in order to protect all critical functionality of your solution<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. Without this, modifying the script may provide to someone sleepless nights due to amount of errors each modification can produce.</p>

<p>Of course, if you are a skilled person and “you know what you do” (or you do it as non-business critical case) - sure, go on, continue whatever you like to do.</p>

<p>On the other hand, if you need to grow, and this solution simply MUST work, do yourself a favor and focus on providing stability to your code by good test coverage, or finding more suitable alternative. Good candidate it to use othere language with which you can more easily provide a reliable growth of your program with also relatively minimal efforts.</p>

<h3 id="large">Large</h3>

<p>Ooooh man. You are about to realize that your script is expanding to the size of a small book, or/and you feel already losing control over it. You supporting so many edge cases, or/and injecting more and more complex to understand syntax. It involves into many mysterious code hacks that look like abominated devil incantations straight from the medieval age sorcery book.</p>

<p>If this is true for you this is time to go serious with considering to switch. In these cases some compiled languages are the best option. Mainly to give you, and your developers a better chance to understand successfully contribute in updating, or expanding further your project.</p>

<p>I can’t tell you what is best for you, as I mentioned before “it depends” and each case may be different. Although my golden rule that applies the 80/20 rule of all my recent scenarios, is to use Golang with some [cobra][weblinki-github-spf13-cobra] library, or if speed is critical use Zig/Rust. Nevertheless, at this level, any choice will be valid option if comes with good testing tools. Having control over the program and guaranteeing its behavior by good coverage, will bing to you a calm mind and provide reliability to your business.</p>

<p>The other factors that could make sense in choosing some other alternative options then shell scripts are:</p>

<ul>
  <li>the easy of use and understanding</li>
  <li>maintenance over longer period of time</li>
  <li>how often the solution/language force you apply the changes</li>
  <li>final program size</li>
  <li>compilation speed</li>
  <li>existing knowledge</li>
  <li>personal/developer preferences</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>I believe that shell-scripting is one of the most satisfying things that each software engineer does, despite of the level of expertise.
However, remember to not lock yourself from often reflecting that each case or change in code may be worth considering following a different path than the current one.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Little disclaimer here. I am not saying that when your script is small should not have tests. All your code should have good test coverage, and I am also fan for providing test for the code I ship. Although, in my carrier I saw many small, medium, and large shell scripts without any tests. And when I asked “why there are no tests”, there was always some “good” excuse: no time, need to focus on next feature, ect, but I do not agree with it. Without any tests you can’t prove anyone that the code you wrote is worth something. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Maciej Sypien</name></author><category term="shell" /><category term="bash" /><category term="sh" /><category term="zsh" /><summary type="html"><![CDATA[I have seen, written, and edited in may carrier many small, medium, and large-size shell programs, both in pure sh and bash. The major thing I have learned over this period of time with shell-scripting are the following patterns:]]></summary></entry><entry><title type="html">Fast massive photo conversion</title><link href="https://egel.github.io/2023/04/05/fast-massive-photo-conversion.html" rel="alternate" type="text/html" title="Fast massive photo conversion" /><published>2023-04-05T00:00:00+00:00</published><updated>2023-04-05T00:00:00+00:00</updated><id>https://egel.github.io/2023/04/05/fast-massive-photo-conversion</id><content type="html" xml:base="https://egel.github.io/2023/04/05/fast-massive-photo-conversion.html"><![CDATA[<p>I know about this trick for a long time but finally found some time to share it with you.</p>

<h2 id="story">Story</h2>

<p>Once I had to send a couple of pictures via e-mail (there were about 20 images). I am aware there are more efficient and smarter options to share images with other person, although sometimes the other side demands it to be an accually e-mail. 🙈🤷‍♂️</p>

<p>I decide to reduce the size, pack it and send it. I realized that images are stored in a strange Apple extension called <code class="language-plaintext highlighter-rouge">*.heic</code>, so this is not a desired extension that someone on the other side would like to see. Furthermore, the original images were big (3-5MB) and I really don’t want to end up with manually resizing and saving all images with desired extension.</p>

<p>Fortunately, there is a brilliant program called <a href="https://imagemagick.org/">imagemagic</a> which saves my ass a thousand times - and btw it’s actually now a group of different programs, but never mind.</p>

<p>With this tool, you can in a good well spirit of automation convert all images from HEIC to more popular JPEG and reduce the size (quality), so they can fit in a standard email size.</p>

<h2 id="how-to">How-to</h2>

<p>Install <a href="https://imagemagick.org/"><code class="language-plaintext highlighter-rouge">imagemagic</code></a> on your computer (it’s free and available on all platforms). If you like my editing, especially the <code class="language-plaintext highlighter-rouge">heic</code> type of images, you may need to install an additional library called <a href="https://www.libde265.org/"><code class="language-plaintext highlighter-rouge">libheif</code></a>. For other platforms use <a href="https://github.com/strukturag/libheif">alternative</a>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>libheif imagemagick parallel
</code></pre></div></div>

<p>Now you just need to specify a directory where your images are stored and execute the command:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>magick mogrify <span class="nt">-format</span> jpg <span class="nt">-quality</span> 60 <span class="k">*</span>.HEIC
</code></pre></div></div>

<div class="alert alert-info" role="alert">
    <p>Pay attention to the case sensitivity of the letters in the image extension, as it may be different than in my command.</p>
    <p>You can also use instead full of <code>magick mogrify ...</code>, just <code>mogrify ...</code> because after the installation of imagemagic all its sub-programs should be available for you directly in the command line.</p>
</div>

<p>The <code class="language-plaintext highlighter-rouge">mogrify</code> program use only 1 CPU at the time. If you have a lot of pictures to convert, you may want to utilize more of your CPUs with GNU <code class="language-plaintext highlighter-rouge">parallel</code>. I wrapped the command above with <code class="language-plaintext highlighter-rouge">parallel</code> and if you execute it the conversion should happen much faster.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>parallel magick mogrify <span class="nt">-format</span> jpg <span class="nt">-quality</span> 60 ::: <span class="k">*</span>.HEIC
</code></pre></div></div>

<p>Yay! A few seconds later all images are converted and they’re ready for upload. Thanks, that’s all 🤘. Have fun further exploring imagemagic secrets. It’s definitely extremely helpful software.</p>]]></content><author><name>Maciej Sypien</name></author><category term="macos" /><category term="linux" /><category term="photos" /><category term="terminal" /><summary type="html"><![CDATA[I know about this trick for a long time but finally found some time to share it with you.]]></summary></entry><entry><title type="html">Favourite Music Lists</title><link href="https://egel.github.io/2023/03/01/favourite-music-playlists.html" rel="alternate" type="text/html" title="Favourite Music Lists" /><published>2023-03-01T00:00:00+00:00</published><updated>2023-03-01T00:00:00+00:00</updated><id>https://egel.github.io/2023/03/01/favourite-music-playlists</id><content type="html" xml:base="https://egel.github.io/2023/03/01/favourite-music-playlists.html"><![CDATA[<p>There is some time for all of us when we start focusing, relaxing, driving a car, or just thinking (you name it), having some sound vibing in your ear which make you sing this song in your head. I also like to fill my time with some music, although it varies depending on the situation I am currently in.</p>

<p>Like you, for years my sound taste also was changing. My childhood, my character, the place I lived, and the people I grow-up with, shaped the music taste I have today - possibly it was similar for you.</p>

<p>When I was a child, I listen to a lot of rock/pop/hip-hop/rap music and played video games (mostly RPG where I wasn’t a good player tho). After the years, I noticed, the time I spent in my youth, created unique moments/atmospheres in my brain, that forged my music taste. For example, I really enjoy nowadays getting back to the move &amp; game soundtracks while focusing, as this bringing me some kind of relief while working with micro breaks for my brain which the music-memory reveal, and then have a feeling I am much more focused/productive.</p>

<p>Over time, I build a sentiment of old’school grove, jazz, electronic, ambient, house/trance, pop, and rock vibes. Not forget about great movie &amp; game soundtracks I love to listen to and of course some classical pieces.</p>

<p>This is a special article for me. It has a much more personal touch with the music that, is with me for years. I hope you find it likable, and inspirational, and maybe you find something interesting and think someday it was worth your time to check out this post.</p>

<p>Thank your time, and enjoy it as much as I do!</p>

<blockquote>
  <p>Disclaimer: I am not the owner of the songs, therefore, I keep the full names &amp; authors of the songs (or mixes) with links to the sources I know where they still exist. In case some songs will not be available anymore by the time you read them, they could be removed mean, and I am sorry. When this happens, just let me know (tweet me, or use comments), and I will do my best to keep them updated.</p>
</blockquote>

<h3 id="single-songs">Single songs</h3>

<p>There are a couple of songs I dear the most over time, although not all I wish, I could put in here. The question is “<em>why do actually these songs land in the lists below</em>”?
I think there were many reasons “why”. I feel some sort of connection to the songs below, simply from just being sentimental to the past memories I had with them, while some events happened in my life, ending on just liking the kind &amp; vibe the music is giving me.</p>

<p>Below I gather a few lists and grouped them into sections that should be a bit useful for you. The order of the songs is mostly random (there is no 1st/last nor best/worse), and some of them are grouped if they come from one artist to have easier sneek.</p>

<ul>
  <li><a href="https://youtu.be/XDpoBc8t6gE">kudasai - the girl i haven’t met</a></li>
  <li><a href="https://youtu.be/oAGWFop_Hx8">kudasai - technicolor</a></li>
  <li><a href="https://youtu.be/COiIC3A0ROM">Al Green - Let’s Stay Together</a></li>
  <li><a href="https://youtu.be/60ZeFnaMh3s">Make Me Say It Again Girl, Pts. 1 &amp; 2</a></li>
  <li><a href="https://youtu.be/ik_BQYbbZ5U">José González - Heartbeats</a></li>
  <li><a href="https://youtu.be/oU0ZmbBY9QI">DJ Cam Quartet - Rebirth of Cool</a></li>
  <li><a href="https://youtu.be/TgjTKbLfW2E">Skarby - Liber x Doniu</a></li>
  <li><a href="https://youtu.be/0rJA9lnGcP4">UB40 - Kingston Town</a></li>
  <li><a href="https://youtu.be/OKgtA3h8WB8">Watch &amp; Vibe - After Dark - Mr.Kitty (Music Video - Lost In Translation)</a></li>
  <li><a href="https://youtu.be/b_YHE4Sx-08">Max Richter - On The Nature Of Daylight (Entropy)</a></li>
  <li><a href="https://youtu.be/8k2AMDvugzw">Kavinsky - Nightcall</a></li>
  <li><a href="https://youtu.be/OPlK5HwFxcw">Pyotr Ilyich Tchaikovsky - Hymn of the Cherubim</a></li>
  <li><a href="https://youtu.be/oxoqm05c7yA">Electronic Gems - 憂鬱 - Sun</a></li>
  <li><a href="https://youtu.be/wcV1UpZAWAc">College feat. Electric Youth - A Real Hero</a></li>
  <li><a href="https://youtu.be/N2SW_MWBa6w">Home - Decay</a></li>
  <li><a href="https://youtu.be/kOWM9ao38lM">Electric Youth - Innocence</a></li>
  <li><a href="https://youtu.be/V1bFr2SWP1I">Israel “IZ” Kamakawiwoʻole - Somewhere over the Rainbow</a></li>
  <li><a href="https://youtu.be/AiuC_CaObbI">Samuel Barber - Agnus Dei</a></li>
  <li><a href="https://youtu.be/S-Xm7s9eGxU">Erik Satie - Gymnopédie No.1</a></li>
  <li><a href="https://youtu.be/oyVJsg0XIIk">Gotye - Eyes Wide Open</a></li>
  <li><a href="https://youtu.be/RBumgq5yVrA">Passenger - Let Her Go</a></li>
  <li><a href="https://youtu.be/fV4DiAyExN0">Hoobastank - The Reason</a></li>
  <li><a href="https://youtu.be/LWnIEHVoiXg">Lifehouse - Spin</a></li>
  <li><a href="https://youtu.be/6EVyTBgLL1Y">Lifehouse - Take Me Away</a></li>
  <li><a href="https://youtu.be/gnIZ7RMuLpU">Coldplay - In My Place</a></li>
  <li><a href="https://youtu.be/fyMhvkC3A84">Coldplay - Every Teardrop Is a Waterfall</a></li>
  <li><a href="https://youtu.be/dvgZkm1xWPE">Coldplay - Viva La Vida</a></li>
  <li><a href="https://youtu.be/VPRjCeoBqrI">Coldplay - A Sky Full Of Stars</a></li>
  <li><a href="https://youtu.be/Sv6dMFF_yts">Fun.: We Are Young ft. Janelle Monáe</a></li>
  <li><a href="https://youtu.be/X2DTnOH_MGc">Foner - September</a></li>
  <li><a href="https://youtu.be/mLqHDhF-O28">Thirty Seconds To Mars - Closer To The Edge</a></li>
  <li><a href="https://youtu.be/TfyRHcs8GL8">Maximum Love - Feel Electric</a></li>
  <li><a href="https://youtu.be/hrf8cbHSm4I">Ta-ku - Love Again (feat. JMSN &amp; Sango)</a></li>
  <li><a href="https://youtu.be/-fFbeSykaJk">We Are All Astronauts - Ether</a></li>
  <li><a href="https://youtu.be/7AoJwMImQZE">We Are All Astronauts - Doves</a></li>
  <li><a href="https://youtu.be/GemKqzILV4w">Snow Patrol - Chasing Cars (Official Video)</a></li>
  <li><a href="https://youtu.be/8GW6sLrK40k">HOME - Resonance</a></li>
  <li><a href="https://youtu.be/4ud0TImHgVo">HOME- - Flood</a></li>
  <li><a href="https://youtu.be/H2BDtpbL-4c">Paradis - Hémisphère</a></li>
  <li><a href="https://youtu.be/guJ25FxCwmY">Varius Manx - Piosenka księżycowa</a></li>
  <li><a href="https://youtu.be/nZBKFoeDKJo">Beach Boys - Wouldn’t It Be Nice</a></li>
  <li><a href="https://youtu.be/vPRonG87eKw">Beach Boys - Barbara Ann</a></li>
  <li><a href="https://youtu.be/XVnvgycRU9k">Theophilus London - Why Even Try</a></li>
  <li><a href="https://youtu.be/DDt3u2Ev1cI">Chris Rea - Driving Home For Christmas</a></li>
  <li><a href="https://youtu.be/Fkz8i72o20s">Paradis - La Ballade de Jim</a></li>
  <li><a href="https://youtu.be/z9_wLS8zYHA">Portland - Deezy Daisy (Oxford Remix)</a></li>
  <li><a href="https://youtu.be/3kJdQb0Fplg">Woodkid - Brooklyn</a></li>
  <li><a href="https://youtu.be/Gs069dndIYk">Earth, Wind &amp; Fire - September</a></li>
  <li><a href="https://youtu.be/sfTf2EdeEnA">Nas - Mastermind (Sourface Remix)</a></li>
  <li><a href="https://youtu.be/4ZHwu0uut3k">Tom Odell - Another Love (Zwette Edit)</a></li>
  <li><a href="https://youtu.be/K8xv6-Fc2fY">Satin Jackets - Hollywood (feat. Eric Cozier)</a></li>
  <li><a href="https://youtu.be/PoRk-4NOwXE">Jonas Mantey - Frei</a></li>
  <li><a href="https://youtu.be/9RMHHwJ9Eqk">n u a g e s - Dreams</a></li>
  <li><a href="https://youtu.be/xLEoAYElRDc">Phoria - Red (Need a Name Remix)</a></li>
  <li><a href="https://youtu.be/E3hltc8Wgfw">Passion Pit - Constant Conversation (St. Lucia Remix)</a></li>
  <li><a href="https://youtu.be/CC5ca6Hsb2Q">Robert Miles - Children [Dream Version]</a></li>
  <li><a href="https://youtu.be/fo9oCklYqIE">Lane 8 - Every Night</a></li>
  <li><a href="https://youtu.be/a7SouU3ECpU">Alesso - Heroes (we could be) ft. Tove Lo</a></li>
  <li><a href="https://youtu.be/JRfuAukYTKg">David Guetta - Titanium ft. Sia</a></li>
  <li><a href="https://youtu.be/-PDter1EKbk">Michael Nyman - The Other Side</a></li>
  <li><a href="https://youtu.be/NU9JoFKlaZ0">Green Day - Wake Me Up When September Ends</a></li>
  <li><a href="https://youtu.be/k016mR9tQdI">Ludovico Einaudi - Fly</a></li>
  <li><a href="https://youtu.be/5JU_l3w0f7o">Und1fin3d - Try</a></li>
  <li><a href="https://youtu.be/9fAZIQ-vpdw">Jon Schmidt - All of Me (original tune)</a></li>
  <li><a href="https://youtu.be/U7RYSQvrUrc">Johann Sebastian Bach-Air on G String</a></li>
</ul>

<h3 id="mixes">Mixes</h3>

<ul>
  <li><a href="https://youtu.be/rahRaVtEQaM">Majestic - Winter Calm - A Sunday Chill Mix</a></li>
  <li><a href="https://youtu.be/dTW2MxfqVLI">Blissful Solitude - Ambient Mix</a></li>
  <li><a href="https://youtu.be/YOJsKatW-Ts">the bootleg boy - RAINING IN ＯＳＡＫＡ</a></li>
</ul>

<h3 id="game-soundtracks-single--mixes">Game Soundtracks (single &amp; mixes)</h3>

<ul>
  <li><a href="https://youtu.be/_34gUVqLkAo">The Elders Scroll V Skyrim Soundtrack - The Streets of Whiterun</a></li>
  <li><a href="https://youtu.be/8F1-1j_ZDgc">Skyrim - Music &amp; Ambience - Rainy Day</a></li>
  <li><a href="https://youtu.be/xULTMMgwLuo">The Elder Scrolls III - Morrowind Soundtrack</a></li>
  <li><a href="https://youtu.be/fngETb9TncM">rain - Relaxing music from Baldur’s Gate 1 &amp; 2, Icewind Dale and Neverwinter Nights soundtrack</a></li>
  <li><a href="https://youtu.be/22bd9soyRIA?t=179">Icewind Dale 2 Soundtrack - Skeleton of a Town</a></li>
  <li><a href="https://youtu.be/22bd9soyRIA?t=1104">Icewind Dale 2 Soundtrack - Return to Kuldahar</a></li>
  <li><a href="https://youtu.be/JveR2Qfvq-I">Deus Ex: Mankind Divided - Ambient Mix</a></li>
  <li><a href="https://youtu.be/8GYL6c_GTE0">The Witcher 3: One hour of Emotional and Relaxing Music</a></li>
  <li><a href="https://youtu.be/8GYL6c_GTE0">Prestigious_Gaming - Prestigious_GamingThe Witcher 3: One hour of Emotional and Relaxing Music</a></li>
  <li><a href="https://youtu.be/q43G9FTaomg">PecoreSoundtracks - Heroes 3 of Might and Magic Soundtrack (ost) [complete / HD]</a></li>
  <li><a href="https://youtu.be/IJiHDmyhE1A">Christopher Tin - Baba Yetu</a></li>
  <li><a href="https://youtu.be/UEaCNtIHSgY">Heroes Might &amp; Magic - Dirt Theme</a></li>
</ul>

<h3 id="movie-soundtracks-single--mixes">Movie Soundtracks (single &amp; mixes)</h3>

<ul>
  <li><a href="https://youtu.be/6CJ96LGGP6w">How to train your dragon Score - Forbidden friendship</a></li>
  <li><a href="https://youtu.be/rY07_dX3F88">How to train your dragon Score: Romantic flight</a></li>
  <li><a href="https://youtu.be/vlEN8svyHj8">Tron Legacy - 03 The Son of Flynn - Daft Punk</a></li>
  <li><a href="https://youtu.be/aFIXKXYfEy0">Tron Legacy - 10 Adagio For TRON - Daft Punk</a></li>
  <li><a href="https://youtu.be/yRvVyMuWbpM">Tron Legacy - 15 Solar Sailer - Daft Punk</a></li>
  <li><a href="https://youtu.be/06MHAIZALjw">Tron Legacy - Special Edition CD 2 - 02 - Encom Part II</a></li>
  <li><a href="https://youtu.be/rvFrRGZ6kh4">Edward Shearmur - K-Pax Soundtrack</a></li>
  <li><a href="https://youtu.be/34F_WHJ-AZE">Man of Steel - Trailer Music - Storm by Craig Armstrong</a></li>
  <li><a href="https://youtu.be/VAZsf8mTfyk">300 OST - Returns a King</a></li>
  <li><a href="https://youtu.be/JNzvqDVm0hw">Basil Poledouris - Hymn for Red October</a></li>
  <li><a href="https://youtu.be/CIaIJoG5EO4">Hans Zimmer - Last Samurai - A Way of Life</a></li>
  <li><a href="https://youtu.be/Z1vp9HgVL6I">Hans Zimmer - Last Samurai - Hard Teacher</a></li>
  <li><a href="https://youtu.be/pcaOVCnsYJM">Hans Zimmer - Film Score Composer - Interstellar - Recording of Pipe Organ</a></li>
  <li><a href="https://youtu.be/vOf78z3Hi4U">Hans Zimmer - The Lion King Soundtrack - Under the Stars</a></li>
  <li><a href="https://youtu.be/C2U-lL_qdTI">Der Demiurg - The Cloud Atlas Sextet</a></li>
  <li><a href="https://youtu.be/mXttp8_xSHQ">Cloud Atlas End Title</a></li>
  <li><a href="https://youtu.be/SORlV8qAuIs">Alan Silvestri- Forrest Gump - The Soundtrack</a></li>
  <li><a href="https://youtu.be/218ELDhIiGY?t=457">Dragon Ball Z - M710 - Prologue &amp; Subtitle I</a></li>
  <li><a href="https://youtu.be/218ELDhIiGY?t=2429">Dragon Ball Z - M1711 - Prologue &amp; Subtitle II</a></li>
</ul>

<h2 id="playlists">Playlists</h2>

<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLLREqAE2gkX7KVjBjUpG0a2hbJ-IuRY9B">For Programming</a> - here I keep a track of songs I mostly listening to while programming.</li>
  <li><a href="https://www.youtube.com/playlist?list=PLLREqAE2gkX6b2fqRggv1eV-fQUpVbrZc">For Car</a> - in car I like “gently intense vibes” (if even I can say that). They suppose to gently land in the ears, but not to gently so they not lulaby me while driving 🙈</li>
  <li><a href="https://www.youtube.com/playlist?list=PLLREqAE2gkX6z_1qAuJOCn5Spf4kvtuM7">Music from Childhood</a> - it’s mostly some polish songs, a sentimental vibes from my childhood and where I was born and growing up.</li>
</ul>]]></content><author><name>Maciej Sypien</name></author><category term="diarty" /><summary type="html"><![CDATA[There is some time for all of us when we start focusing, relaxing, driving a car, or just thinking (you name it), having some sound vibing in your ear which make you sing this song in your head. I also like to fill my time with some music, although it varies depending on the situation I am currently in.]]></summary></entry><entry><title type="html">Setup macbook M1 for web developer</title><link href="https://egel.github.io/2023/03/01/setup-new-macbook-m1.html" rel="alternate" type="text/html" title="Setup macbook M1 for web developer" /><published>2023-03-01T00:00:00+00:00</published><updated>2023-03-01T00:00:00+00:00</updated><id>https://egel.github.io/2023/03/01/setup-new-macbook-m1</id><content type="html" xml:base="https://egel.github.io/2023/03/01/setup-new-macbook-m1.html"><![CDATA[<p>Setuping mac is definitely not an every day task. It’s usually long process, in order to get an efficient work station. In this article, I want to show you, how I approach to configure M1. I will share the process with the programs and the preferences I use.</p>

<div class="alert alert-info" role="alert">
    <b>Little disclaimer</b>: I am not setting up macs every day, so when you may read this article, some things may outdate. If something will not work, toot me <a href="https://twitter.com/MaciejSypien">tweet me</a> or let me know in the comments, so I could fix it. Thanks! 🤘
</div>

<h2 id="prerequisite">Prerequisite</h2>

<ul>
  <li>prepare some time ~1-2h</li>
  <li>good internet connection (as there will be plenty things to download)</li>
  <li>something to drink</li>
  <li>positive mood 😉 - I will try to make it as easy as possible for you</li>
</ul>

<h3 id="browsers">Browsers</h3>

<ul>
  <li><a href="https://brave.com/">Brave</a> (it’s based on chromium)</li>
  <li><a href="https://www.mozilla.org/en-US/firefox/new/">Firefox</a></li>
  <li><a href="https://www.microsoft.com/en-gb/edge">Edge</a></li>
  <li><a href="https://www.apple.com/safari/">Safari</a> should be already installed</li>
</ul>

<h3 id="password-managers">Password managers</h3>

<ul>
  <li>First download your password manager(s)!</li>
  <li>Optionally: download browsers extensions for easier usage</li>
</ul>

<h3 id="apple-developer-tools">Apple Developer Tools</h3>

<p>Unfortunately many programs will need Apple developer tools, so we install them as well via terminal command. Pay attention as this step might take a while… (this step took me ~10-15min)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xcode-select <span class="nt">--install</span>
</code></pre></div></div>

<div class="alert alert-info" role="alert">
    In case of problems with installation via terminal, follow <a href="https://stackoverflow.com/a/52522566">this URL</a>.
</div>

<h3 id="brew-package-manager">Brew Package Manager</h3>

<p>The <a href="https://brew.sh/index_de">brew</a> is de facto standard macOS Package Manager.</p>

<p>Following <a href="https://gist.github.com/ChristopherA/a579274536aab36ea9966f301ff14f3f">https://gist.github.com/ChristopherA/a579274536aab36ea9966f301ff14f3f</a></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/bin/bash <span class="nt">-c</span> <span class="s2">"</span><span class="si">$(</span>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="si">)</span><span class="s2">"</span>

<span class="c"># disable analytics</span>
brew analytics off
</code></pre></div></div>

<div class="alert alert-info" role="alert">
    <p>
        Optionally: I am installing my usual packages from my dotfiles. If you are interested take a look on <a href="https://raw.githubusercontent.com/egel/dotfiles/main/configuration/Brewfile">my Brewfile</a>.
    </p>

<pre>
# to install from Brewfile
curl https://raw.githubusercontent.com/egel/dotfiles/main/configuration/Brewfile -o ~/Brewfile

brew bundle install --file=~/Brewfile
</pre>

<p>
    Later you can save all installed packages with: 
    <pre>brew bundle dump --file=~/.private/Brewfile</pre>
</p>
</div>

<h3 id="iterm2">iTerm2</h3>

<p>Installed via brew. In case use: <code class="language-plaintext highlighter-rouge">brew install --cask iterm2</code></p>

<p><strong>font, size, window theme</strong></p>

<p>If you like hack font, do not rush with installing it via: <code class="language-plaintext highlighter-rouge">brew install font-hack</code>. Instead you may want to have <strong>Hack Font with Powerline symbols</strong> from the NERD Fonts <a href="https://www.nerdfonts.com/font-downloads">https://www.nerdfonts.com/font-downloads</a>. To see a small difference take a look on screenshot below with Neovim.</p>

<table>
  <thead>
    <tr>
      <th><a href="https://sourcefoundry.org/hack/">Hack Font (Standard)</a></th>
      <th><a href="https://www.nerdfonts.com/">Hack Nerd Font</a> (with powerline symbols)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">brew install font-hack</code></td>
      <td><code class="language-plaintext highlighter-rouge">brew tap homebrew/cask-fonts; brew install font-hack-nerd-font</code></td>
    </tr>
    <tr>
      <td><img src="/assets/posts/setup-macbook-m1/item2-with-hack-font-standard.png" alt="hack-font" /></td>
      <td><img src="/assets/posts/setup-macbook-m1/item2-with-hack-nerd-font.png" alt="hack-nerd-font" /></td>
    </tr>
  </tbody>
</table>

<div class="alert alert-info" role="alert">
    <p>
        In case you have already downloaded dotfiles locally you can copy all fonts into your local user folder. Nerd Fonts are also there 😉.
    </p>

<pre>
cp ~/privatespace/github.com/egel/dotfiles/assets/fonts/* ~/Library/Fonts
</pre>
</div>

<p>Below you will find some screenshots with configuration I usually setup.</p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-font.png" alt="iterm2-font" /></p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-theme-minimal.png" alt="iterm2-window-theme" /></p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-general-selection.png" alt="iterm2-general-selection" /></p>

<p><strong>Colorscheme Gruvbox</strong></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://raw.githubusercontent.com/herrbischoff/iterm2-gruvbox/master/gruvbox.itermcolors <span class="nt">-o</span> ~/Downloads/gruvbox.itermcolors
</code></pre></div></div>

<p>Import downloaded gruvbox color preset into iTerm (2), and after importing activate theme (3).</p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-import-gruvbox.png" alt="iterm2-colorscheme" /></p>

<p><strong>Iterate through the arguments of previous commands</strong> - this is awesome feature of ZSH shell, so if you are interested follow my other post how to <a href="/2022/03/27/loop-through-previous-arguments-from-command-line.html">loop through previous arguments</a>.</p>

<p>Finally you should see something like this:</p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-final-window.png" alt="iterm2-final-window" /></p>

<h3 id="ides-vscode-idea">IDEs (VScode, IDEA)</h3>

<p>Download the basic editors and IDEs.</p>

<ul>
  <li><a href="https://www.sublimetext.com/download_thanks?target=mac">Sublime</a> - best for fast small changes</li>
  <li><a href="https://code.visualstudio.com/docs/?dv=osx">VS Code</a> - personal coding IDE</li>
  <li><a href="https://www.jetbrains.com/idea/download/?fromIDE=#section=mac">IDEA</a> - work coding IDE (especially GoLand, DataGrip, WebStorm)</li>
</ul>

<p><strong>Mobile App development</strong>:</p>

<ul>
  <li><a href="https://developer.android.com/studio">Android Studio</a> - if you do some Android</li>
  <li><a href="https://apps.apple.com/de/app/xcode/id497799835?l=en&amp;mt=12">Xcode</a> - if you do some iOS/macOS</li>
</ul>

<h3 id="git">Git</h3>

<p>I put this out of dotfiles as git is essential to do any further steps. Later we will update <code class="language-plaintext highlighter-rouge">.gitconfig</code> to be in synch with our dotfiles repo.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://raw.githubusercontent.com/egel/dotfiles/main/configuration/.gitconfig <span class="nt">-P</span> ~/
wget https://raw.githubusercontent.com/egel/dotfiles/main/configuration/.gitconfig.local <span class="nt">-P</span> ~/
wget https://raw.githubusercontent.com/egel/dotfiles/main/configuration/.gitconfig.local <span class="nt">-O</span> ~/.gitconfig.local_work
</code></pre></div></div>

<p>Later in GPG section, we will make sure that gpg keys will be properly added to <code class="language-plaintext highlighter-rouge">.local</code> &amp; <code class="language-plaintext highlighter-rouge">.local_work</code> files, as they will be needed to sign the commits.</p>

<p>For <strong>linux</strong> &amp; <strong>macOS (Intel)</strong>, at this moment you would need to run this command <code class="language-plaintext highlighter-rouge">git config --global gpg.program $(which gpg)</code>, so the path to gpg program can be correctly updated in <code class="language-plaintext highlighter-rouge">.gitconfig</code>.</p>

<h3 id="ssh">SSH</h3>

<h4 id="create-ssh-keys">Create ssh keys</h4>

<p>I love gitlab page for <a href="https://docs.gitlab.com/ee/user/ssh.html">configuration of SSH keys</a>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen <span class="nt">-t</span> ed25519 <span class="nt">-C</span> <span class="s2">"johndoe@example.com"</span>
</code></pre></div></div>

<div class="alert alert-info" role="alert">
<p>If you forgot to add passphrase for your key use this command. See more at <a href="https://docs.gitlab.com/ee/user/ssh.html#update-your-ssh-key-passphrase">update your ssh-key passphrase</a>.</p>

<pre>
ssh-keygen -p -f /path/to/ssh_key
</pre>
</div>

<h4 id="configure-ssh">Configure ssh</h4>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ~/.ssh/config</span>

<span class="c"># Connect to gitlab.com</span>
Host gitlab.com
HostName gitlab.com
Preferredauthentications publickey
IdentityFile ~/.ssh/id_ed25519
</code></pre></div></div>

<div class="alert alert-info" role="alert">
<p>If you want you can also use <code>ssh-agent</code>, but if you plan to use GPG to sign messages, <code>ssh-agent</code> you can replace with <code>gpg-agent</code>.</p>

<p>More information you can find in here: <a href="https://unix.stackexchange.com/a/250045">gpg-agent instead of ssh-agent</a>.</p>
</div>

<h3 id="synchronize-your-dotfiles">Synchronize your dotfiles</h3>

<p>I like to make my files synchronized with my remote repository - this helps me to update main origin when my local changes arise.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/privatespace/github.com/egel
<span class="nb">cd</span> ~/privatespace/github.com/egel
git clone git@github.com:egel/dotfiles.git
</code></pre></div></div>

<p>Re-linking the files that was directly downloaded from the repo, in order to get full synchronization with the private dotfiles repository.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># gitconfig</span>
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gitconfig ~/.gitconfig

<span class="c"># idea (for vim plugin)</span>
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.ideavimrc ~/.ideavimrc
</code></pre></div></div>

<div class="alert alert-info">
<p><b>Optional:</b> In my configuration I setup few additional files, that help me manage my dotfiles. Like storing private passwords, having additional private configurations, ect. Those files by the design are meant NOT BE STORED under version control systems.</p>
<p>Also make sure the files have correct permissions, only for you.</p>

<pre>
touch ~/.zshrc.local
chmod 600 ~/.zshrc.local

touch ~/.envpass.private
chmod 600 ~/.envpass.private
</pre>
</div>

<h4 id="verify-your-connection">Verify your connection</h4>

<p>At this moment you should check if your connection is established. Running the command the second time should give you message with your git user.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-T</span> git@gitlab.com

<span class="c"># run it 2nd time, to get user</span>
<span class="nv">$ </span>ssh <span class="nt">-T</span> git@gitlab.com
Welcome to GitLab, @john.doe!
</code></pre></div></div>

<h3 id="gpg">GPG</h3>

<div class="alert alert-warning">
    Before starting this section make sure you know and understand why signing your own commits may be so important for you. I better explain this in my another article: <a href="/2019/03/24/the-lesson-of-verifying-git-commits.html">The lesson of verifying Git commits</a>.
</div>

<p>Let’s start with basics, like linking configuration folder with local directory.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.gnupg
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gnupg/dirmngr.conf ~/.gnupg/dirmngr.conf
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gnupg/gpg-agent.conf ~/.gnupg/gpg-agent.conf
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gnupg/gpg.conf ~/.gnupg/gpg.conf
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gnupg/sks-keyservers.netCA.pem ~/.gnupg/sks-keyservers.netCA.pem
</code></pre></div></div>

<p>Now, download your gpg keys (private &amp; public) for all your accounts private (or/and work), as we will add them to gpg configuration in order to sign your things (like commits, private emails).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg <span class="nt">--import</span> public_key.asc
gpg <span class="nt">--import</span> private_key.asc
</code></pre></div></div>

<p>Get your gpg fingerprint as we will need to use in git. Execute command below and get “signingKey” = last 16 chars of your fingerprint key. (I add arrow, to make it easier for you).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>gpg <span class="nt">--list-secret-keys</span> <span class="nt">--with-fingerprint</span> <span class="nt">--keyid-format</span> LONG your@email.com

                     |- this would be your <span class="s2">"signingKey"</span> <span class="nt">-------</span> |- or here
                     ▼                                          |
sec   rsa4096/RPGLBRKNFTAZ2S9K 2019-03-17 <span class="o">[</span>SC]                  ▼
      Key fingerprint <span class="o">=</span> VXE7 T2QX FCJZ YQ6L BEGJ  MLMM RPGL BRKN FTAZ 2S9K
uid                 <span class="o">[</span> unknown] John Doe &lt;johndoe@example.com&gt;
ssb   rsa4096/EBEE77C5734494A6 2019-08-23 <span class="o">[</span>E]
</code></pre></div></div>

<p>Restart <code class="language-plaintext highlighter-rouge">gpg-agent</code> in order to use latest configuration.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>killall gpg-agent
2023-03-27 15:13:12 gpg-agent[2253] SIGTERM received - shutting down ...
2023-03-27 15:13:12 gpg-agent[2253] gpg-agent <span class="o">(</span>GnuPG<span class="o">)</span> 2.4.0 stopped

<span class="nv">$ </span>gpg-agent <span class="nt">--daemon</span>
2023-03-27 15:14:46 gpg-agent[2253] gpg-agent <span class="o">(</span>GnuPG<span class="o">)</span> 2.4.0 started
</code></pre></div></div>

<p>Fill the key in <code class="language-plaintext highlighter-rouge">~/.gitconfig.local</code>. So it’s look more-less like:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[user]</span>
    <span class="py">user</span> <span class="p">=</span> <span class="s">"John Doe"</span>
    <span class="py">email</span> <span class="p">=</span> <span class="s">johndoe@example.com</span>
    <span class="py">signingKey</span> <span class="p">=</span> <span class="s">RPGLBRKNFTAZ2S9K</span>

<span class="nn">[commit]</span>
    <span class="py">gpgsign</span> <span class="p">=</span> <span class="s">true</span>
</code></pre></div></div>

<p>Test applied config by reloading terminal and commit something, to see if your commits are signed successfully.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">-S</span> <span class="s2">"test commit with signing"</span>
</code></pre></div></div>

<p>If everything will went successfully, you should get pinentry window, like the one below:</p>

<p><img src="/assets/posts/setup-macbook-m1/gpg-pinentry-mac.png" alt="GPG pinentry-mac" /></p>

<h3 id="vim--neovim">Vim &amp; Neovim</h3>

<p>Without a doubt vim is the king of simple text editors. Many of you may argue, but I don’t want to lead you astray 😆. When I discoverd vim, I was so much confused “why the heck there is so much noise about this thing”! After looong time later, I understood why and I wrote <a href="/2015/04/06/is-worth-to-know-the-vim-editor-and-why.html">Is worth to know the Vim Editor and why?</a> and <a href="/2020/11/29/the-vims-hidden-superpowers.html">The Vim’s hidden superpowers</a>.</p>

<p>Let’s start as usual with configuring vim and neovim.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># nvim (my primary editor)</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/.config
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.config/nvim ~/.config/nvim

<span class="c"># vim (after switch to nvim, using this config rarely)</span>
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.vimrc ~/.vimrc
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.vim/ ~/.vim/

<span class="c"># Open nvim &amp; vim and install plugins via</span>
:PlugInstall
</code></pre></div></div>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-tmux.png" alt="iterm2-nvim" /></p>

<h3 id="tmux">Tmux</h3>

<p><a href="https://github.com/tmux/tmux/wiki">Tmux</a> - terminal multiplexer and <a href="https://github.com/tmux-plugins/tpm">Tmux Plugin Manager</a>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># link configuration</span>
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.tmux.conf ~/.tmux.conf
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.tmux-osx.conf ~/.tmux-osx.conf

<span class="c"># install tmux-plugin manager</span>
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

<span class="c"># type this in terminal if tmux is already running</span>
tmux <span class="nb">source</span> ~/.tmux.conf
</code></pre></div></div>

<p>Open new session abd type <code class="language-plaintext highlighter-rouge">tmux new -t new</code>. Next, install plugins from the <code class="language-plaintext highlighter-rouge">.tmux.conf</code> file via <kbd>prefix</kbd> + <kbd>I</kbd> (pay attention, it’s big “i”. Prefix = <kbd>ctrl</kbd> + <kbd>b</kbd>).</p>

<p><img src="/assets/posts/setup-macbook-m1/iterm2-tmux.png" alt="iterm2-tmux" /></p>

<h4 id="zsh--oh-my-zsh">ZSH + oh-my-zsh</h4>

<p>I was positively surprised that by default M1 use zsh shell.</p>

<p><img src="/assets/posts/setup-macbook-m1/terminal-default-shell.png" alt="terminal-default-shell" /></p>

<p>Install missing oh-my-zsh</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh <span class="nt">-c</span> <span class="s2">"</span><span class="si">$(</span>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<h5 id="theme">Theme</h5>

<p>For years I use <a href="https://github.com/oskarkrawczyk/honukai-iterm-zsh">honukai</a> theme, as it gives me best orientation in the shell.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.oh-my-zsh/custom/themes/
curl https://raw.githubusercontent.com/oskarkrawczyk/honukai-iterm/master/honukai.zsh-theme <span class="nt">-o</span> ~/.oh-my-zsh/custom/themes/honukai.zsh-theme
</code></pre></div></div>

<p>Reopen terminal to apply changes.</p>

<h3 id="system-preferences">System Preferences</h3>

<h4 id="screenshots">Screenshots</h4>

<p>For all types of screen records, I use default mac screenshot tool, with some combinations from Snagit. The combination of both give the fastest experience to finish a screenshot or record a screen for documentation.</p>

<p>I like to have one path for all type of screen records and usually choose something like:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set new default path</span>
defaults write com.apple.screencapture <span class="nv">$HOME</span>/Documents/Screenrecords

<span class="c"># kill current UI to apply new write path (no worries, it will not destroy anything)</span>
killall SystemUIServer
</code></pre></div></div>

<h4 id="finder">Finder</h4>

<p>Setup list view as a default view for all folders.</p>

<ol>
  <li>Open hard drive view (usually it’s called “Macintosh HD”)</li>
  <li>Press <kbd>⌘</kbd> + <kbd>j</kbd></li>
</ol>

<p><img src="/assets/posts/setup-macbook-m1/finder-harddrive-settings.png" alt="Finder hard drive settings" /></p>

<p>Next after accepting “Use as Defaults”, open terminal and remove all <code class="language-plaintext highlighter-rouge">.DS_Store</code> files from system used by the Finder, in order to remove all overrides (finder save all meta data about folders in .DS_Store).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>find / <span class="nt">-name</span> <span class="s2">".DS_Store"</span>  <span class="nt">-exec</span> <span class="nb">rm</span> <span class="o">{}</span> <span class="se">\;</span>
</code></pre></div></div>

<p>Additionally, I like to <strong>display file extensions</strong> and <strong>sort folder first</strong>, therefore my usually setting for finder window is like following:</p>

<ol>
  <li>Open finder</li>
  <li>Press <kbd>⌘</kbd> + <kbd>,</kbd> to open window “Finder Settings”</li>
</ol>

<p><img src="/assets/posts/setup-macbook-m1/finder-settings.png" alt="Finder settings" /></p>

<h4 id="trackpad">Trackpad</h4>

<p>For the mac trackpad I like to setup 2 things I am so get used to, that I cannot imagine work without:</p>

<p><strong>Swiping between screens with 4 fingers</strong></p>

<p><img src="/assets/posts/setup-macbook-m1/preferences-trackpad-swipe-screens.png" alt="Preferences trackpad swipe screens" /></p>

<p><strong>dragging elements with 3 fingers</strong></p>

<p><img src="/assets/posts/setup-macbook-m1/preferences-accessibility-pointer-control.png" alt="preferences-accessibility-pointer-control" /></p>

<h4 id="displays">Displays</h4>

<p>I think this is pretty standard, although having one screen in vertical position is very helpful as this sometimes enable to look on things from different perspective.</p>

<p><img src="/assets/posts/setup-macbook-m1/preferences-displays.png" alt="preferences-displays" /></p>

<h4 id="desktop--stage-manager">Desktop &amp; Stage Manager</h4>

<p>In new version of macOS Sonoma, they introduce widgets on the desktop. One of the new default
features is when user will click on the background it reveals the desktop.</p>

<p>I prefere to disable this feature and below paste small video-tutorial.</p>

<p><img src="/assets/posts/setup-macbook-m1/preferences-desktop-and-stage-manager-disable.png" alt="disable-stage-manage-on-desktop" /></p>

<h4 id="disable-dictation">Disable dictation</h4>

<p><img src="/assets/posts/setup-macbook-m1/do-you-want-to-enable-dictation.png" alt="do-you-want-to-enable-dictation" /></p>

<div class="alert alert-info">
<p><b>Disclaimer:</b> at the moment of writing macOS Sonoma recently removed the option to complately disable this annoying dictation feature on M1 macbooks.</p>
</div>

<p>The least annoying option I found so far, is to change the current key to the custom mapping that is difficult to click. For example <kbd>Ctrl</kbd> + <kbd>Option</kbd> + <kbd>Shift</kbd> + <kbd>⌘</kbd> + <kbd>&#92;</kbd></p>

<p>This simple soliution is also proposed in <a href="https://apple.stackexchange.com/a/445750/236340">Permanently disable “Enable Dictation” keyboard shortcut in Monterey</a>.</p>

<p><img src="/assets/posts/setup-macbook-m1/preferences-keyboard-dictation.png" alt="preferences-keyboard-dictation" /></p>

<hr />

<h2 id="additional-libs-and-setups">Additional libs and setups</h2>

<h3 id="nvm--yarn">NVM &amp; yarn</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install nvm</span>
curl <span class="nt">-o-</span> https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

<span class="c"># install specific version of node</span>
nvm <span class="nb">install </span>18
nvm <span class="nb">alias </span>default 18

<span class="c"># test</span>
which node
node <span class="nt">--version</span>

<span class="c"># apply changes</span>
<span class="nb">source</span> ~/.zshrc
</code></pre></div></div>

<h4 id="yarn">yarn</h4>

<p>Yarn is connected to version of node running, so best way to install it is via current used node/npm.</p>

<div class="alert alert-info">
<p><b>Info</b>: If you will use many node versions (via <a href="https://github.com/nvm-sh/nvm" target="_blank">nvm</a>), you also should to install yarn for each node version.</p>
</div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install yarn globaly</span>
npm <span class="nb">install</span> <span class="nt">-g</span> yarn

<span class="c"># test</span>
yarn <span class="nt">-v</span>
</code></pre></div></div>

<h3 id="ruby">Ruby</h3>

<p>Follow my other post how to setup ruby on macOS - <a href="/2018/11/10/if-possible-do-not-use-the-ruby-system-version-on-mac-osx.html">If possible do not use the ruby system version on mac osx</a></p>

<p>Standard link for configuration file</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create symlink from dotfiles dotfiles repository</span>
<span class="nb">ln</span> <span class="nt">-sf</span> ~/privatespace/github.com/egel/dotfiles/configuration/.gemrc ~/.gemrc
</code></pre></div></div>

<h3 id="python">Python</h3>

<p>Follow my other post how to setup python on macOS - <a href="/2022/01/30/how-to-properly-set-up-python-project.html">How to properly set up Python project?</a></p>

<h3 id="java">JAVA</h3>

<p>I did not found better way to install Java, like through <a href="https://sdkman.io/">SDK-MAN</a>. I am not much fan of Java but this is really awesome Java Version Manager similar to <code class="language-plaintext highlighter-rouge">rbenv</code>.</p>

<p>To install it start with:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install</span>
curl <span class="nt">-s</span> <span class="s2">"https://get.sdkman.io"</span> | bash<span class="sb">`</span>

<span class="c"># test if succeeded (or reload terminal)</span>
skd version
</code></pre></div></div>

<div class="alert alert-info">
In case you wondering, my configuration for SDK-MAN you can find in my <a href="https://github.com/egel/dotfiles/blob/main/configuration/.zshrc">.zshrc</a>
</div>

<h3 id="apple-silicon---rosetta">Apple Silicon - Rosetta</h3>

<p>Some programs may require installing Apple’s rosetta</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>softwareupdate <span class="nt">--install-rosetta</span>
</code></pre></div></div>]]></content><author><name>Maciej Sypien</name></author><category term="diary" /><category term="macos" /><category term="macbook" /><category term="profession" /><summary type="html"><![CDATA[Setuping mac is definitely not an every day task. It’s usually long process, in order to get an efficient work station. In this article, I want to show you, how I approach to configure M1. I will share the process with the programs and the preferences I use.]]></summary></entry></feed>