Running GitLab Locally in Kubernetes
Photo by unsplash


In a previous post, I discussed running Kubernetes locally on my Mac M1 using tools like orbstack and kind, complete with local DNS resolution.


My primary challenge was running GitLab locally. While several methods exist, such as using docker with docker-compose, these approaches often led to startup issues and failed to work reliably.

I then explored using the GitLab Helm chart, with comprehensive documentation available here. My goals included achieving local DNS resolution and correctly handling self-signed certificates. In this case orbstack proved to be highly effective.

Solution 1

The solution leverages the tool orbstack

Initial Setup

Orbstack simplifies the process by making the domain k8s.orb.local available on your local DNS on a Mac. This allows us to deploy GitLab using the Helm chart with the correct configuration right from the start.

Here is the example code. I am using the values-minikube-minimum.yaml, which has been patched to work with orbstack.

kubectl config use-context orbstack

kubectl create ns gitlab
helm repo add gitlab
helm repo update

# as the chart version is mapped here to the app/GitLab version
helm upgrade --install gitlab gitlab/gitlab \
 --timeout 120s \
 --namespace gitlab \
 --version 7.11.2 \
 -f \
 --set global.hosts.domain="k8s.orb.local" \
 --set global.hosts.externalIP="" \
 --set nginx-ingress.enabled=true

# wait until the webservice is up
kubectl -n gitlab rollout status --timeout 2m deploy gitlab-webservice-default
# test without valid cert chain: -k flag
curl -fsSkIL gitlab.k8s.orb.local

In the second step, I store the Certificate chain (includes the CA), as I need it for the Mac Keychain to see the Root CA as valid:

# crt with chain
kubectl get secret -n gitlab gitlab-wildcard-tls-chain -o jsonpath={'.data.gitlab\.k8s\.orb\.local\.crt'} | base64 --decode > gitlab.k8s.orb.local.crt
# add it to the Mac keychain
security add-trusted-cert -d -r trustRoot -k $HOME/Library/Keychains/login.keychain-db gitlab.k8s.orb.local.crt

And it shoud look like this and trusted gitlab-keychain

Now test the access again, but with a valid keychain

# Note: no -k
curl -fsSIL gitlab.k8s.orb.local

If you get the following error on Mac curl: (35) OpenSSL/3.0.13: error:16000069:STORE routines::unregistered scheme, it is because of the version of curl. For me it was:

curl --version

curl 8.7.1 (aarch64-apple-darwin23.5.0) libcurl/8.7.1 OpenSSL/3.0.13 zlib/1.3.1 brotli/1.1.0 zstd/1.5.6 libidn2/2.3.7 libpsl/0.21.5 libssh2/1.11.0 nghttp2/1.61.0
Release-Date: 2024-03-27

I fixed it by running the command from a docker container:

docker run -it --network host -v $(pwd):/build alpine:3 sh
apk update && apk add curl
curl --version
cp /build/gitlab.k8s.orb.local.crt  /usr/local/share/ca-certificates/
WARNING: ca-cert-gitlab.k8s.orb.local.pem does not contain exactly one certificate or CRL: skipping
# this message can be ignored. CRT is loaded neverthelesse
# see

curl -IL gitlab.k8s.orb.local

In action, it looks like this.


Now open your browser on https://gitlab.k8s.orb.local and login with user root and password, which you retrieve as follows 🎉

kubectl get secret -n gitlab gitlab-gitlab-initial-root-password -ojsonpath='{.data.password}' | base64 --decode ; echo

The login screen looks as follows:


Now that I have GitLab running and are logged in as root, let’s create a Personal access token (PAT) gitlab-pat

Then you can test the access:

# Note: no -k flag, as the crt is in our mac keychain
export GITLAB_TOKEN=<token>
curl -IL -H "PRIVATE-TOKEN: $GITLAB_TOKEN" https://gitlab.k8s.orb.local/api/v4/version | jq 

