Skip to content

Jenkins CI/CD Guide (Noob‑Friendly, Battle‑Tested)

This file updates the original Jenkins guide with everything we actually did while setting up a working CI/CD flow:

  • GitHub → Jenkins (webhook triggers)
  • Jenkins → .NET build/test
  • Jenkins → Docker build
  • Jenkins → GitHub Container Registry (GHCR) push
  • Jenkins → k3s deploy using kubectl set image
  • Fixing the real problems that commonly block beginners

Architecture overview

flowchart LR
  A[git push to GitHub] -->|Webhook| B[Jenkins Pipeline]
  B --> C[dotnet build + test]
  B --> D[Docker build]
  D --> E[Push image to GHCR]
  B -->|kubectl set image| F[k3s Deployment rollout]
  E --> F
  F --> G[New pod running]

Assumptions

  • Ubuntu server (this guide fits Ubuntu 24.04 “noble”).
  • Jenkins runs on the same server (or at least can reach Docker + k3s API).
  • You have a k3s cluster and kubectl works from the Jenkins server.
  • Your repo contains:
  • Search.Api/Search.Api.csproj
  • Search.Api/Dockerfile
  • We will add Search.Api/Jenkinsfile
  • Your Kubernetes deployment exists (example: deployment/search-api in namespace default).

1) Install Jenkins on Ubuntu

1.1 Install Java

sudo apt-get update
sudo apt-get install -y fontconfig openjdk-17-jre

What these do - apt-get update: refresh package lists. - openjdk-17-jre: installs Java runtime required by Jenkins.

Verify:

java -version

1.2 Install Jenkins

curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key \
 | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null

echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
 https://pkg.jenkins.io/debian-stable binary/ \
 | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null

sudo apt-get update
sudo apt-get install -y jenkins
sudo systemctl enable --now jenkins

Verify:

sudo systemctl status jenkins --no-pager

Open Jenkins: - http://<SERVER_IP>:8080

Unlock password:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Then install Suggested Plugins.


2) Jenkins plugins you must have

Go to Manage Jenkins → Plugins and ensure these are installed:

  • Pipeline
  • Pipeline: Declarative
  • Git
  • GitHub
  • Credentials Binding
  • (Optional but useful) Docker Pipeline

If you see errors like “No such DSL method 'pipeline'” → you’re missing Pipeline plugins.


3) Requirements on the Jenkins server (the steps beginners miss)

3.1 Docker permission for Jenkins

If you see:

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

Fix:

sudo usermod -aG docker jenkins
sudo systemctl restart jenkins

What this does - Adds the jenkins Linux user to the docker group so it can use the Docker socket. - Jenkins must be restarted so the new group membership is applied.

Test:

sudo -u jenkins docker ps

3.2 Install .NET SDK (so dotnet build works)

If you see dotnet: not found from Jenkins, install .NET.

On Ubuntu 24.04, the safest working approach is the Canonical backports PPA:

sudo apt-get update
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:dotnet/backports
sudo apt-get update
sudo apt-get install -y dotnet-sdk-9.0

Verify:

dotnet --version

4) Credentials (most failures come from wrong credential types)

You have two separate authentication needs:

1) SCM (Git clone): Jenkins must clone your GitHub repo. 2) GHCR (docker push): Jenkins must push Docker images to GHCR.

4.1 Create a GitHub token (PAT)

Create a GitHub PAT (classic is simplest) with:

  • repo
  • read:packages
  • write:packages

If write:packages is missing you will get: - permission_denied: create_package

4.2 Create Jenkins credentials

Go to: Manage Jenkins → Credentials → System → Global credentials → Add Credentials

Create these:

A) SCM credentials (for Git checkout)

  • Kind: Username with password
  • ID: github-scm
  • Username: your GitHub username
  • Password: your PAT

Why this matters: - The “SCM credentials dropdown” expects Username/Password (or SSH key). - A “Secret text” token typically will NOT appear there.

B) GHCR username (for docker login)

  • Kind: Username with password
  • ID: ghcr-user
  • Username: your GitHub username
  • Password: your PAT

C) GHCR token (secret text)

  • Kind: Secret text
  • ID: ghcr-token
  • Secret: your PAT

5) kubeconfig credential (the safe way we used)

5.1 Get kubeconfig from the server to your PC

k3s kubeconfig is often here:

  • /etc/rancher/k3s/k3s.yaml

Copy it to your user folder and make it readable:

sudo cp /etc/rancher/k3s/k3s.yaml /home/<USER>/kubeconfig.yaml
sudo chown <USER>:<USER> /home/<USER>/kubeconfig.yaml
chmod 600 /home/<USER>/kubeconfig.yaml

Download it to your PC (example):

scp <USER>@<SERVER_IP>:/home/<USER>/kubeconfig.yaml .

5.2 Add kubeconfig to Jenkins as “Secret file”

Go to credentials and add:

  • Kind: Secret file
  • ID: kubeconfig
  • Upload: kubeconfig.yaml

Important: - Secret file is required. Secret text causes “file name too long” / “cannot stat”.


6) Create the Jenkins job (Pipeline from SCM)

6.1 Create the job

Jenkins → New Item - Name: search-api - Type: Pipeline

6.2 Configure job

In job → Configure:

  • Definition: Pipeline script from SCM
  • SCM: Git
  • Repo URL: your repository
  • Credentials: github-scm
  • Branch: main
  • Script Path: Search.Api/Jenkinsfile

If you see: - fatal: couldn't find remote ref refs/heads/master
→ you used master but your branch is main.

