Привет всем! Меня зовут Виталий, я фронтендер в Mish. Решил недавно освоить полноценный автоматический деплой проекта, чтобы все работало само. Расскажу и вам, что из этого получилось.
В статье буду разговаривать о деплое только фронтенда. Про деплой бэкенда расскажу в следующем материале.
Выбираем платформу
Настраивать деплой файлов и переносить их на сервер в определенную папку — как-то несовременно. Поэтому взгляд упал на Docker.
Дальше встал вопрос хостинга. Можно было воспользоваться AWS, но хотелось построить весь процесс с нуля. Поэтому выбрал сторонний сервис, чтобы и домен, и все остальное было свое.
Так как до этого проект лежал на хостинге Sprinthost, а недавно они добавили виртуальные боксы, решил купить его и там же настроить деплой. Не хотелось платить много денег за мощности, поэтому решил собирать приложение за счет GitHub, раз уж они такие добрые.
Теперь выбираем сервер дороже 100 рублей и начинаем думать, как реализовать проект.
Начинаем процесс
Раз мы будем использовать докер, то грех не использовать Docker Hub. Вот как это будет выглядеть:
Если описать эту историю словами, то выглядеть будет так:
Собираем на Git новый image приложения.
Закидываем его в репозиторий Docker Hub.
Обновляем контейнер.
Наслаждаемся победой.
Для начала нам нужно будет определиться с Linux, который будем использовать. Так как Sprinthost предоставляет готовые сборки, выберем Ubuntu + Docker + Pointer. Но если захотите иначе, все это сработает и на CentOS.
Подготавливаем окружение
Создадим Dockerfile для нашего фронтенда:
# syntax=docker/dockerfile:1
FROM node:16.10.0-alpine as build
RUN apk add --no-cache python2 g++ make
WORKDIR /sbh-fe-ts
COPY package*.json .
RUN yarn install
COPY . .
RUN yarn run build
FROM nginx:1.19
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /sbh-fe-ts/build /usr/share/nginx/html
Если вы новичок, то можете еще не знать, что такое nginx.conf и зачем он нужен. Это конфигурационный файл nginx — сервера, который будет отображать нашу страницу по заданному домену. Его задача — лежать внутри проекта. Для этого создаем в корне папку nginx и в нее кладем .CONFIG файл:
http {
server {
listen 80;
server_name {your domain name};
root /usr/share/nginx/html;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
}
}
Подготавливаем репозиторий Docker Hub
Репозитории в Docker Hub есть приватные и публичные. Использовать можно любой. Я не хотел тратить бесплатный приватный слот, поэтому сделал его публичным. Вы можете сделать так, как удобно вам.
Заходим на Docker Hub, регистрируемся, запоминаем пароль и логин — он понадобится в будущем. Теперь создаем репозиторий:
Нажимаем на синюю кнопку, после чего указываем тип репозитория и его название:
Если все прошло успешно, вы увидите надпись login succeeded.
Настраиваем сервис
Как помним из схемы, сначала нужно подтянуть все обновления image из репозитория, потом обновить контейнер. Для этого проще всего создать собственный скрипт, чтобы запускать его потом из-под GitHub Actions.
Создадим отдельного пользователя и дадим необходимые права. Сразу переходим на этого пользователя, чтобы скрипт был ему доступен:
sudo adduser frontend
sudo groupadd docker
sudo usermod -aG docker frontend
sudo usermod -aG sudo frontend
su frontend
Теперь создаем новый файл под названием deploy.sh и пропишем в него следующие команды:
#!/bin/bash
echo "Stop container"
docker stop frontend
docker rm frontend
docker image rm {dockerhub username}/{dockerhub repo name}
echo "Pull image"
docker pull {dockerhub username}/{dockerhub repo name}
echo "Start frontend container"
docker run -p 80:80 --name frontend -d {dockerhub username}/{dockerhub repo name}
echo "Finish deploying!"
В данном случае я назвал пользователя frontend — для прозрачного распределения обязанностей — и выдал ему права доступа к контейнеру Docker, чтобы не было ошибок. Нужно учитывать, что подобный деплой сделает недоступным сервис во время остановки и запуска контейнера. Чтобы всего этого избежать, можно использовать дополнительные возможности, о которых я расскажу в следующих материалах.
Создаем контейнер
Теперь придется немного поработать руками. Собираем локально image и закидываем его на Docker Hub через консоль или Docker Desktop. Первый вариант удобнее:
docker build . -t {docker username}/{docker repo name}
docker push {docker username}/{docker repo name}
Далее переходим в контейнер на сервере и обновляем image:
docker pull {docker username}/{docker repo name}
Теперь создадим контейнер, который будем обновлять. Заодно проверим, как все работает:
docker run -p 80:80 --name frontend -d {docker username}/{dockerhub repo name}
У меня это выглядит вот так:
docker build . -t rstpgod/schoolsolver
docker push rstpgod/schoolsolver
Переходим в консоль сервера и проверяем, все ли работает:
docker pull rstpgod/schoolsolver
docker run -p 80:80 --name frontend -d rstpgod/schoolsolver
Результат перед вами:
Все запустилось с первого раза. Главное — не забудьте выключить nginx, если он запущен на сервере, чтобы не было конфликтов. Это можно сделать такой командой:
sudo systemctl stop nginx
Мы на середине пути — что имеем к этому этапу:
Сборку приложения в докере
Репозиторий на Dockerhub
Работающий контейнер на сервере
Автоматизируем процесс
Для этого используем GitHub Actions.
Заходим в наш репозиторий и задаем все секретные ключи:
Docker username
Docker password
Docker Repo Name
Server ip
Server port (default 22)
Server user login
Server user password
Добавляем их в секретные ключи. В итоге страница будет выглядеть так:
Теперь можем переходить к настройке самого GitHub Action. Идем во вкладку и выбираем Manual Workflow. Задаем туда этот код:
name: Publish to server
on:
push:
branches: [ "master" ]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}
tags: latest
labels: latest
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
server_update:
needs: push_to_registry
name: Update server buy ssh
runs-on: ubuntu-latest
steps:
- name: Connect and run script
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script_stop: true
script: bash deploy.sh
Первая часть job запускает стандартный workflow гита:, он собирает image и пушит его в репозиторий. Здесь же можно настроить различные метки, например, если хотите хранить больше одного image на сервере.
В server_update подключаемся к нашему серверу и запускаем написанный скрипт, чтобы обновить контейнер с приложением. Также указываем script stop, если выпала какая то ошибка на сервере.
Еще указан параметр needs. Он нужен, чтобы программа дождалась предыдущего шага и только потом начала что-то творить на сервере.
Тут же можно настроить билд для pull requst и проверки на сборку, чтобы случайно не залить нерабочую версию.
name: Docker Image CI
on:
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker build . -t pull_request:$(date +%s)
Так мы защищаем главную ветку и триггерим сборку только в тот момент, когда кто-то вносит изменения в основной код. Как только мы объединили две ветки, запускается верхний workflow, который обновляет версию на сервере.
Теперь все готово — можем поаплодировать и себе и погладить по голове. Пользуйтесь моими советами и алгоритмами, чтобы у вас все было, но вам за это ничего не было.