Създаване на CI/CD верига и автоматизация на работата с Docker

Написах първите си сайтове в края на 90-те години. Тогава беше много лесно да ги направиш работещи. Имаше Apache сървър на някакъв споделен хостинг и можеше да влезеш в този сървър по FTP, като напишеш нещо такова в браузъра: ftp://ftp.example.com. След това необходимо беше да въведеш име и парола и да качиш файловете на сървера. Бяха други времена, всичко тогава беше по-просто, отколкото сега.

Създаване на CI/CD верига и автоматизация на работата с Docker

През последните две десетилетия всичко се промени значително. Сайтовете станаха по-сложни, преди да бъдат пуснати в production, трябваше да се събират. Един единствен сървър стана множество сървъри, работещи зад балансировачи на натоварването, а използването на системи за контрол на версиите стана обичайно.

За личния си проект имах специална конфигурация. И знаех, че ми е необходима възможността да развърна сайта в продукция, извършвайки само едно действие: записването на кода в клон master в GitHub. Освен това, знаех, че за да осигуря работата на моето малко уеб приложение, не искам да се занимавам с управлението на огромен клъстер Kubernetes, или да ползвам технологията Docker Swarm, или да поддържам парк от сървъри с подове, агенти и всякакви други усложнения. За да постигна целта си за максимално опростяване на работата, ми се наложи да се запозная с CI/CD.

Ако имате малък проект (в нашия случай става дума за Node.js проект) и искате да научите как да автоматизирате разгръщането на този проект, така че това, което се съхранява в репозитория, точно да отговаря на това, което работи в продукция, предполагам, че тази статия може да ви заинтересува.

Предварителни изисквания

Очаква се читателят на тази статия да има основни познания в работата със командния ред и писането на Bash скриптове. Освен това, ще са му необходими акаунти Travis CI и Docker Hub.

Цели

Няма да кажа, че тази статия може безусловно да бъде наречена "учебно ръководство". Това е по-скоро документ, в който разказвам за това, което съм научила, и описвам удовлетворителния за мен процес на тестване и разгръщане на кода в продукция, извършван за един автоматизиран проход.

Ето какъв стана моят работен процес.

За кода, изпратен в който и да е клон на репозитория, освен master, се извършват следните действия:

  • Стартира се изграждане на проекта на Travis CI.
  • Извършват се всички модулни, интеграционни и енд-то-енд тестове.

Само за кода, който попада в master, се извършва следното:

  • Всичко, което е казано по-горе, плюс…
  • Създаване на Docker образ на базата на текущия код, настройки и среда.
  • Публикуване на образа в Docker Hub.
  • Свързване с продукционен сървър.
  • Изтегляне на образа от Docker Hub на сървъра.
  • Спиране на текущия контейнер и стартиране на нов, базиран на новия образ.

Ако не знаете нищо за Docker, образи и контейнери – не се притеснявайте. Ще ви разкажа всичко това.

Какво е CI/CD?

Съкращението CI/CD се разшифрова като „непрекъсната интеграция/непрекъснато разгръщане“.

▍Непрекъсната интеграция

Непрекъсната интеграция е процес, при който разработчиците правят комити в основното хранилище на изходния код на проекта (обикновено в клон master). Качеството на кода се осигурява чрез автоматизирано тестване.

▍Непрекъснато разгръщане

Непрекъснатото разгръщане е често автоматизирано разгръщане на кода в продукция. Втората част на съкращението CI/CD понякога се разширява до „непрекъсната доставка“. Това, по принцип, е същото като „непрекъснато разгръщане“, но „непрекъсната доставка“ предполага необходимостта от ръчно потвърждаване на промените преди стартирането на процеса на разгръщане на проекта.

Започване

Приложението, на което учех всичко това, се нарича TakeNote. Това е уеб проект, по който работя, предназначен за вземане на бележки. Първоначално се опитах да направя JAMStack-проект или просто фронтенд приложение без сървър, за да се възползвам от стандартните възможности за хостинг и разгръщане на проекти, които предлага Netlify. С увеличаването на сложността на приложението, се наложи да създам и неговата сървърна част, а това означаваше, че трябваше да формулирам собствена стратегия за автоматизирана интеграция и автоматизирано разгръщане на проекта.