If you see: - Unable to find Jenkinsfile
→ Script Path is wrong.


7) Enable webhook triggers (no more manual “Build Now”)

7.1 Enable trigger in Jenkins job

Job → Configure → Build Triggers: - ✅ GitHub hook trigger for GITScm polling

Save.

7.2 Add webhook in GitHub

GitHub repo → Settings → Webhooks → Add webhook

  • Payload URL: http(s)://<JENKINS_HOST>/github-webhook/
  • Content type: application/json
  • Events: Just the push event

Then open the webhook → Recent deliveries → confirm 200.


8) Final working Jenkinsfile (the one that solved all our issues)

Create file:

Search.Api/Jenkinsfile

pipeline {
  agent any

  environment {
    // GHCR registry owner (your GitHub username or org)
    REGISTRY = "ghcr.io/rotem-blik"

    // image name in GHCR
    IMAGE = "search-api"

    // Jenkins credentials
    GHCR_USERNAME = credentials('ghcr-user')
    GHCR_TOKEN = credentials('ghcr-token')

    // Git commit SHA
    GIT_SHA = "${env.GIT_COMMIT}"
  }

  stages {

    stage('Checkout') {
      steps {
        deleteDir()      // wipes workspace safely as Jenkins user
        checkout scm
      }
    }

    stage('Build') {
      steps {
        sh 'dotnet build Search.Api/Search.Api.csproj -c Release'
      }
    }

    stage('Test') {
      steps {
        sh 'dotnet test Search.Api/Search.Api.csproj -c Release --no-build'
      }
    }

    stage('Docker Build & Push') {
      steps {
        sh '''
          docker build \
            -t $REGISTRY/$IMAGE:$GIT_COMMIT \
            -f Search.Api/Dockerfile Search.Api

          echo "$GHCR_TOKEN" | docker login ghcr.io \
            -u "$GHCR_USERNAME" --password-stdin

          docker push $REGISTRY/$IMAGE:$GIT_COMMIT
        '''
      }
    }

    stage('Deploy to k3s') {
      steps {
        withCredentials([file(credentialsId: 'kubeconfig', variable: 'KCFG')]) {
          sh '''
            # Put kubeconfig in the workspace (always writable)
            KCFG_TMP="$(mktemp)"
            cp "$KCFG" "$KCFG_TMP"
            export KUBECONFIG="$KCFG_TMP"

            # Deploy the exact image tag Jenkins built (GIT_COMMIT)
            kubectl -n default set image deployment/search-api \
              search-api=$REGISTRY/$IMAGE:$GIT_COMMIT

            # Wait for the new pod to be ready
            kubectl -n default rollout status deployment/search-api

            rm -f \"$KCFG_TMP\"
          '''
        }
      }
    }
  }
}

9) What each important command means (noob explanation)

docker build

docker build -t ghcr.io/USER/search-api:<TAG> -f Search.Api/Dockerfile Search.Api
  • -t: name + tag of the image.
  • -f: path to Dockerfile.
  • Search.Api: the build context (the folder sent to Docker).
    If you use the wrong folder, Docker won’t find files.

docker login

echo "$TOKEN" | docker login ghcr.io -u "$USER" --password-stdin
  • Logs Docker into GHCR.
  • --password-stdin is safer than -p.

docker push

docker push ghcr.io/USER/search-api:<TAG>
  • Uploads the image to GHCR.

kubectl set image

kubectl -n default set image deployment/search-api search-api=ghcr.io/USER/search-api:<TAG>
  • Updates the Deployment’s container to a new image.
  • deployment/search-api: name of Kubernetes Deployment.
  • search-api=: container name inside the deployment (must match your YAML).
  • This triggers a rolling update.

kubectl rollout status

kubectl -n default rollout status deployment/search-api
  • Waits until Kubernetes finishes deploying the new pod.

KUBECONFIG in workspace (the key fix)

We avoided writing to /var/lib/jenkins/.kube and used:

  • $WORKSPACE/kubeconfig (writable by Jenkins)

This solved permission issues like: - “Permission denied” when copying kubeconfig.


10) Troubleshooting (the real problems we fixed)

A) No such DSL method 'pipeline'

Cause: Pipeline plugins missing.
Fix: install Pipeline + Pipeline: Declarative.

B) dotnet: not found

Cause: .NET SDK missing on Jenkins server.
Fix: install dotnet-sdk-9.0 (see section 3.2).

C) Docker socket permission denied

Fix:

sudo usermod -aG docker jenkins
sudo systemctl restart jenkins

D) permission_denied: create_package

Cause: token missing write:packages OR wrong GHCR path.
Fix: correct PAT scopes + use ghcr.io/<your-username>.

E) kubeconfig “file name too long” / “cannot stat”

Cause: kubeconfig stored as secret text, treated like a path.
Fix: store kubeconfig as Secret file and use withCredentials([file(...)]).

F) Workspace permission denied after enabling webhook

Sometimes workspace ends up owned by a wrong user. Fix:

sudo chown -R jenkins:jenkins /var/lib/jenkins/workspace/search-api
sudo chmod -R u+rwX /var/lib/jenkins/workspace/search-api

Final result

sequenceDiagram
  participant Dev as Developer
  participant GH as GitHub
  participant J as Jenkins
  participant R as GHCR
  participant K as k3s

  Dev->>GH: git push
  GH->>J: webhook (/github-webhook/)
  J->>J: build + test
  J->>R: docker push image:<git sha>
  J->>K: kubectl set image + rollout
  K-->>Dev: new version running