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
kubectlworks from the Jenkins server. - Your repo contains:
Search.Api/Search.Api.csprojSearch.Api/Dockerfile- We will add
Search.Api/Jenkinsfile - Your Kubernetes deployment exists (example:
deployment/search-apiin namespacedefault).
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:
reporead:packageswrite: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-stdinis 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