Setting Up a Git Server on K3S with Gitea


One of my big goals for my home lab is to set up a completely local deployment pipeline to practice Continuous Delivery in a safe environment. To that end, I set the cluster up with Gitea, an open source git server with support for GitHub Action-like delivery pipelines.

Solving the storage problem by adding NFS support to my NAS

Thus far, my Raspberry Pis in my K3S cluster have been using local storage on their installed SD cards. If I’m hosting a Git server on my cluster, those will quickly fill up, so I need another storage option. Fortunately, I already have network-attached storage available in the form of a TrueNAS server that I run on an old PC. TrueNAS supports connections using NFS - Network File System - which allows the K3S cluster to access folders on the NAS over the network as if they are part of a local file system.

TrueNAS supports NFS by default, but it needs an NFS share defined first. Since I’m using this share as a folder for my Gitea installation, I’ve made a share pointing to a folder called gitea in my NAS.

Screenshot of a TrueNAS Add NFS Share dialogue showing the path -- /mnt/storage/gitea -- to a share for a Gitea install

For now the user is set to root and the usergroup to wheel to avoid permissions issues during setup. Tightening these permissions later will improve security.

Simplifying configuration of cluster machines using Ansible

With the server side of my NFS running as a service on my NAS, I needed to set up the client side on my Raspberry Pi K3S nodes. To do that I had to first install an NFS client on all of my nodes. But this is probably not the only time I would need to log into these nodes and add new functionality on a machine level, and I don’t want to be constantly logging into each node to make manual updates. I can’t guarantee I won’t make a typo or another kind of mistake that gets these nodes out-of-sync, so I want to have some way of editing the configuration once and applying it to every node. Hence I set up infrastructure-as-code (IaC) software that allows me to do just that.

While Chef and Puppet would be good options for a larger, more complex fleet, Ansible is the answer to my IaC needs. No agents need to run on the Pis, the configuration for each machine on the fleet is defined in a text-based playbook file, and it allows me to track the configuration of the K3S nodes in source control.

Setting up SSH key auth

Ansible requires SSH key auth, so I set up key auth on each Pi.

# generate the key
ssh-keygen -t ed25519 -C "ansible"

# copy to each of the nodes
# the name of the user is pi
ssh-copy-id pi@raspberrypi1
ssh-copy-id pi@raspberrypi2
ssh-copy-id pi@raspberrypi3
ssh-copy-id pi@raspberrypi4

Running ansible playbook

I need to keep track of my fleet configuration, so I created a directory named pi-cluster. In that directory I created two files: inventory.ini and setup-nodes.yml.

inventory.ini holds the inventory of all of the hosts in my fleet, giving names to each of the hosts and putting them into groups — k3s_server for the K3S server node, k3s_agents for each of the agent nodes, and k3s_cluster as a group of all of the machines in the cluster. It also sets the ansible_user and ansible_python_interpreter, as it needs a user on each node that has SSH access and it needs the location of the Python interpreter on each node.

[k3s_server]
pi-server ansible_host=192.168.50.X
 
[k3s_agents]
pi-agent1 ansible_host=192.168.50.X
pi-agent2 ansible_host=192.168.50.X
pi-agent3 ansible_host=192.168.50.X
 
[k3s_cluster:children]
k3s_server
k3s_agents
 
[k3s_cluster:vars]
ansible_user=pi
ansible_python_interpreter=/usr/bin/python3

The setup-nodes.yml file is the playbook file containing configuration for each of the nodes in the cluster. I applied the configuration to k3s_cluster so it applies to all of the nodes in the cluster, and I set the NFS server IP and the mount path on the NFS. The playbook updates the package manager cache, installs the required packages for NFS support, enables and starts rpcbind, tests if the NFS mount is reachable, and prints out the result of that test.

---
- name: Configure k3s cluster nodes
  hosts: k3s_cluster
  become: true

  vars:
    nfs_server: "192.168.50.X"
    nfs_mount_path: "/mnt/tank/k3s"

  tasks:
    - name: Update apt cache
      apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install required packages
      apt:
        name:
          - nfs-common        # NFS client for TrueNAS mounts
          - open-iscsi        # iSCSI support (useful to have)
          - curl
          - git
        state: present

    - name: Enable and start rpcbind (required for NFS)
      systemd:
        name: rpcbind
        enabled: true
        state: started

    - name: Test NFS mount is reachable
      command: "showmount -e {{ nfs_server }}"
      register: nfs_exports
      changed_when: false
      failed_when: false

    - name: Show NFS exports from TrueNAS
      debug:
        var: nfs_exports.stdout_lines

Playbooks in Ansible are declarative and idempotent, so they describe the desired end state of the configuration and have the same end result no matter how many times they are run. By keeping my playbooks in source control I have a tracked record of the configuration of my physical fleet.

I tested the connectivity of my cluster by running a ping against the inventory.

ansible k3s_cluster -i inventory.ini -m ping

Ping succeeds so I know I have connectivity.

Screenshot of the return value of an ansible command to ping every node in the inventory

I ran the playbook and verified that each node has access to the NFS mount.

ansible-playbook -i inventory.ini setup-nodes.yml

Installing the NFS StorageClass Provisioner

I used Helm to install the NFS Subdir external provisioner, allowing me to create subdirectories on the NFS share automatically when I create a PersistentVolumeClaim in my K3S cluster.

# Add repo for the provisioner
helm repo add nfs-subdir-external-provisioner \
  https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

# install the provisioner
helm install nfs-provisioner \
  nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace kube-system \
  --set nfs.server=192.168.50.24 \
  --set nfs.path=/mnt/tank/k3s \
  --set storageClass.name=truenas-nfs \
  --set storageClass.defaultClass=true

I confirmed that truenas-nfs is running as a default storage class by running the following command.

kubectl get storageclass

screenshot showing that truenas-nfs is enabled as a default storageclass

I can now store my git repository on my NAS in an NFS share.

Adding a local Git server and Continuous Delivery Pipeline solution using Gitea

I used helm to install Gitea using my NAS for storage. This took a little trial and error, as part of the install is a PostgreSQL cluster with 2 replica pods that initialize from a primary pod. The first time I tried to install Gitea that replication took too long and the liveness probe executed before the replication was finished. That caused some errors (Error code 137) and a persistent CrashLoopBackOff status for the replica pods. I increased the delay before the livenessProbe and the readinessProbe for these pods using a values.yaml file:

postgresql-ha:
  postgresql:
    livenessProbe:
      initialDelaySeconds: 300   # 5 minutes — adjust based on DB size
      timeoutSeconds: 10
      failureThreshold: 6
    readinessProbe:
      initialDelaySeconds: 300   # set this too, keeps traffic away until ready
      timeoutSeconds: 10
      failureThreshold: 6
# Add repo and update
helm repo add gitea-charts https://dl.gitea.com/charts/
helm repo update

# Install gitea and set the admin and persistence settings
helm install gitea gitea-charts/gitea \
  --file values.yaml \
  --namespace gitea \
  --create-namespace \
  --set gitea.admin.username=admin \
  --set gitea.admin.password=yourpassword \
  --set gitea.admin.email=you@example.com \
  --set persistence.storageClass=truenas-nfs \
  --set persistence.size=20Gi

Next step: deploying an app using a continuous delivery pipeline on Gitea

Now that I have Gitea running on my cluster, I have a local Git server with pipeline functionality. In my next post, I’ll take the next step to publishing my first project using a pipeline on Gitea.