با استفاده از GitLab و داکر یک خط CI/CD متنوع بسازین تا بهره‌وری رو افزایش بدین.

معمولا پیش میاد که توسعه‌دهنده‌ها برای توسعه یک پروژه شخصی به یک محیط استقرار خصوصی نیاز پیدا کنن. من گزینه‌های زیادی رو برای آزمایش، پکیج‌بندی و استقرار راه‌حل‌هام در طول توسعه‌ی یک محصول امتحان کردم.

اگر هنوز یک محیط CI/CD ندارین، باید به فکر خرید یا راه‌اندازی یکی از اون‌ها باشین. راه‌حل‌های زیادی در قالب پلتفرم‌های آنلاین (PaaS) وجود داره، اما من نسخه عمومی GitLab رو برای نیازهای گیت و CI/CD خودم ترجیح میدم. نسخه خود‌-میزبان GitLab عملا محدودیتی نداره. من هر دو نوع اپلیکیشن‌های میکروسرویس و یکپارچه رو توسعه می‌دم، پس انعطاف‌پذیری CI/CD برای من خیلی مهمه.

در این مقاله می‌خوایم راه‌اندازی GitLab با استفاده از داکر رو یاد بگیریم. ما برای میزبانی GitLabمون از داکر داخل یک VPS استفاده می‌کنیم. هنگام ساخت اجراکننده‌های GitLab رو هر زمان که بخوایم فراخوانی می‌کنیم و ایمیج‌های داکر رو می‌سازیم. با پیکربندی که خواهیم گفت، قادریم هر build رو با هر تکنولوژی stack، می‌خواد Go-based باشه یا NodeJS یا جاوا یا غیره، اجرا کنیم.

انتخاب سخت‌افزار

برای انتخاب سخت‌افزارمون، باید ببینیم می‌خوایم چه چیزی رو داخل این ماشین استقرار بدیم. این یک فهرست تقریبی از چیزهایی هست که برای راه‌اندازی سیستممون نیاز داریم:

  • داکر. در لینوکس، هیچ امکانی برای مجازی‌سازی نداره، به همین دلیل باید از منابع سیستم میزبان استفاده کنه.
  • GitLab CE که داکر رو اجرا و استفاده می‌کنه - ما بین چهار تا شش گیگ RAM فقط برای همین قسمت نیاز داریم.
  • اجراکننده‌های GitLab. چهل تا صد مگابایت RAM اضافه برای هر اجراکننده لازمه، اگر دارید چیز سنگینی می‌سازید، ممکنه بیشتر از این هم نیاز داشته باشید.

در خصوص فضای ذخیره‌سازی، این هم یک زمینه دیگه‌ست که باید در اون راحت باشیم. GitLab بیش از حد فضا مصرف می‌کنه و این کارش دلیل خوبی داره - جدا از CI/CD، که اون هم یکی از مخازن گیته، و ساختن کش، تنها چیزی که نیاز داریم Disk و RAM هست. می‌تونیم در مورد CPU کوتاه بیایم چون در حین اجرای CI/CD الزامی نیست.

آماده سازی GitLab درون داکر

گام‌هایی که در ادامه میاد این فرایند رو توضیح می‌ده:

  • نصب داکر
  • نصب GitLab داخل یک کانتینر داکر
  • نصب Nginx در ماشین میزبان
  • اجرای GitLab از طریق HTTPS و با استفاده از Nginx میزبان  و دستور certbot
  • اضافه کردن تعدادی اجراکننده GitLab با استفاده از داکر و اتصال اون‌ها به فایل نصب و راه‌اندازی GitLab ما

یک پیش‌نیاز برای گام‌هایی که در این پست ذکر شده، داشتن یک داکر نصب شده کامل است. یک آموزش جامع توسط Digital Ocean وجود داره که برای هر ماشینی که اوبونتو LTS 18.xx بر روی اون نصب شده، قابل اجرا است.

نصب GitLab به عنوان یک کانتینر داکر

