راه‌اندازی 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 شکست می‌خورد. روش جدید این است:

  1. در پروژه به مسیر Settings ← CI/CD ← Runners بروید و New project runner را بزنید. (برای Runner مشترک بین پروژه‌ها، همین کار را در سطح گروه یا در Admin area انجام دهید.)
  2. تگ‌ها (مثلاً docker) و سایر تنظیمات را همین‌جا در رابط وب تعیین کنید و Runner را بسازید.
  3. 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 را ببینید.

دیدگاهتان را بنویسید

جستجو

فهرست مطالب