В моя случай приложението представлява Express-сървър, работещ в среда на Node.js, обслужващ едностранично React приложение и поддържащ защитен сървърен API. Тази архитектура следва стратегия, която можете да намерите в това ръководство за фуллстек автентикация.

Консултирах се с другом, който е експерт по автоматизация и ме попита какво трябва да направя, за да работи всичко както трябва. Той ми подхвърли идея за това как трябва да изглежда автоматизираният работен поток, изложен в раздела „Цели“ на тази статия. Фактът, че съм поставила такива цели, означава, че трябва да разбера как да използвам Docker.

Docker

Docker е инструмент, който, благодарение на технологията за контейнеризация, позволява лесното разпространение на приложения, а също и тяхното разгръщане и стартиране в една и съща среда, дори когато самата платформа Docker работи в различни среди. За начало трябваше да получа инструментите за команден ред (CLI) на Docker. Инструкцията за инсталиране на Docker не може да се нарече много ясна и разбираема, но от нея може да се разбере, че за да направите първата стъпка в инсталацията, трябва да изтеглите Docker Desktop (за Mac или Windows).

Docker Hub е нещо като GitHub за git-репозитории или регистър npm за JavaScript пакети. Това е онлайн репозитория за образи на Docker. Именно там се свързва Docker Desktop.

Така че, за да започнете работа с Docker, трябва да направите две неща:

След това можете да проверите работоспособността на Docker CLI, като изпълните следната команда за проверка на версията на Docker:

docker -v

След това влезте в Docker Hub, като въведете, когато ви попитат, потребителското си име и парола:

docker login

За да използвате Docker, трябва да разбирате концепциите за образи и контейнери.

▍Образи

Образът е като план, съдържащ инструкции за изграждане на контейнер. Това е неизменяем моментен снимка на файловата система и настройките на приложението. Разработчиците могат лесно да обменят образи.

# Вывод сведений обо всех образах
docker images

Тази команда ще изведе таблица със следния заглавие:

REPOSITORY     TAG     IMAGE ID     CREATED     SIZE
---

След това ще разглеждаме някои примери за команди в същия формат — първо идва командата с коментар, а след това — пример за това какво може да изведе.

▍Контейнери

Контейнерът е изпълним пакет, който съдържа всичко необходимо за изпълнението на приложението. При този подход приложението винаги ще работи по един и същи начин, независимо от инфраструктурата: в изолирана среда и при една и съща среда. Става въпрос за това, че в различни среди се стартират инстанции на едно и също изображение.

# Перечисление всех контейнеров
docker ps -a
CONTAINER ID     IMAGE     COMMAND     CREATED     STATUS     PORTS     NAMES
---

▍Тагове

Тагът е указание за конкретна версия на изображението.

▍Кратко ръководство за командите на Docker

Ето преглед на някои често използвани команди на Docker.

Екип

Контекст

Действие.

docker build

Образ

Създаване на изображение от Dockerfile

docker tag

Образ

Тагиране на изображение

docker images

Образ

Извеждане на списък с изображения

docker run

Контейнерът

Стартиране на контейнер, основан на изображение

docker push

Образ

Изпращане на изображение в регистратура

docker pull

Образ

Изтегляне на изображение от регистратура

docker ps

Контейнерът

Извеждане на списък с контейнери

docker system prune

Изображение/Контейнер

Премахване на неизползвани контейнери и изображения

▍Файл Dockerfile

Знам как локално да стартирам приложение за продукция. Имам конфигурация на Webpack, предназначена за изграждане на готово React приложение. След това имам команда, която стартира сървър, основан на Node.js, на порта 5000. Изглежда така:

npm i         # инсталиране на зависимости
npm run build # изграждане на React приложение
npm run start # стартиране на Node сървър

Трябва да се отбележи, че нямам пример на приложение за този материал. Но за експерименти може да подхожда всяко просто Node приложение.

За да използвате контейнера, ще трябва да дадете инструкции на Docker. Това става чрез файл, наречен Dockerfile, който се намира в кореновата директория на проекта. Този файл в началото изглежда доста объркан.