بعد از نصب و راه‌ندازی داکر، نوبت به اجرای ایمیج GitLab با استفاده از یک محل ذخیره‌سازی دائمی داخل ماشین میزبان است. بنابراین GitLab داخل یک کانتینر داکر اجرا می‌شه، ولی از فضای ذخیره‌سازی ماشین میزبان برای ذخیره‌سازی داده و بارگذاری پیکربندی استفاده می‌کنه.

sudo docker run --detach \
  --hostname gitlab.example.com \
  --publish 127.0.0.1:4443:443 --publish 127.0.0.1:4000:80 \
  --name gitlab \
  --restart always \
  --volume /srv/gitlab/config:/etc/gitlab \
  --volume /srv/gitlab/logs:/var/log/gitlab \
  --volume /srv/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest

این یعنی:

نام میزبان رو هنگام نصب gitlab.example.com تعیین کنید.

پورت‌های 443، 80 و 22 رو باز کنید و به پورت‌های متقابلشون در ماشین میزبان وصلشون کنید.

حجم‌های کانتینر رو بر روی ماشین میزبان سوار کنید.

  • پوشه srv/gitlab/config/ تنظیمات GitLab رو نگه می‌داره
  • پوشه  srv/gitlab/logs/ لاگ‌های GitLab رو نگه می‌داره
  • پوشه srv/gitlab/data/ داده‌های منابع گیت رو نگه می‌داره

در نظر داشته باشید که اون پوشه‌ها در ماشین میزبان نشانه‌گذاری شدن، که یعنی محل اون‌ها حین اجرای مجدد کانتینرها و ارتقاهای GitLab ثابت می‌مونه. اگر ما با استفاده از داکر نسخه ارتقا یافته GitLab رو اجرا کنیم و همون پوشه‌ها رو در همون محل‌ها در میزبان نشانه‌گذاری کنیم، می‌تونیم از داده‌های قدیمی‌مون در نسخه جدید GitLab استفاده کنیم. 

همچنین این یعنی لازم نیست موقع خوندن اسناد GitLab، محل پوشه‌های پیکربندی رو به خاطر بسپرید. برای مثال در نصب فعلی، ما پوشه /etc/gitlab رو به آدرس srv/gitlab/config در ماشین نشانه‌گذاری کردیم. بنابراین وقتی پیکربندی GitLab می‌گه: شما همچنین می تونین فقط /etc/gitlab/gitlab.rb… رو ویرایش کنید، منظورش اینه که شما می‌تونین فقط/srv/gitlab/config/gitlab.rb رو در ماشین میزبانتون ویرایش کنین. 

چون موقع مستندسازی نصب GitLab، فرض رو بر این می‌ذاره که GitLab داخل ماشین میزبان نصب شده، در حالی که در این مورد داخل کانتینر داکر نصب شده.

دسترسی به URL

متوجه اون به‌هم‌پیوستگی عجیب پورت‌ها در 127.0.0.1:4443:443 --publish 127.0.0.1:4000:80 شدین؟ خب این یعنی این که مسیر ورودی و خروجی فقط در میزبان local قرار داره. پورت‌های 4443 و 4000 کانتینر هرگز برای دنیای بیرون باز نخواهند بود. ما از پروکسی برگردان Nginx که داخل همون ماشین نصب شده استفاده می‌کنیم تا به URL دسترسی داشته باشیم.

ما می‌خوایم در دامنه انتخابی خودمون، GitLab فقط از طریق HTTPS قابل دسترسی باشه. بیاین اسم این دامنه رو بذاریم mydomain.com. برای این مرحله ما باید وارد کنترل پنل سایتی بشیم که دامنه رو ازش خریداری کردیم و وارد قسمتی بشیم که اجازه می‌ده نام سرورها رو انتخاب کنیم. روش ایجاد بین ارائه دهنده‌های خدمات ثبت دامنه مختلف فرق داره، اما اصول کار یکسانه. در کنترل پنل، نام زیر‌دامنه (در این مورد git) رو بر روی IP سرور تنظیم کنید. به خاطر این پست، بیاین فرض کنیم IP سرور شما  55.55.55.55 است.

تنظیمات نام سرور ما باید به شکل زیر باشه (ممکنه مقادیر بیشتری ببینید، ولی در این مورد اونا بی ربطن)

