Mark Ku's Blog

Lightweight and Free CI/CD for Small Teams Using GitLab Pipeline and a Self-Hosted Runner

The Pain Point

A typical software delivery workflow involves multiple environments:

  1. Development (DEV)
  2. Testing / QA (QAT)
  3. User Acceptance Testing (UAT)
  4. Production (PROD)

As the number of projects grows and environments multiply, manual application deployments become a real headache — and human error can easily cause a deployment to land in the wrong environment. Adopting a CI/CD tool solves this by automating builds and deployments.

What CI/CD Solves

  1. Eliminates conflicts caused by multiple branches or multiple environments.
  2. Reduces manual deployment errors and failures, and shortens recovery time.
  3. Lets developers stay focused on development.
  4. Improves team communication and collaboration efficiency.

What Is CI/CD?

In software engineering, CI/CD (Continuous Integration / Continuous Delivery or Continuous Deployment) bridges the gap between development and operations teams by automating the build, test, and deployment stages of an application. Modern DevOps practice encompasses continuous development, continuous testing, continuous integration, continuous deployment, and continuous monitoring throughout the development lifecycle. (Wikipedia)

My CI/CD Architecture

  1. Host a .NET Core project configured with Docker on GitLab.
  2. Set up a GitLab Runner on a local machine to build and deploy the application.

Planned Deployment Flow

GitLab CI/CD architecture: developer pushes, builds Docker, deploys
GitLab CI/CD architecture: developer pushes, builds Docker, deploys

1. Build Stage

Git Commit > Trigger build stage > Build Docker image using PowerShell

2. Deploy Stage

Manual deploy > Trigger deploy stage > Notify user "Deployment Start" > Deploy Docker image to container server > Notify user "Deploy End"

Let's Get Started

1. Prerequisites

  • Your project must be hosted on GitLab with a Dockerfile configured.
  • Set up a Dockerfile for your project (see the earlier article).

2. Go to the GitLab Admin Panel > Settings > CI/CD

GitLab UI highlighting Settings menu and CI/CD option
GitLab UI highlighting Settings menu and CI/CD option

3. Find Auto DevOps > Expand > Check "Continuous deployment to production" > Save changes

GitLab Auto DevOps continuous deployment to production setting
GitLab Auto DevOps continuous deployment to production setting

4. Scroll down to Runners > Expand > Disable Shared Runners, then copy the GitLab CI URL and token to register your local runner

GitLab runner registration URL, token, and shared runner disable option
GitLab runner registration URL, token, and shared runner disable option

5. Install gitlab-runner

choco install gitlab-runner
gitlab-runner --version  // 測試版本

P.S. Chocolatey usage guide

GitLab CI/CD settings for configuring specific and shared runners
GitLab CI/CD settings for configuring specific and shared runners

6. Register the gitlab-runner

gitlab-runner register
Windows PowerShell registering GitLab runner, showing token and success
Windows PowerShell registering GitLab runner, showing token and success

7. Start the runner

gitlab-runner run

8. After the runner starts, you'll see a new active runner in the GitLab admin panel

GitLab CI/CD Runners page with an active specific runner highlighted
GitLab CI/CD Runners page with an active specific runner highlighted

9. Add a .gitlab-ci.yml file in the project root — GitLab will execute the defined stages when conditions are met

stages:
  - build       
  - deploy
  
build:
  stage: build
  script:    
    - ./build.ps1 # build docker image
  only:
    - main    
    
deploy:
  stage: deploy
  script:
    - ./tg-notify.ps1 "deploy start" # telegram bot 通知部署開始
    - ./linebot-notify.ps1 "deploy start" # line bot 通知部署開始
    - ./deploy.ps1 # deploy docker image to container
    - ./tg-notify.ps1 "deploy end" # telegram bot 通知部署結束
    - ./linebot-notify.ps1 "deploy end" # line bot 通知部署結束
  when: manual    
    