will give the details

  "version": "16.11.2-ee",
  "revision": "d210b947e3e",
  "kas": {
    "enabled": true,
    "externalUrl": "wss://kas.k8s.orb.local",
    "version": "16.11.2"
  "enterprise": true

Docker-in-Docker (DinD) jobs

Download the file

and then I adapt it as follows. But note to adapt the path of your docker socket. You can find this out via

docker context ls

NAME         DESCRIPTION                               DOCKER ENDPOINT                
default      Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
orbstack *   OrbStack                                  unix:///Users/<user>/.orbstack/run/docker.sock

and replace this in the following path gitlab-runner.runner.config then in path = "...".

See now the full example:

# values-minikube.yaml
# This example intended as baseline to use Minikube for the deployment of GitLab
# - Minimized CPU/Memory load, can fit into 3 CPU, 6 GB of RAM (barely)
# - Services that are not compatible with how Minikube runs are disabled
# - Some services entirely removed, or scaled down to 1 replica.
# - Configured to use, and for the domain

# Minimal settings
    configureCertmanager: false
    class: "nginx"
    domain: k8s.orb.local # <== Change here
    # No external IP
    # externalIP: # <== Change here
  # Disable Rails bootsnap cache (temporary)
      enabled: false
    # Configure the clone link in the UI to include the high-numbered NodePort
    # value from below (`gitlab.gitlab-shell.service.nodePort`)
    port: 32022
# Don't use certmanager, we'll self-sign
  install: false
# Use the `ingress` addon, not our Ingress (can't map 22/80/443)
  enabled: true # <== Change here
# Save resources, only 3 CPU
  install: false
gitlab-runner: # <== Change here the whole block
  install: true 
  # "gitlab-wildcard-tls-chain" assumes your release name is "gitlab". If it is set to something else,
  #   replace "gitlab" below with your own release name.
  certsSecretName: gitlab-wildcard-tls-chain
    privileged: true
    # for dind
    # fix
    config: |
              name = "docker-socket"
              path = "/Users/mavogel/.orbstack/run/docker.sock"
              mount_path = "/var/run/docker.sock"
              read_only = false      
# Reduce replica counts, reducing CPU & memory requirements
    minReplicas: 1
    maxReplicas: 1
    minReplicas: 1
    maxReplicas: 1
    minReplicas: 1
    maxReplicas: 1
    # Map gitlab-shell to a high-numbered NodePort to support cloning over SSH since
    # Minikube takes port 22. However on orbstack, we could use port 22, but we leave it
      type: NodePort
      nodePort: 32022
    minReplicas: 1
    maxReplicas: 1

As a next step, I will add runners with dind, so I can make use of the docker socket 💡 within the GitLab runner.

helm upgrade --install gitlab gitlab/gitlab \
 --timeout 120s \
 --namespace gitlab \
 --version 7.11.2 \
 -f adapted-values-minikube-minimum.yaml

You can now see a gitlab-gitlab-runner-* pod running. The content of the lower split are the logs of that pod and how it successfully registers with the Gitlab instance 🎉


Now, of course, we want to test this, and the best way is to create an example project with the following .gitlab-ci.yml:

  image: docker:24.0.5
    # I use the DOCKER_HOST default value as I mount the socket
    # and connect insecurely
 - printenv | grep DOCKER | sort
 - docker version

If we now run this pipeline, we see the following output that confirms that the docker cli client in the container could successfully connect to the underlying server/daemon we mounted into the runner 🎉



Of course, we want to tear everything down after our tests, to free the resources. You can achieve this as follows:

helm uninstall gitlab -n gitlab --wait
# watch teardown
watch kubectl get po -A
kubectl delete ns gitlab

At the next rollout of the helm chart you will still have the data for Gitlab, because it is stored in a pv.

If you want to fully clean your Gitlab data, remove the pvc's as well:

kubectl get pvc -n gitlab
# remove them
kubectl delete pvc -n gitlab -l release=gitlab
kubectl delete pvc -n gitlab -l

Now everything is clean 🗑️

Solution 2

If you cannot or are not allowed to use orbstack due to company rules, you can also use kind. See also the official Gitlab docs for more details, as it is also the recommended way to develop its helm chart.

Ok, let’s start it off.

Start kind with the following config kind-gitlab-ssl.yaml

kind: Cluster
- role: control-plane
  # to be able to run dind in the GitLab pipelines 
  # see
  - hostPath: /var/run/docker.sock # <-- or use the orbstack socket
    containerPath: /var/run/docker.sock
    # containerPort below must match the values file:
    #   nginx-ingress.controller.service.nodePorts.https
    # Change hostPort if port 443 is already in use.
  - containerPort: 32443
    hostPort: 443
    listenAddress: ""
    # containerPort below must match the values file:
    #   nginx-ingress.controller.service.nodePorts.ssh
    # Using high-numbered hostPort assuming port 22 is
    #   already in use.
  - containerPort: 32022
    hostPort: 32022
    listenAddress: ""

with the command:

kind create cluster \
 --name gitlab \
 --image kindest/node:v1.29.2 \
 --config kind-gitlab-ssl.yaml \
 --wait 1m

When the cluster is up and running, you can use the example files from the Gitlab helm repository:

# get your ip:
## mac: ipconfig getifaddr en0
## linux: hostname -i
export MY_IP=$(ipconfig getifaddr en0)

# switch the k8s context
kubectl config use-context kind-gitlab

# install Gitlab
helm upgrade --install gitlab gitlab/gitlab \
 --timeout 120s \
 --namespace gitlab \
 --set global.hosts.domain=$ \
 --set global.hosts.externalIP="" \
 -f \
 -f \
 --set nginx-ingress.enabled=true

The teardown is simpler here, as we can delete the whole cluster:

kind delete cluster --name gitlab


In this blog post, I shared my exploration of how to set up GitLab locally to evolve pipelines and its jobs by using orbstack or kind and the corresponding helm chart.

Enjoy ❤️

Like what you read? You can hire me, or drop me a message to see which services 💻 may help you 👇