TTL Value Name Type
600 55.55.55.55 git A
600 55.55.55.55 @ A

خدمت رسانی از طریق Nginx و https

دو راه برای راه اندازی Nginx وجود داره.:

  1.  استفاده از یک کانتینر داکر، نصب داخل یک ماشین میزبان
  2. نشانه‌گذاری پورت‌های باز  به دنیای بیرون  توسط کانتینرهای داکر

من شخصا راه دوم رو ترجیح می‌دم، چون دوست دارم همه چیز تمیز باشه. همین‌طور ترجیح می‌دم گواهی‌نامه‌های certbot رو برای استفاده‌های آینده مثل آزمایش ایمیج‌های داکر (از میزبان به عنوان سرور محل آزمایش استفاده کنیم) در یک مکان داخل ماشین مجازی متمرکز کنم. 

برای انجام این کار اول باید با استفاده از دستور زیر Nginx رو نصب کنیم:

apt install nginx

و بعد certbot رو نصب کنیم.

sudo apt-get updatesudo apt-get install software-properties-commonsudo add-apt-repository universesudo add-apt-repository ppa:certbot/certbotsudo apt-get update

و سپس certbot رو اجرا کنیم.

sudo certbot --nginx

اگر موفق شده باشید، وقتی از آدرس https://mydomain.com بازدید کنید، یک صفحه وب می‌بینید نه یک درایو آزمایشی.

بعد سرورتون رو داخل پیکربندی Nginx راه اندازی کنید. Certbot از قبل در مسیر /etc/nginx/sites-enabled یک فایل پیکربندی ایجاد کرده. پیکربندی رو برای میزبان و پورتی که بهش اشاره می‌کنه مثل زیر عوض کنید:

server {
	server_name git.domain.com;
	client_max_body_size 256M;

	location / {
		proxy_pass http://localhost:4000;

		proxy_read_timeout 3600s;
		proxy_http_version 1.1;
		# Websocket connection
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection $connection_upgrade;
	}

	listen [::]:443;

	listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/git.domain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/git.domain.com/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

در ابتدا باید زیردامنه رو راه بندازید و به پورت وصلش کنید. وقتی دامنه git.example.com باشه، Nginx ترافیک رو روی پورت 4000 روی این ماشین منتقل می‌کنه، که در این مثال با GitLab داخل داکر مطابقت داره. 

وقتی فایل نصب Nginx رو با استفاده از دستور service nginx restart مجدد راه‌اندازی کنیم، آماده‌ایم تا از دامنه https://git.mydomain.com/ دیدن کنیم. اگر همه چیز درست انجام شده باشه، ما نتیجه زیر رو می‌بینیم. یک صفحه که به شما اجازه می‌ده پسوردتون برای کاربر root رو تنظیم کنید.

وقتی راه‌ندازی اولیه رو تموم کردیم، یک مخزن گیت با یک اپلیکیشن NodeJS داخلش می‌سازیم. (نحوه انجام این کار اینجا توضیح داده نمی‌شه).

اپلیکیشن نمونه

در این قسمت فرض بر اینه که ما از قبل یک مخزن گیت با یک اپلیکیشن NodeJS قابل ساخت و فایل داکر معتبر داخلش ساختیم. به عنوان نمونه، ما از یک فایل داکر یه کم پیشرفته‌تر استفاده می‌کنیم که یک اپلیکیشن NodeJS می‌سازه. فایل داکر ممکنه فرق داشته باشه، اما فرایند انجام کار فرق نداره. فایل داکر زیر، داخل root مخزن گیت ذخیره شده.

FROM node:10.16
EXPOSE 8080

WORKDIR /app/

COPY . .
COPY package*.json ./

RUN npm install
RUN npm run build
RUN echo "finished building"
RUN ls -afl dist

FROM node:10.16-alpine
WORKDIR /app/

COPY --from=0 /app/dist ./dist
COPY package*.json ./
COPY --from=0 /app/node_modules ./node_modules


ENTRYPOINT NODE_ENV=production npm run start:prod

فایل داکر بالا از node:10.16 برای انتقال اپلیکیشن ما استفاده می‌کنه. وقتی کار ساخت تموم شد، با استفاده از 10.16-alpine به عنوان ایمیج اصلی، یک ایمیج آماده اجرا تولید می‌کنه. به این ترتیب می‌تونیم تمام اجزای مورد نیاز رو موقع ساخت به صورت نصب شده داشته باشیم (webpack، node-sass، ابزارهای ترجمه متن)، اما فقط تعدادی از اون‌ها در حال اجرا باشن، که باعث می‌شه اندازه ایمیج خیلی کم باشه. این کار باعث می‌شه وقتی فایل رو داخل رجیستری کانتینر داکر ذخیره می‌کنیم، در استفاده از فضا صرفه‌جویی خوبی بشه (یک گیگابایت برای هر build). این موضوع به خصوص زمانی که از رجیستری داکر پولی استفاده می‌کنید خیلی مهمه.

اضافه کردن اجراکننده‌ها و آماده کردن build‌ها با داکر

حالا که ما یک فایل نصب آماده داریم، باید اجرا کننده‌های GitLab (gitlab-runners) رو اضافه و به فایل نصبمون متصل کنیم. وقتی Gitlab Pipline اجرا شد، دنبال یک اجرا‌کننده پیکربندی شده که در دسترسه می‌گرده و از اون برای اجرای build استفاده می‌کنه. یک اجراکننده GitLab در حالت‌های مختلفی می‌تونه عمل کنه، که نشون می‌ده build چطور کار  می‌کنه. در کنار حالت‌های مختلف، از فراخوانی پادهای Kubernetes، یا کانتینرهای داکر برای اجرای build‌ها پشتیبانی می‌کنه. 

برای راحتی کار، ما از حالت داکر ساده استفاده می‌کنیم، که یک کانتینر جدید با ایمیج انتخابی شما (که توسط فایل داکرتون مشخص میشه) ظاهر می‌کنه. 

دستور زیر رو داخل ترمینال اجرا کنید:

docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

دستور بالا gitlab-runner رو به عنوان یک کانتینر اجرا می‌کنه. همچنین پوشه /srv/gitlab-runner/config مزبان رو بر /etc/gitlab-runner یعنی محل کانتینر سوار می‌کنه. همون‌طور که در مورد فایل نصب GitLab‌مون اتفاق افتاد، پیکربندی کانتینر داخل پوشه میزبان ما هم ماندگاره، که یعنی با تغییر پیکربندی داخل /srv/gitlab-runner/config، پیکربندی کانتینر gitlab-runner هم تغییر می‌کنه. همین طور اجرای مجدد کانتینر باعث از بین رفتن پیکربندی نمی‌شه. 

یک ps ساده داکر در صورتی که همه چیز درست باشه، اطلاعات زیر رو به ما نشون می‌ده.

8c3322fea7d4        gitlab/gitlab-ce:latest                        "/assets/wrapper"        42 hours ago        Up 42 hours (healthy)   0.0.0.0:23->22/tcp, 127.0.0.1:4000->80/tcp, 0.0.0.0:4443->443/tcp   gitlab

48aea5eded7e        gitlab/gitlab-runner:latest                    "/usr/bin/dumb-init ..."   5 months ago        Up 5 days                                                                                   gitlab-runner

ما باید از این کانتینر برای ساخت پیکربندی اجراکننده جدید فایل نصب Gitlab‌مون استفاده کنیم. به آدرس https://mydomain.com/admin/runners برید.

می‌بینیم GitLab پیام "Use the following registration token during setup" رو نشون و یک توکن ثبت بهمون می‌ده. 

این توکن توسط gitlab runner برای ثبت پیکربندی جدید اجراکننده استفاده می‌شه. بعد از کپی کردن توکن، بیاین با استفاده از فایل پیکربندی gitlab-runner که تازه ساختیم، پیکربندی اجراکننده رو آماده کنیم. بیاین با استفاده از دستور bash وارد کانتینر تازه ساخته شده gitlab-runner بشیم.

$ docker exec -ti gitlab-runner bash

حالا با استفاده از دستور gitlab-runner register یک فایل پیکربندی اجراکننده جدید درست کنیم. کنسول از شما چند سوال در مورد نحوه آماده‌سازی اجراکننده‌تون می‌پرسه. ما اطلاعات خواسته شده رو به صورت زیر وارد می‌کنیم:

root@48aea5eded7e:/# gitlab-runner register

Runtime platform                                    arch=amd64 os=linux pid=249 revision=a987417a version=12.2.0
Running in system-mode.

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://mydomain.com

Please enter the gitlab-ci token for this runner:
<<your gitlab ci token here>>

Please enter the gitlab-ci description for this runner:
[48aea5eded7e]: sample-docker-runner

Please enter the gitlab-ci tags for this runner (comma separated):
docker (whatever you need)
Registering runner... succeeded                     runner=4usxjjv2

Please enter the executor: custom, docker-ssh, parallels, shell, docker+machine, docker, ssh, virtualbox, docker-ssh+machine, kubernetes:
docker

Please enter the default Docker image (e.g. ruby:2.6):
alpine:latest
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

root@48aea5eded7e:/#

حالا می‌تونیم دوباره از لینک https://mydomain.com/admin/runners بازدید کنیم و می‌بینیم که اجراکننده جدیدمون در لیست اجراکننده‌ها در دسترسه:

نوشتن مراحل build و اجرای build

فایل .gitlab-ci.yml زیر داخل root مخزن GitLab ما قرار داره.

image: docker:latest

build_job:
  stage: build
  script:
    - ls
    - echo "starting job..."
    - docker build -t "${CI_PROJECT_NAME}:${CI_COMMIT_REF_NAME}-0.1.${CI_JOB_ID}" .
    - echo job finished
  only:
    - develop
    - master

به لینک https://mydomain.com/<myproject>/<myrepo>/pipelines برین و pipeline رو اجرا کنین.

وقتی build تمام شد، ماشین داکر میزبان ما یک ایمیج جدید می‌سازه، همونی که توسط این pipeline ساخته شده.

موضوعات پیشرفته

وصل کردن gitlab-runner و کانتینر GitLab به یک شبکه

اگر تا حالا متوجه نشدید، هرچند فایل نصب GitLab استفاده از http://localhost:4000 رو پیشنهاد کرده بود، ما برای GitLab یک URL کامل از دامنه‌مون در نظر گرفتیم. ما این کار رو کردیم چون gitlab-runner و کانتینرهای gitlab در یک شبکه منطقی ساکن نیستن، از این رو وقتی localhost رو از داخل کانتینر gitlab-runner فراخوانی می‌کنیم، پیام Connection Refused رو دریافت می‌کنیم. 

برای درست کردن این مشکل، باید به داکر بگیم اون دو کانتینر در یک شبکه منطقی وجود دارن.

در مثال ما، می‌تونیم این کار رو انجام بدیم:

$ docker network create gitlabnet
$ docker network connect gitlabnet gitlab
$ docker network connect gitlabnet gitlab-runner

که gitlab و gitlab runner نام کانتینرها هستن که در طول ساخت نام‌گذاری شدن (با استفاده از دستور --name=...).

حالا به جای دادن آدرس https://git.mydomain.com به دستور gitlab-runner register به عنوان آدرس root، می‌تونیم آدرس http://gitlab رو بدیم که آدرس DNS داخلی داکر داده شده است.

پرهیز از خطای Docker-In-Docker (داکر داخل داکر)

در مثال ما، همون‌طور که در فایل .gitlab-ci.yml تعیین شده، از image: docker:latest به عنوان ایمیج اصلی استفاده کردیم. فایل داکر ما از یک ایمیج NodeJS هم استفاده می‌کنه. این یعنی شما از Docker-In-Docker استفاده می‌کنید (dind). این  کار فشار زیادی روی build ها میذاره. ممکنه در بعضی موارد مهم نباشه، اما اگر منابع محدودی دارید، هنگام ساخت به سرعت حافظه‌تون تموم میشه.  برای این مشکل دو راه حل وجود داره:

اولین راه حل خیلی ساده است و مربوط می‌شه به بازنویسی فایل‌های gitlab-ci.yml تا از ایمیج انتخابی خودتون به عنوان ایمیج اصلی استفاده کنید و تمام مراحل رو به جای داکر در اون‌جا انجام بدید. این کار بار ساخت کامل رو از روی دوش فایل داکر بر می‌داره، چون شما احتمالا فقط برای مرحله آخر هر build بهش نیاز دارید. (فایل‌های ساخته‌شده رو کپی کنید و ایمیج رو بسازید).

اگر می‌خواین به فایل داکرتون کنترل کامل بدین، راه دیگه‌ای هم هست. می‌تونین اجراکننده رو طوری تنظیم کنید که برای اجرای دستورات فایل داکر از داکر میزبان استفاده کنه. شما این کار رو می‌تونین با تنظیم پیکربندی gitlab-runner برای استفاده از داکر میزبان انجام بدین. در مثال ما، فایل پیکربندی در مسیر /srv/gitlab-runner/config/config.toml قرار داره. فایل رو به صورت زیر تغییر بدید:

[[runners]]
  name = "sample-docker-runner"
  url = "https://git.mydomain.com"
  token = "NyVXkhyh1atSm5x_werQ"
  executor = "docker"
  [runners.custom_build_dir]
  [runners.docker]
    tls_verify = false
    image = "alpine:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache  = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    shm_size = 0
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

دقت کنید که ما /var/run/docker.sock:/var/run/docker.sock رو به کانتینری که توسط gitlab-runner فراخوانی می‌شه، اضافه کردیم.

اما توجه داشته باشید که این کار، داکر میزبان رو در معرض کانتینر قرار می‌ده. این کار خطرات امنیتی رو به همراه داره و باید در استقرارهای بزرگ GitLab با در نظر گرفتن این خطرات، احتیاط کرد.

حالا چه کار باید بکنیم

راه‌اندازی GitLab با استفاده از داکر از چیزی که فکر می‌کردم راحت‌تر بود. من از این موضوع به عنوان یک پایه برای راه اندازی یک محیط توسعه که در بین چند سرور گسترده شده برای پروژه‌های شخصی استفاده کنیم.

این جا کارهایی رو برای کسانی که این پست رو کامل کردن پیشنهاد می‌دم.

  • پیکربندی خروجی های SSH. با این پیکربندی می‌تونین از HTTPS برای کشیدن‌ها و رها کردن‌‌ها استفاده کنید. اگر می‌خواین SSL رو فعال کنید، باید پورت 22 از کانتینر GitLab رو باز کنید و کمی پیکربندی‌های پیشرفته انجام بدید تا از قاطی شدن SSL گیت‌لب با SSL ماشین جلوگیری کنید (که به صورت پیش فرض روی یک پورت اجرا میشن).
  • از رجیستری‌های خارجی داکر استفاده کنید. می‌تونید از یک سرویس رایگان مثل canister.io به عنوان میزبان برای ایمیج‌های داکر استفاده کنید.
  • از یک ابزار مدیریت ایمیج/کانتینر مثل https://www.portainer.io/ برای مدیریت ایمیج ها/کانتینرهای ماشین میزبان استفاده کنید. این شامل فایل نصب GitLab و اجراکننده‌های GitLab هم می‌شه.
  • با کمی جستجو می‌تونید سرورهای خیلی ارزونی پیدا کنید. اما من پیشنهاد می‌کنم یک هاست پیدا کنید که هزینه‌ش رو به صورت ساعتی می‌گیره. این موضوع به خصوص زمانی مفیده که برای یک پروژه کوچک به محیط CI/CD نیاز دارید، چون می‌تونید ماشین رو در ساعاتی که استفاده نمی‌کنید، خاموش کنید تا هزینه چیزی رو که استفاده نمی‌کنید، پرداخت نکنید.

سخن آخر

ما تنها یک چشمه از کارهایی که می‌شه با این پیکربندی انجام داد رو نشون دادیم. من خودم به خاطر کارهایی که می‌شه با یک VPS کوچک که روش داکر و GitLab نصب شده، انجام داد، بارها شگفت زده شدم.