Но това, което съдържа, е просто описание с особени команди за нещо, което прилича на настройка на работна среда. Ето някои от тези команди:

  • FROM — Тази команда стартира файла. В нея се указва основното изображение, на което се изгражда контейнерът.
  • COPY — Копиране на файлове от локален източник в контейнера.
  • WORKDIR — Настройка на работната директория за следващите команди.
  • RUN — Изпълнение на команди.
  • EXPOSE — Настройка на порта.
  • ENTRYPOINT — Указване на изпълняваната команда.

Dockerfile може да изглежда така:

# Загрузить базовый образ
FROM node:12-alpine

# Скопировать файлы из текущей директории в директорию app/
COPY . app/

# Использовать app/ в роли рабочей директории
WORKDIR app/

# Установить зависимости (команда npm ci похожа npm i, но используется для автоматизированных сборок)
RUN npm ci --only-production

# Собрать клиентское React-приложение для продакшна
RUN npm run build

# Прослушивать указанный порт
EXPOSE 5000

# Запустить Node-сервер
ENTRYPOINT npm run start

В зависимост от избрания на основния образ, може да се наложи да инсталирате допълнителни зависимости. Факт е, че някои основни образи (като Node Alpine Linux) са създадени с идеята да бъдат възможно най-компактни. В резултат на това в тях може да липсват някои приложения, на които разчитате.

▍Сглобяване, тагване и стартиране на контейнер

Локалното сглобяване и стартиране на контейнера - това е, след като имаме Dockerfile, задачите са доста прости. Преди да изпратите образа на Docker Hub, трябва да го тествате локално.

▍Сглобяване

Първо трябва да сглобите образ, посочвайки име и, което не е задължително, таг (ако таг не бъде зададен, системата автоматично ще назначи таг на образа latest).

# Сборка образа
docker build -t <image>:<tag> .

След изпълнението на тази команда можете да наблюдавате как Docker извършва сглобяването на образа.

Изпращане на контекста на сглобяване до Docker демон   2.88MB
Стъпка 1/9 : FROM node:12-alpine
 ---> ...изпълнение на стъпките за сглобяване...
Успешно сглобен 123456789123
Успешно тагнат <image>:<tag>

Сглобяването може да отнеме няколко минути — тук всичко зависи от това колко зависимости имате. След завършване на сглобяването можете да изпълните командата docker images и да погледнете описанието на новия си образ.

REPOSITORY          TAG               IMAGE ID            CREATED              SIZE
<image>             latest            123456789123        Преди минута        x.xxGB

▍Стартиране

Образът е създаден. А това означава, че на негова основа може да се стартира контейнер. Тъй като искам да имам възможност да получавам достъп до приложението, работещо в контейнера, на адрес localhost:5000, в лявата част на двойката 5000:5000 в следната команда зададох 5000. В дясната част се намира портът на контейнера.

# Запуск с использованием локального порта 5000 и порта контейнера 5000
docker run -p 5000:5000 <image>:<tag>

Сега, когато контейнерът е създаден и стартиран, можете да използвате командата docker ps за да погледнете информацията за този контейнер (или можете да използвате командата docker ps -a, която извежда информация за всички контейнери, а не само за работещите).

CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                      PORTS                    NAMES
987654321234        <image>             "</bin/sh -c 'npm run…"   6 секунди назад        Работи 6 секунди                0.0.0.0:5000->5000/tcp   stoic_darwin

Ако сега отидете на адрес localhost:5000 — можете да видите страницата на работещото приложение, която изглежда точно като страницата на приложението, работещо в продукционна среда.

▍Назначаване на таг и публикуване

За да използвате един от създадените образи на продукционния сървър, трябва да имате възможност да качите този образ от Docker Hub. Това означава, че първо трябва да създадете хранилище на Docker Hub за проекта. След това ще разполагате с място, където да изпратите образа. Образът трябва да бъде преименуван така, че името му да започва с името на потребителя ви в Docker Hub. След това трябва да следва името на хранилището. В края на името може да има всеки етикет. По-долу е показан пример за именуване на образи по тази схема.

Сега можете да съберете образ с ново име и да изпълните командата docker push за изпращане в хранилището Docker Hub.

docker build -t \/:\ .
docker tag \/:\ \/:latest
docker push \/:\

