Is your precious code safe? An easy way to ensure availability of code/repositories.

My primary homelab repository responsible for documenting all of my infrastucture.

Recently I started learning the practice of GitOps and IAC (Infrastructure as Code), CI/CD (Continuous Integration & Continuous Developement); applying this practice to my homelab in the end was far easier than I ever anticipated. However there is still a cost that it can be tedious and take time to translate the running state/intended state of my homelab to ansible/bash script that can be run as part of a build.

Just for those of you who aren't familiar with GitOps pipelines. The way this works is I have a .gitlab-ci.yml file, an "inventory" file and in my repo I have various configs that I have written ansible and bash scripts to apply. The gitlab-ci file contains a direction of what container image to use to process the bash commands in the script area and then every time the configuration of my lab changes, it runs this script against the inventory file I supply and spiders out into all the code I have written/imported into my WIP pipeline over time.

My current .gitlab-ci.yml for purpose of example. (Note: In the 2 rsync tasks I am cloning the file-system; this is how I was backing up the repo's before setting up this mirror. Sharing all this stuff to try and be comprehensive.)
My ansible "inventory" file just so you can see what that looks like, in case you are curious about gitlab CI/CD...
This is my init.yaml. I designated this script as the entrypoint for my ansible automations, it is a list of playbooks in the order I'd like them executed! 😁 This isn't best practice, Idek what that would be. This is just a demo of how I have GitOps working for me.
This is a sample of a ansible task that I execute during my pipeline from the init.yaml

As stated above, this describes how I am primarily using gitlab. My intent is to translate the full active state of my environment to code, if I accomplish this, I could push my config for all these deployments if I wanted/needed to reset my lab all of my deployments would be configured without backups or any of the data from the code.. There's so many other things we might accomplish via this practice besides that, like key/pass rotation or more performant backups that only copy data and no binaries. I have plans to push all of the scripts I have written my entire life to this platform now, just because it is a really easy and intuitive tool to work with! Especially when you start getting the hang of these things. Well when we start to rely on a platform as our #1 go to for code and this code has time and value put into it, I start to worry of the reliability of the platform and want to consider the case that something happens to my Gitlab instance... Will I still be able to access my SUPER important IAC code or will I be fighting to recover that? In asking myself these questions I decided to take steps to ensure the outcome in this hypothetical situation. I added to my Gitlab pipeline some bash lines to copy the important repository to 2 other hosts and also setup a mirror of the most important repositories using gitea (a super lightweight application that has most of the functionality of github). Now I have peace of mind that my code is safe and for once I feel beyond solid that my work is safe! You can even mirror your code to github if you want.

This post is to serve as a simple FYI to anyone who isn't aware that we can do this! 🙏 I wish I thought of this a long time ago! I lost quite a bit of work in my day 🤣

Here is the deployment I used for gitea ⬇️ (It's super easy to deploy!)

To install docker and docker compose:

https://docs.docker.com/engine/install/

https://docs.docker.com/compose/install/linux/

Put it in somewhere like:

sudo nano /opt/docker/gitea/docker-compose.yaml

Insert this text then edit if desired Ctrl+x, then "y" to save and "enter" to exit.

services:
  gitea-server:
    depends_on:
      - gitea-postgres
    environment:
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=gitea-postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=$POSTGRES_PASS
    image: docker.io/gitea/gitea:1.23.1-rootless
    ports:
      - "7843:3000" # This is the Web-UI, change 7843 accordingly.
      - "2222:2222" # This is the ssh port. Change it accordingly.
    restart: always
    userns_mode: keep-id:uid=1000,gid=1000
    volumes:
      - /opt/docker/gitea/data:/var/lib/gitea
      - /opt/docker/gitea/config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    # logging: &logging
      # driver: "loki"
      # options:
        # loki-batch-size: "400"
        # loki-url: "http://monitoring:3100/loki/api/v1/push"
        # max-size: "10m"
        # max-file: "3"
        # mode: "non-blocking"
        # loki-retries: "2"
        # loki-max-backoff: "800ms"
        # loki-timeout: "1s"
 
  gitea-postgres:
    #labels: *logging
    container_name: gitea-postgres
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=$POSTGRES_PASS
      - POSTGRES_DB=gitea
    image: docker.io/library/postgres:14
    restart: always
    volumes:
      - /opt/docker/gitea/postgres:/var/lib/postgresql/data

Next create the .env and set a postgres password:

sudo nano /opt/docker/gitea/.env

Insert this text then edit if desired Ctrl+x, then "y" to save and "enter" to exit.

POSTGRES_PASS=<a password string>

GITEA__database__PASSWD=$POSTGRES_PASS
POSTGRES_PASSWORD=$POSTGRES_PASS

Now we can initiate the pull sequence to activate our gitea deployment:

cd /opt/docker/gitea
docker compose up -d

If you used my deployment without changing the ports gitea will now be available at the host you deployed it to via http://<ip/hostname>:7843

Now it is super easy to make a mirror in gitea:

Step one: create a new repository.
Step two: click the migrate repository button.
Step three: Select the platform the repo you are planning to mirror is utilizing.
Step four: Enter the URL of whatever repo you will be mirroring. (You have additional options, syncing LFS files and even syncing a Wiki that isn't part of the codebase is supported!

Make sure to add the access token if the repo you are mirroring is not public or has portions that are not publicly available that you wish to be retained in the mirror.

Wait for it to copy down the repo to your machine and make it local! 😁
We have a local mirror of the repository we cloned in the above steps now!
On the slide before if you hit the settings button, we can set a custom sync interval; note it defaults to every 8 hours 😲 This is up to you!

Resources:

This is the best guide I have found on how Gitlab CI/CD works. It is most of the motivation I had to start working on my IAC stuff. It has been going splendid. I recently worked out how I am going to automate Windows, which is very nice!

This is the latest state of the Dockerfile that have edited to build my ansible Docker container image. This image is used to run my main ansible workflow that is the heart of my CI/CD pipeline. I struggled figuring out how to run winrm inside a docker container and really wanted to automate Windows machines, so I preservered. Just a hint here, in case you go/are going that route... I did not find a really easy guide on how to set this up anywhere. I put it together through much search and investigation.

FROM python:3.13.5-alpine3.22
WORKDIR /app
ADD . /app
ENV ANSIBLE_HOST_KEY_CHECKING 'false'
RUN apk add --no-cache openssh-client rsync
RUN pip3 install --upgrade pip
RUN pip3 install requests ansible-core==2.18.6 ansible-lint==25.6.1 pywinrm
RUN ansible-galaxy collection install ansible.posix
RUN ansible-galaxy collection install ansible.windows
RUN ansible-galaxy collection install community.docker
CMD [ "ansible-playbook", "--version" ]

Here is my gitlab deployment "docker-compose.yaml just for reference, I won't be giving a throughrough walkthrough but I thought I'd share since I already dumped a lot of other git info!


services:
    gitlab:
        image: 'gitlab/gitlab-ce:latest'
        container_name: gitlab
        restart: always
        hostname: 'git.domain.com'
        environment:
            GITLAB_OMNIBUS_CONFIG: |
                external_url 'https://git.domain.com'
                gitlab_rails['gitlab_shell_ssh_port'] = 22
                letsencrypt['enabled'] = false
                nginx['enable'] = true
                nginx['listen_port'] = 8017
                nginx['listen_https'] = false
        ports:
            - '8017:8017'
            - '2424:22'
        volumes:
            - '/opt/docker/gitlab/config:/etc/gitlab'
            - '/opt/docker/gitlab/logs:/var/log/gitlab'
            - '/opt/docker/gitlab/data:/var/opt/gitlab'
        secrets:
          - gitlab_root_password
    # logging: &logging
      # driver: "loki"
      # options:
        # loki-batch-size: "400"
        # loki-url: "http://monitoring:3100/loki/api/v1/push"
        # max-size: "10m"
        # max-file: "3"
        # mode: "non-blocking"
        # loki-retries: "2"
        # loki-max-backoff: "800ms"
        # loki-timeout: "1s"
secrets:
  gitlab_root_password:
    file: /opt/docker/gitlab/root_password.txt

As always, I hope that you found something in this walk-thru useful or interesting. Let me know if there is anything you are wanting to explore further or if I left you with any additional questions! Otherwise I will be signing off now and wish you an amazing day today! <3

Warm regards,

SoFMeRight