Привет всем! Меня зовут Виталий, я фронтендер в 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, который обновляет версию на сервере.

Теперь все готово — можем поаплодировать и себе и погладить по голове. Пользуйтесь моими советами и алгоритмами, чтобы у вас все было, но вам за это ничего не было.

Комментарии (2)


  1. fanatrstp
    20.10.2023 11:13
    -1

    Отличная статья, всё доходчиво написано, но я ждал статью про человека паука, а теперь какие-то умные слова знаю!


  1. GriazniyASS
    20.10.2023 11:13
    -2

    Шикарная статья,красиво написано. Теперь буду знать как не заплакать