# На практика това може да изглежда, например, така:
docker build -t user\/app:v1.0.0 .
docker tag user\/app:v1.0.0 user\/app:latest
docker push user\/app:v1.0.0

Ако всичко върви по план, образът ще бъде достъпен на Docker Hub и лесно ще може да се качи на сървъра или да се предостави на други разработчици.

Следващи стъпки

До този момент се уверихме, че приложението, в формата на Docker контейнер, работи локално. Качихме контейнера на Docker Hub. Всичко това означава, че вече сме напреднали доста добре към целта. Сега трябва да решим още два въпроса:

  • Настройка на CI инструмента за тестване и разгръщане на кода.
  • Настройка на продукционния сървър, така че да може да качва и изпълнява нашия код.

В нашия случай като CI/CD решение използваме Travis CI. Като сървър — DigitalOcean.

Трябва да се отбележи, че тук можете да използвате и друга комбинация от услуги. Например, вместо Travis CI можете да използвате CircleCI или Github Actions. А вместо DigitalOcean — AWS или Linode.

Решихме да работим с Travis CI и в този сервис вече имам нещо настроено. Затова сега ще ви разкажа накратко как да го подготвите за работа.

Travis CI

Travis CI е инструмент за тестване и разгръщане на код. Не искам да влизам в подробности за настройката на Travis CI, тъй като всеки проект е уникален и това няма да донесе особена полза. Но ще разкажа за основите, които ще ви позволят да започнете работа, ако решите да ползвате Travis CI. Каквото и да изберете — Travis CI, CircleCI, Jenkins или нещо друго, навсякъде ще се прилагат подобни методи за настройка.

За да започнете работа с Travis CI, отидете на сайта на проекта и създайте акаунт. След това интегрирайте Travis CI с вашия GitHub акаунт. По време на настройването на системата, ще трябва да посочите репозитория, с който искате да автоматизирате работата, и да разрешите достъп до него. (Аз ползвам GitHub, но съм сигурна, че Travis CI може да се интегрира и с BitBucket, и с GitLab, и с други подобни услуги).

Всеки път, когато Travis CI започне работа, се стартира сървър, който изпълнява командите, посочени в конфигурационния файл, включително разгръщането на съответните клонове на репозитория.

▍Животен цикъл на задача

Конфигурационният файл на Travis CI, наречен .travis.yml и съхраняван в кореновата директория на проекта, поддържа концепцията за събития жизнен цикъл на задачата. Ето тези събития, представени в реда, в който се случват:

  • apt addons
  • cache components
  • before_install
  • инсталиране
  • before_script
  • скрипт
  • before_cache
  • after_success или after_failure
  • before_deploy
  • deploy
  • after_deploy
  • after_script

▍Тестване

В конфигурационния файл ще настроя локален сървър Travis CI. Като език избрах Node версия 12 и указах на системата, че трябва да инсталира зависимостите, необходими за използване на Docker.

Всичко, което е изброено в .travis.yml, ще се изпълнява при всички pull заявки към всички клонове на репозитория, освен ако не е указано друго. Това е полезна функция, тъй като означава, че можем да тестваме целия код, който постъпва в репозитория. Това позволява да знаем дали кодът е готов да бъде записан в клон master, и няма ли да наруши процеса на изграждане на проекта. В тази глобална конфигурация инсталирам всичко локално, стартирам сървъра на разработчика Webpack на фона (това е особеност на работния ми процес) и изпълнявам тестовете.

Ако искате да се показват значки с информация за покритие на кода с тестове в репозитория ви, тук можете да намерите кратко ръководство за използване на Jest, Travis CI и Coveralls за събиране и показване на тези данни.

И така, ето съдържанието на файла .travis.yml:

# Установить язык
language: node_js

# Установить версию Node.js
node_js:
  - '12'

services:
  # Использовать командную строку Docker
  - docker

install:
  # Установить зависимости для тестов
  - npm ci

before_script:
  # Запустить сервер и клиент для тестов
  - npm run dev &

script:
  # Запустить тесты
  - npm run test

Тук завършват действията, които се извършват за всички клони на репозитория и за pull заявки.

▍Разгръщане

