GitLab CI/CD 實作
這篇從自架 GitLab 開始,一路做到用 .gitlab-ci.yml 完成自動化建置、測試與部署。概念的部分可以先看 What is CI/CD?。
本篇同時涵蓋 Docker 與 Podman 兩種容器引擎——Podman 是無 daemon(daemonless)、可免 root(rootless)的容器引擎,CLI 幾乎與 Docker 相同(alias docker=podman 就能通)。指令有差異的地方,我會用分頁對照。
整體流程:架設 GitLab → 建立專案 → 安裝並註冊 Runner → 撰寫 .gitlab-ci.yml → 推 code 觸發 Pipeline。
Docker 與 Podman 的三個關鍵差異,剛好對應這篇的重點:
- 有無 daemon → Podman build image 不需要
docker:dind,直接podman build。 - Runner 的 socket → Podman 提供 Docker 相容 socket,Runner 的 docker executor 可直接接上。
- 自動更新 → Docker 用 Watchtower,Podman 用內建的
podman auto-update。
一、架設 GitLab(self-hosted)
用一個 docker-compose.yml 起 GitLab CE(社群版)。這份設定 Docker / Podman 通用:
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
restart: always
hostname: gitlab.example.com
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://gitlab.example.com'
# 22 埠常被主機的 SSH 佔用,把 GitLab 的 SSH 改到 2222
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '80:80'
- '443:443'
- '2222:22'
volumes:
- ./config:/etc/gitlab # 設定檔
- ./logs:/var/log/gitlab # log
- ./data:/var/opt/gitlab # 資料(repo、DB…)
shm_size: '256m'
這裡要改成你自己的位址:hostname 與 external_url 要填瀏覽器 / git 實際連進來的網址,GitLab 會用它產生 clone URL 等各種連結。
- 內網自用:直接填機器的 LAN IP 最簡單,例如
external_url 'http://10.8.4.xxx'(不依賴 DNS)。 - 主機名 / FQDN:例如
http://gitlab.example.com(機器換 IP 也不用改,但需 DNS 解析得到)。
以下為其他設定的注意事項:
- 埠號要一致:GitLab 內建 nginx 會監聽
external_url裡的埠。80 沒被占用就用預設;若被占用,改成http://<位址>:8929這種,並把ports的對外映射也改成一致的8929:8929。 - 別挑到 8080:GitLab 的 puma 內部預設就聽
8080,若external_url也用 8080 會埠衝突(nginx 先佔了 → puma 綁不到 →Errno::EADDRINUSE→ 一直卡在 502)。- 換別的 port(如
8929) - 如果要用 8080,需要加一行
puma['port'] = 8081讓 puma 改走其他的內部 port
- 換別的 port(如
gitlab_shell_ssh_port是 git-over-SSH 的埠(此處設 2222,對應ports的2222:22)。
只有 rootless Podman 要注意:volumes 改用 named volumes(點開)
上面 ./config、./data 這種主機 bind-mount,在 rootless Podman 下會因 user-namespace 的 UID 對應,讓 reconfigure 在 chown 檔案時噴 Errno::EPERM(Operation not permitted)→ 容器一直 crash 重啟、連不上。把 volumes 改成 Podman 自管的 named volumes 就好(Docker 或 rootful Podman 用原本的 bind-mount 沒問題):
# 把 gitlab 服務的 volumes 改成:
volumes:
- gitlab-config:/etc/gitlab
- gitlab-logs:/var/log/gitlab
- gitlab-data:/var/opt/gitlab
# 並在檔案最外層宣告這些 named volumes:
volumes:
gitlab-config:
gitlab-logs:
gitlab-data:
啟動:
docker compose up -d
pip install podman-compose # 若尚未安裝
podman-compose up -d
docker logs -f gitlab(或 podman logs -f gitlab)觀察,直到 healthy 才連得進去。它也相當吃記憶體,建議主機至少 4GB RAM。只有 rootless Podman 要注意:無法綁 80/443 這種低位埠(點開)
rootless Podman 預設不能綁 1024 以下的埠(如 80/443)。三個做法擇一:
- 改用 rootful:
sudo podman-compose up -d - 把服務對應到高位埠(例如
8080:80) - 放寬系統限制:
sudo sysctl net.ipv4.ip_unprivileged_port_start=80
取得初始 root 密碼
GitLab 會產生一組隨機的 root 密碼,放在容器內(有效 24 小時):
docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
podman exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
首次登入
用 root + 上面那組初始密碼登入 http://gitlab.example.com。
- 第一次登入可能會要你填寫個人資料(set profile,名稱等)——因為你是用
root登入,這是在設定root這個管理員帳號的資料,不是建立新帳號;名稱隨意填(例如Administrator或你的名字)即可。- 慣例上
root只用來做管理,日常開發建議另建一個個人帳號,root保留給後台管理。
- 慣例上
- 進去後立刻改密碼:右上角頭像 → Edit profile → Password(初始密碼檔 24 小時後會被自動清掉)。
二、建立專案並推上程式碼
登入後 New project → Create blank project,建立一個 repo,然後把本地專案推上去:
git remote add origin http://gitlab.example.com/your-name/your-project.git
git branch -M main
git push -u origin main
三、安裝並註冊 GitLab Runner
Pipeline 實際跑在 Runner 上。Runner 一律用 docker executor,差別只在它背後接的是 Docker daemon 還是 Podman 的相容 socket。
Podman 需要先啟用它的 Docker 相容 socket(Docker 本身就有 docker.sock,可略過這步):
# 僅 Podman:啟用 socket,會出現在 /run/podman/podman.sock
sudo systemctl enable --now podman.socket
Runner 容器把對應的 socket 掛進去當作 docker.sock:
gitlab-runner:
image: gitlab/gitlab-runner:latest
container_name: gitlab-runner
restart: always
volumes:
- ./runner-config:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock
gitlab-runner:
image: docker.io/gitlab/gitlab-runner:latest
container_name: gitlab-runner
restart: always
volumes:
- ./runner-config:/etc/gitlab-runner
- /run/podman/podman.sock:/var/run/docker.sock # ← 改指向 Podman
註冊 Runner
到專案的 Settings → CI/CD → Runners,取得註冊用的 URL 與 token,接著:
docker exec -it gitlab-runner gitlab-runner register \
--non-interactive \
--url "http://gitlab.example.com" \
--token "YOUR_RUNNER_TOKEN" \
--executor "docker" \
--docker-image "docker:24" \
--docker-privileged \
--description "docker-runner"
podman exec -it gitlab-runner gitlab-runner register \
--non-interactive \
--url "http://gitlab.example.com" \
--token "YOUR_RUNNER_TOKEN" \
--executor "docker" \
--docker-host "unix:///var/run/docker.sock" \
--docker-image "quay.io/podman/stable" \
--docker-privileged \
--description "podman-runner"
- executor = docker:每個 job 都在一個乾淨的容器裡執行。
- docker-image:job 沒指定
image時的預設映像。 --docker-privileged:等一下要在 job 裡 build image 會用到。
註冊完成後,Runners 頁面會出現一個綠色的 Runner,代表已上線待命。
四、撰寫 .gitlab-ci.yml
在專案根目錄放一個 .gitlab-ci.yml,GitLab 偵測到就會自動驅動 Pipeline。基本結構:
stages: # 定義階段與順序
- build
- test
- deploy
job-name:
stage: build # 屬於哪個階段
image: node:20 # 這個 job 用的容器映像
script: # 實際要執行的指令
- echo "hello CI"
stages:階段依序執行;同一階段的 job 會並行,全部成功才進下一階段。script:job 的主要指令;before_script/after_script可放前置與收尾。rules/only:控制 job 何時執行(例如只在main分支)。artifacts/cache:傳遞 build 產物 / 快取相依套件加速。- 內建變數:
$CI_COMMIT_SHORT_SHA、$CI_COMMIT_BRANCH、$CI_REGISTRY、$CI_REGISTRY_IMAGE… 由 GitLab 自動注入。
五、實作範例:build → test → deploy
以「把服務容器化、推上 registry、再自動部署」為例(這也是我在系上網站採用的流程)。只有 build 這一段 Docker / Podman 不同,test 與 deploy 兩者共用。
建置階段——Docker 需要 docker:dind(Docker-in-Docker);Podman 無 daemon,直接 podman build 即可:
build-image:
stage: build
image: docker:24
services:
- docker:24-dind # Docker-in-Docker
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker build -t "$IMAGE" -t "$CI_REGISTRY_IMAGE:latest" .
- docker push "$IMAGE"
- docker push "$CI_REGISTRY_IMAGE:latest"
build-image:
stage: build
image: quay.io/podman/stable
variables:
STORAGE_DRIVER: vfs # 巢狀環境最穩定,免 dind
script:
- podman login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- podman build -t "$IMAGE" -t "$CI_REGISTRY_IMAGE:latest" .
- podman push "$IMAGE"
- podman push "$CI_REGISTRY_IMAGE:latest"
測試與部署階段(共用):
stages:
- build
- test
- deploy
variables:
IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
# (build-image 見上方,依引擎擇一)
test:
stage: test
image: node:20
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
script:
- npm ci
- npm test
deploy:
stage: deploy
image: alpine:latest
script:
- echo "觸發部署:新版 image 已推上 registry"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
保護 main 分支的部署
實務上,讓 main 的整合只能透過 Merge Request:開發者在自己的分支開發 → 開 MR → 通過 review 才 merge 進 main → 觸發 deploy。這樣可以避免快速開發時把有問題的程式直接部署上線。
六、部署主機的自動更新
CI 把新 image 推上 registry 後,讓部署主機自動拉新版、重啟容器:Docker 生態用 Watchtower,Podman 則用內建的 podman auto-update。
# 部署主機的 docker-compose.yml 加一個 watchtower 服務
services:
watchtower:
image: containrrr/watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 300 # 每 5 分鐘檢查一次
# 容器帶上 auto-update label,並產生 systemd 服務
podman run -d --name app \
--label io.containers.autoupdate=registry \
your-registry/your-app:latest
podman generate systemd --new --files --name app
# 之後(可搭配 podman-auto-update.timer 定期執行)
podman auto-update
deploy 階段其實只要把新 image 推上 registry,剩下的更新交給部署主機自動完成,連 SSH 都免了,交接與維護也更容易——快速開發、便利維護、低成本。小結
| 面向 | Docker | Podman |
|---|---|---|
| 架設 GitLab | docker compose | podman-compose(注意 rootless 埠號) |
| Runner socket | docker.sock | podman.sock(Docker API 相容) |
| Job 內建置 image | docker:dind + docker build | 直接 podman build(免 daemon) |
| 自動更新部署 | Watchtower | podman auto-update + systemd |
Podman 的優勢主要在免 daemon與可 rootless(安全性較佳),且大部分指令與 Docker 相容,遷移成本很低;核心的 GitLab CI/CD 流程兩者完全一致。