default: 
  interruptible: true # 新的流水線工作建立時,目前的這工作是否繼續執行(預設為 false)
  tags:
  - test

10. Add a PowerShell script for Telegram notifications (tg-notify.ps1) in the project root

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$botToken = 'your token'
$chatID = 'your chatId'
$messageText = $args[0]

$telegramURI = ("https://api.telegram.org/bot" + $botToken + "/sendMessage")
$telegramJson = ConvertTo-Json -Compress @{chat_id = $chatID; text=$messageText}
$telegramResponse = Invoke-RestMethod -Uri $telegramURI -Method Post -ContentType 'application/json;charset=utf-8' -Body $telegramJson

11. Add a PowerShell script for Line Bot notifications (linebot-notify.ps1) in the project root

$token = your token
$toUserId = linebotUserid
$Header = @{
         'Authorization' = 'Bearer $token'
}

$postParams =  @"
{
     "to": "$toUserId",   
     "messages": [
         {           
             "text": "$args",
             "type": "text"
         }
     ]
    
}
"@

Invoke-WebRequest -Uri https://api.line.me/v2/bot/message/push -Method POST -ContentType 'application/json' -Headers $Header -Body $postParams

12. Add a PowerShell script to build the Docker image (build.ps1) in the project root

$containerUrl = "tcp://192.168.50.52:2376"
$imageName = "shopcart"
$dockerfile = "./Comma.Web/Dockerfile"
$outputPath = "./"
$imageFilePath = $outputPath + $imageName
$port="8888:80"

# 本地建置映像檔
docker build -t $imageName . -f $dockerfile

docker save -o $imageFilePath  $imageName # save 要搭配 load ; import 搭配 export

13. Add a PowerShell deployment script (deploy.ps1) in the project root

$containerName = "shopcart-1"
$containerUrl = "tcp://192.168.20.20:2376"
$imageName = "shopcart"
$outputPath = "./"
$imageFilePath = $outputPath + $imageName
$port="8888:80"

# 停用容器
docker --tls -H="$containerUrl" ps -a -f ancestor=$containerName --no-trunc -q | foreach-object { docker --tls -H="$containerUrl" stop $_ }
docker --tls -H="$containerUrl" ps -a -f name=$containerName --no-trunc -q | foreach-object { docker --tls -H="$containerUrl" stop $_ }

# 移除容器
docker --tls -H="$containerUrl" ps -a -f ancestor=$containerName* --no-trunc -q | foreach-object { docker --tls -H="$containerUrl" rm -f $_ }
docker --tls -H="$containerUrl" ps -a -f name=$containerName* --no-trunc -q | foreach-object { docker --tls -H="$containerUrl" rm -f $_ }

# 移除映像檔

$existingImages = docker --tls -H="$containerUrl" images $imageName
If ($existingImages.count -gt 1) {
write-host "[Removing image]Removing the existing image.."
docker --tls -H="$containerUrl" rmi -f $imageName
} else {
write-host "[Removing image]The image does not exist"
}

# 將本地的映像檔匯入 Docker 主機
docker --tls -H="$containerUrl" load --input  $imageFilePath
				
# 建立及啟動容器應用
docker --tls -H="$containerUrl" run -d --name $containerName --restart=always -p $port $imageName				

Screenshots

1. Any git commit to the project triggers an automatic GitLab build

GitLab Pipelines UI showing passed and failed CI/CD runs
GitLab Pipelines UI showing passed and failed CI/CD runs

2. After the build completes, manually triggering a deploy notifies users via Line and Telegram about the deployment status

Messaging app showing automated build and deploy status messages
Messaging app showing automated build and deploy status messages

Wrap Up

GitLab Pipelines is a fantastic tool — and it's completely free. Without standing up a single server, cloud GitLab combined with a local Runner gave me full CI/CD, with Line Bot notifications keeping the team informed whenever a deployment is in progress.

References

Reference Reference Reference Reference Reference

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11