Приемайки, че всички автоматизирани тестове са завършили успешно, ние, което не е задължително, можем да разпръснем кода на продукционния сървър. Тъй като искаме да правим това само за кода от клона master, предоставяме на системата съответните инструкции в настройките за разгръщане. Преди да опитате да използвате кода, който ще разгледаме по-нататък във вашия проект, искам да ви предупредя, че трябва да имате реален скрипт, който да се извиква за разгръщане.

deploy:
  # Събиране на Docker контейнер и изпращане на Docker Hub
  provider: script
  script: bash deploy.sh
  on:
    branch: master

Скриптът за разгръщане решава две задачи:

  • Събиране, тагиране и изпращане на образ на Docker Hub с помощта на CI инструмент (в нашия случай това е Travis CI).
  • Изтегляне на образ на сървъра, спиране на стария контейнер и стартиране на новия (в нашия случай сървърът работи на платформата DigitalOcean).

Първо трябва да настроите автоматичния процес на събиране, тагиране и изпращане на образ на Docker Hub. Всичко това много напомня на това, което вече правехме ръчно, с изключение на факта, че тук ни е необходима стратегия за присвояване на уникални тагове на образите и автоматизация на входа в системата. Имах затруднения с някои детайли на скрипта за разгръщане, като стратегия за тагиране, вход в системата, кодиране на SSH ключовете, установяване на SSH връзка. Но, за щастие, приятелят ми много добре се справя с bash, както и с много други неща. Той ми помогна да напиша този скрипт.

Така че, първата част на скрипта е изпращането на образа на Docker Hub. Да се направи това е доста просто. Използваната от мен схема за тагиране предполага комбиниране на git хаш и git таг, ако такъв съществува. Това позволява да се осигури създаването на уникален таг и опростява идентификацията на компилацията, на която е основан. DOCKER_USERNAME и DOCKER_PASSWORD – това са потребителските променливи на средата, които могат да бъдат зададени чрез интерфейса на Travis CI. Travis CI автоматично ще обработва тайна информация, така че тя да не попада в чужди ръце.

Ето първата част на скрипта deploy.sh.

#!/bin/sh
set -e # Остановить скрипт при наличии ошибок

IMAGE="<username>/<repository>"                             # Образ Docker
GIT_VERSION=$(git describe --always --abbrev --tags --long) # Git-хэш и теги

# Сборка и тегирование образа
docker build -t ${IMAGE}:${GIT_VERSION} .
docker tag ${IMAGE}:${GIT_VERSION} ${IMAGE}:latest

# Вход в Docker Hub и выгрузка образа
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push ${IMAGE}:${GIT_VERSION}

Каква ще бъде втората част на скрипта, зависи изцяло от това какъв хост използвате и как е организирано свързването с него. В моя случай, тъй като използвам Digital Ocean, за свързване към сървъра се използват командите doctl. При работа с AWS ще се използва утилитата aws, и т.н.

Настройката на сървъра не беше особено сложна. Настроих дроплет, базиран на основен образ. Трябва да се отбележи, че избраната от мен система изисква еднократна ръчна инсталация на Docker и еднократен ръчен старт на Docker. За инсталирането на Docker използвах Ubuntu 18.04, така че ако вие също използвате Ubuntu, можете просто да следвате този опростеното ръководство.

Не споменавам конкретни команди за услугата, тъй като този аспект може да варира значително в различни случаи. Просто ще предоставя общ план на действията, които се изпълняват след свързване по SSH към сървъра, на който проектът ще бъде разположен:

  • Трябва да намерите контейнера, който в момента работи, и да го спрете.
  • След това трябва да стартирате нов контейнер на заден план.
  • Трябва да зададете локалния порт на сървъра на стойност 80 — това ще позволи достъпа до сайта на адрес от вида example.com, без да посочвате порта, а не да използвате адрес като example.com:5000.
  • И, накрая, трябва да изтриете всички стари контейнери и образи.

Ето продължението на скрипта.

# Найти ID работающего контейнера
CONTAINER_ID=$(docker ps | grep takenote | cut -d" " -f1)

# Остановить старый контейнер, запустить новый, очистить систему
docker stop ${CONTAINER_ID}
docker run --restart unless-stopped -d -p 80:5000 ${IMAGE}:${GIT_VERSION}
docker system prune -a -f

