راهاندازی CI/CD با GitLab و Docker، قدمبهقدم
اگر هنوز برای هر استقرار به سرور SSH میزنید، git pull میکنید و دستی Build میگیرید، هر استقرار یک ریسک است: یک مرحله فراموش میشود، روی سرور چیزی متفاوت با مخزن کد اجرا میشود و برگشت به نسخهی قبل سخت است. در این آموزش یک Pipeline کامل با GitLab CI/CD و Docker میسازیم که با هر Push روی شاخهی اصلی، ایمیج را میسازد، تست میکند، در Container Registry گیتلب قرار میدهد و روی سرور Production با Docker Compose مستقر میکند.
یک نکتهی مهم برای تیمهای ایرانی
دسترسی به gitlab.com از ایران ممکن است به دلیل تحریمها محدود یا مسدود باشد و حسابهایی که از IP ایران استفاده میکنند در معرض مسدود شدن هستند. به همین دلیل بسیاری از تیمها GitLab را روی سرور خودشان (Self-managed) نصب میکنند. همهی مراحل این آموزش برای GitLab Self-managed نوشته شده و روی gitlab.com هم با تغییرات کوچک کار میکند. در نسخهی Self-managed، Container Registry باید با تنظیم registry_external_url در فایل /etc/gitlab/gitlab.rb فعال شده باشد.
معماری
- سرور GitLab: مخزن کد، Pipeline و Container Registry.
- سرور Runner: اجرای Jobها. بهتر است جدا از سرور GitLab و سرور Production باشد.
- سرور Production: فقط Docker و Docker Compose دارد و ایمیجهای آماده را از Registry دریافت میکند. هیچ Buildی روی آن انجام نمیشود.
قدم ۱: نصب GitLab Runner
روی سرور Runner (Ubuntu 24.04) ابتدا Docker را نصب کنید، سپس مخزن رسمی GitLab Runner را اضافه کنید. مستندات GitLab توصیه میکند اسکریپت را قبل از اجرا بخوانید:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" -o script.deb.sh
less script.deb.sh
sudo bash script.deb.sh
sudo apt install gitlab-runner
gitlab-runner --version
قدم ۲: ساخت و ثبت Runner با توکن احراز هویت
روش ثبت Runner در نسخههای اخیر GitLab تغییر کرده است. روش قدیمی، یعنی Registration Token که در صفحهی تنظیمات نمایش داده میشد، منسوخ (Deprecated) شده و از GitLab 17.0 به بعد مدیر سرور میتواند آن را غیرفعال کند. در این صورت ثبت با روش قدیمی با خطای 410 Gone شکست میخورد. روش جدید این است:
- در پروژه به مسیر Settings ← CI/CD ← Runners بروید و New project runner را بزنید. (برای Runner مشترک بین پروژهها، همین کار را در سطح گروه یا در Admin area انجام دهید.)
- تگها (مثلاً
docker) و سایر تنظیمات را همینجا در رابط وب تعیین کنید و Runner را بسازید. - GitLab یک Runner Authentication Token با پیشوند
glrt-نمایش میدهد. آن را کپی کنید.
روی سرور Runner:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.example.com" \
--token "glrt-xxxxxxxxxxxxxxxxxxxx" \
--executor "docker" \
--docker-image "alpine:3.20" \
--description "docker-runner-1"
توجه کنید که در روش جدید تگها، قفل بودن و سطح دسترسی را نمیتوان از خط فرمان تعیین کرد و این تنظیمات در رابط وب انجام میشود. جزئیات دقیق را برای نسخهی GitLab خودتان در مستندات رسمی بررسی کنید.
برای ساخت ایمیج Docker داخل Job، از سرویس Docker-in-Docker استفاده میکنیم که به حالت privileged نیاز دارد. فایل /etc/gitlab-runner/config.toml را باز کنید و در بخش [runners.docker] این مقادیر را تنظیم کنید:
[runners.docker]
image = "alpine:3.20"
privileged = true
volumes = ["/certs/client", "/cache"]
sudo gitlab-runner restart
حالت privileged یعنی Jobها دسترسی گستردهای به میزبان دارند؛ پس این Runner را فقط به پروژههای مورد اعتماد اختصاص دهید و روی سرور جداگانه اجرا کنید. اگر Runner در ایران است و ایمیجهای docker و alpine از Docker Hub دریافت نمیشوند، راهحلها را در دسترسی به Docker Hub از ایران ببینید.
قدم ۳: آمادهسازی سرور Production
روی سرور Production یک کاربر برای استقرار بسازید که عضو گروه docker باشد، و یک کلید SSH مخصوص Pipeline تولید کنید:
sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG docker deploy
sudo mkdir -p /opt/myapp && sudo chown deploy:deploy /opt/myapp
# روی سیستم خودتان:
ssh-keygen -t ed25519 -f deploy_key -N "" -C "gitlab-ci"
# محتوای deploy_key.pub را به /home/deploy/.ssh/authorized_keys روی سرور اضافه کنید
یادتان باشد که عضویت در گروه docker عملاً معادل دسترسی root است؛ این کلید را فقط برای همین منظور استفاده کنید. فایل docker-compose.yml و .env را در /opt/myapp قرار دهید. در فایل Compose، ایمیج را با یک متغیر مشخص کنید:
services:
app:
image: ${APP_IMAGE}
env_file: .env
restart: unless-stopped
برای اینکه سرور بتواند از Registry دریافت کند، در پروژه از مسیر Settings ← Repository ← Deploy tokens یک توکن با دسترسی read_registry بسازید و یک بار روی سرور وارد شوید:
sudo -u deploy docker login registry.example.com -u <deploy-token-username>
قدم ۴: متغیرهای CI/CD و شاخههای محافظتشده
در مسیر Settings ← CI/CD ← Variables این متغیرها را اضافه کنید:
| نام | نوع | تنظیمات | مقدار |
|---|---|---|---|
SSH_PRIVATE_KEY |
File | Protected | محتوای deploy_key |
SSH_KNOWN_HOSTS |
Variable | Protected | خروجی ssh-keyscan prod.example.com |
DEPLOY_HOST |
Variable | Protected | deploy@prod.example.com |
متغیرهای Protected فقط در Pipelineهای شاخهها و تگهای محافظتشده در دسترساند. شاخهی main را در Settings ← Repository ← Protected branches محافظت کنید تا فقط افراد مجاز بتوانند روی آن Push یا Merge کنند. به این ترتیب کسی نمیتواند با ساختن یک شاخهی دلخواه، کلید SSH سرور Production را بخواند. متغیرهای Registry مثل CI_REGISTRY، CI_REGISTRY_IMAGE، CI_REGISTRY_USER و CI_REGISTRY_PASSWORD را GitLab خودش در هر Job تعریف میکند.
قدم ۵: فایل .gitlab-ci.yml
این فایل را در ریشهی مخزن قرار دهید. مثال برای یک پروژهی Node.js است؛ مرحلهی تست را با دستورات پروژهی خودتان عوض کنید. نسخهی ایمیج docker و docker:dind را با نسخهی Docker فعلی هماهنگ و یکسان نگه دارید.
stages:
- test
- build
- deploy
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_TLS_CERTDIR: "/certs"
test:
stage: test
image: node:24-alpine
cache:
key:
files:
- package-lock.json
paths:
- .npm/
script:
- npm ci --cache .npm --prefer-offline
- npm test
build:
stage: build
image: docker:28
services:
- docker:28-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build
--cache-from $CI_REGISTRY_IMAGE:latest
--build-arg BUILDKIT_INLINE_CACHE=1
-t $IMAGE_TAG -t $CI_REGISTRY_IMAGE:latest .
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy:
stage: deploy
image: alpine:3.20
environment:
name: production
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 600 "$SSH_PRIVATE_KEY"
script:
- ssh -i "$SSH_PRIVATE_KEY" $DEPLOY_HOST
"cd /opt/myapp && export APP_IMAGE=$IMAGE_TAG && docker compose pull app && docker compose up -d app && docker image prune -f"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
توضیح بخشهای مهم:
- تگ با شناسهی Commit: هر استقرار یک ایمیج یکتا دارد. برای برگشت به نسخهی قبل، کافی است Job استقرار یک Pipeline قدیمی را از صفحهی Environments دوباره اجرا کنید.
- cache در Job تست: پوشهی کش npm بر اساس محتوای
package-lock.jsonنگهداری میشود و تا وقتی وابستگیها تغییر نکنند، نصب سریعتر انجام میشود. برای pip و Composer هم همین الگو را با مسیر کش آنها استفاده کنید. - cache-from در Job ساخت: چون هر Job در یک Docker-in-Docker تازه اجرا میشود، کش لایهها از بین میرود. با
--cache-fromوBUILDKIT_INLINE_CACHE، لایههای تغییرنکرده از ایمیج قبلی در Registry استفاده میشوند. - rules: Build و استقرار فقط روی شاخهی پیشفرض اجرا میشوند؛ در Merge Requestها فقط تست اجرا میشود.
- environment: GitLab تاریخچهی استقرارها را در بخش Operate ← Environments نگه میدارد.
قدم ۶: تست Pipeline
git add .gitlab-ci.yml
git commit -m "Add CI/CD pipeline"
git push origin main
در بخش Build ← Pipelines اجرای مراحل را ببینید. بعد از سبز شدن Job استقرار، روی سرور بررسی کنید:
cd /opt/myapp
docker compose ps
docker compose logs --tail=50 app
بهبودهای بعدی
- Migration دیتابیس: قبل از
up -d، یک مرحلهیdocker compose run --rm app <migrate-command>اضافه کنید. - Staging: یک Job استقرار دیگر برای سرور Staging بسازید و استقرار Production را با
when: manualدستی کنید. - اسکن امنیتی: ایمیج را قبل از Push با ابزاری مثل Trivy اسکن کنید.
- Registry مستقل: اگر چند پروژه و چند تیم دارید، Harbor امکانات بیشتری مثل Proxy Cache و اسکن خودکار دارد.
- Dockerfile بهینه: هر چه ایمیج کوچکتر و لایهها مرتبتر باشد، Pipeline سریعتر است. نمونهها را در راهنمای داکرایز کردن لاراول، جنگو و Node.js ببینید.
جمعبندی
با این Pipeline، هر تغییر در شاخهی اصلی بهصورت خودکار تست، ساخته و مستقر میشود؛ روی سرور Production دقیقاً همان ایمیجی اجرا میشود که تست شده است و برگشت به نسخهی قبل چند ثانیه طول میکشد. نکتهی کلیدی، جدا نگه داشتن Runner، استفاده از متغیرهای Protected و اینکه سرور Production هیچوقت خودش Build نمیگیرد.
نمیخواهید این کار را خودتان انجام دهید؟ تیم مروانت GitLab Self-managed، Runnerها، Registry و Pipeline کامل CI/CD را برای پروژههای شما راهاندازی میکند. صفحهی برونسپاری پروژههای Docker را ببینید.