Някои неща, на които да се обърне внимание

Възможно е, когато се свържете към сървъра по SSH от Travis CI, да видите предупреждение, което няма да позволи продължаването на инсталацията, тъй като системата ще чака реакция от потребителя.

Автентичността на хоста '<hostname> (<IP address>)' не може да бъде установена.
RSA ключовият отпечатък е <key fingerprint>.
Сигурни ли сте, че искате да продължите с връзката (да/не)?

Научих, че стринговият ключ може да бъде кодиране в base64, за да се запази в такъв вид, с който е удобно и безопасно да се работи. На етапа на инсталацията можете да декодирате публичния ключ и да го запишете в файл known_hosts за да се отървете от гореописаната грешка.

echo <public key> | base64 # извежда <публичния ключ, кодиране в base64>

В практиката тази команда може да изглежда така:

echo "123.45.67.89 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== you@example.com" | base64

И ето как изглежда това, което тя извежда — низ в кодировка base64:

123.45.67.89 ssh-rsa AABAB3NzaC1y...

Това е командата, за която споменахме по-горе

install:
  - echo  | base64 -d >> $HOME/.ssh/known_hosts

Същият подход може да се използва и с частния ключ при установяване на връзка, тъй като за достъп до сървъра вероятно ще ви е необходим частен ключ. При работа с ключа е нужно само да осигурите безопасното му съхранение в променлива на околната среда на Travis CI и да се уверите, че никъде не се извежда.

Още нещо, на което трябва да обърнете внимание, е, че може да се наложи да стартирате целия скрипт за разгръщане, представен в един ред, например — с помощта на doctl. Това може да изисква малко допълнителни усилия.

doctl compute ssh  --ssh-command "всички команди ще бъдат тук && тук"

TLS/SSL и балансировка на натоварването

След като направих всичко, за което ставаше въпрос по-горе, последният проблем, с който се сблъсках, беше, че сървърът нямаше SSL. Тъй като ползвам сървър на Node.js, за да накарам да работи обратен прокси Nginx и Let’s Encrypt, трябваше да се повозим.

Никак не ми се искаше да изпълнявам всичките тези SSL настройки ръчно, затова просто създадох балансировчик на натоварването и записах информацията за него в DNS. В случая с DigitalOcean, например, създаването на автоматично обновяем самоподписан сертификат на балансировчика на натоварването е проста, безплатна и бърза процедура. Този подход има и допълнителна предимство, което е, че ако е необходимо, позволява много лесно да настроите SSL на множество сървъри, работещи зад балансировчика на натоварването. Това позволява на самите сървъри напълно да не „мислят“ за SSL, но да продължат да използват порта 80. Така че настройката на SSL на балансировчика на натоварването е много по-лесна и удобна, отколкото алтернативните методи.

Сега можем да затворим всички портове на сървъра, които приемат входящи връзки — освен порта 80, използван за свързване с балансировчика на натоварването, и порта 22 за SSH. В резултат опит за директен достъп до сървъра през всички портове, с изключение на тези два, ще бъде неуспешен.

Итог

След като направих всичко, за което разказах в този материал, вече не се страхувах нито от платформата Docker, нито от концепцията за автоматизирани CI/CD вериги. Успях да настроя верига за непрекъсната интеграция, в която се извършва тестване на кода преди да влезе в продукция и автоматично разгръщане на кода на сървъра. Всичко това за мен все още е сравнително ново и съм уверена, че има начини да подобря автоматизирания си работен процес и да го направя по-ефективен. Затова, ако имате идеи по този въпрос — дайте през мен да знам. Надявам се, че тази статия ви е помогнала в вашите начинания. Искам да вярвам, че след като я прочетете, сте научили толкова, колкото научих и аз, докато се запознавах с всичко, за което разказах в нея.

P.S. В нашия маркетплейс има образ Docker, който може да бъде инсталиран с един клик. Можете да проверите работата на контейнерите на VPS. Всички нови клиенти получават 3 дни безплатно за тестване.

Уважаеми читатели! Използвате ли технологии CI/CD във вашите проекти?

Създаване на CI/CD верига и автоматизация на работата с Docker

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster