Как стать автором
Обновить

Публикация Vue.js-приложения в GitHub Packages с помощью GitHub Actions для самых маленьких

Время на прочтение30 мин
Количество просмотров8K

В этой серии вы узнаете как собрать докер-образ приложения на Vue.js и как опубликовать его в GitHub Packages. Вот так. Вот в общем-то и... не всё... Одним GitHub Action, как это было для Spring Boot приложения, о котором я рассказывал тут, в этот раз обойтись не получится. Нужно ещё проделать некоторые манипуляции, о которых я и расскажу в данной статье.

Сборка docker-образа

В качестве примера собирать будем вот этот проект. Подробнее о нём я писал в этой статье.

Итак, как же можно собрать докер-образ для приложения на Vue.js?! Обратимся к разработчикам этого фреймворка и поищем ответ на официальном сайте Vue.js. Там нам предлагают создать Dockerfile и в качестве примеров дают содержимое двух докер-файлов.

В первом примере используется простой, не требующий конфигурации, HTTP-сервер.

Пример с простым HTTP-сервером
FROM node:lts-alpine

# устанавливаем простой HTTP-сервер для статики
RUN npm install -g http-server

# делаем каталог 'app' текущим рабочим каталогом
WORKDIR /app

# копируем оба 'package.json' и 'package-lock.json' (если есть)
COPY package*.json ./

# устанавливаем зависимости проекта
RUN npm install

# копируем файлы и каталоги проекта в текущий рабочий каталог (т.е. в каталог 'app')
COPY . .

# собираем приложение для production с минификацией
RUN npm run build

EXPOSE 8080
CMD [ "http-server", "dist" ]

В документации об этом сервере пишут, что он прост и достаточно уязвим для хакерских атак, и рекомендуют использовать более широко известные решения, такие как NGINX или Apache.

Второй пример уже более приближен к реальности, и в нём как раз таки используется выше упомянутый NGINX.

Пример с NGINX
# этап сборки (build stage)
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# этап production (production-stage)
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Единственное, чего не хватает во втором варианте, на мой взгляд, это конфигурации этого самого NGINX. Она будет нужна, если, например, требуется перенаправлять запросы из Vue.js приложения на бэкенд, т.е. проксировать запросы.

Если немного покопаться, то и такой пример можно найти на официальном сайте.

FROM node:latest as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build

FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

В этом Dockerfile используется многоступенчатая сборка докер-образа. Многоступенчатые сборки нужны, чтобы создавать маленькие образы, с минимальным набором пакетов и зависимостей, т.е. только с тем, что нужно для запуска вашего приложения. В данном случае присутствует два этапа сборки. На первом этапе - build-stage - стягивается Node.js и происходит сборка приложения Vue.js для production. На втором этапе - production-stage - происходит уже непосредственно сборка production образа.

Описание содержимого Dockerfile

Здесь, для самых любознательных, приведу более подробное описание того, что происходит в данном Dockerfile.

FROM node:latest as build-stage. Устанавливаем в качестве базового образа (т.е. образа, на основе которого мы, по сути, создаём свой образ для сборки), официальный образ Node.js последней версии. Так же, здесь мы указываем название первого этапа сборки - build-stage, чтобы в дальнейшем можно было обращаться в инструкции COPY к этому этапу по имени. Имена этапов можно не указывать, и тогда к ним можно обращаться с помощью цифр (к первой инструкции FROM можно будет обратиться с помощью 0 и т.д.).

WORKDIR /app. Устанавливаем директорию app текущей рабочей директорией, и инструкции COPY, ADD, RUN, CMD и ENTRYPOINT, идущие за WORKDIR, уже будут работать с этой директорией. Если директория отсутствует, то WORKDIR создаст её.

COPY package*.json ./. Копируем package.json и package-lock.json из текущего локального контекста сборки в текущую рабочую директорию, которая была задана инструкцией WORKDIR, т.е. в данном случае это будет /app. Текущий локальный контекст сборки задаётся в команде docker build. Например, в команде docker build . текущий локальный контекст сборки задаётся с помощью ., что означает текущую директорию, в которой была запущена команда.

RUN npm install. Выполняем команду npm install, чтобы установить зависимости проекта (а также добавляем новый слой в докер-образ).

COPY ./ .. Копируем все файлы и директории из текущего локального контекста сборки в текущую рабочую директорию. Насколько я понимаю ./ и . - это одно и то же и означает текущую директорию; пишут по-разному, вероятно, чтобы подчеркнуть, что это именно директория (либо невнимательность и копипаста). Если я ошибаюсь и различие всё-таки есть - пишите об этом в комментариях.
Внимательный читатель (хотя, наверное, и невнимательный тоже) может заметить что зачем-то до этого отдельно были скопированы package.json и package-lock.json, а теперь копируется вообще всё, включая и эти файлы. Это нужно, чтобы закешировать установку зависимостей, которая происходит командой npm install, с помощью слоёв докера. Таким образом, если зависимости не будут меняться, а точнее не будут меняться файлы package.json и package-lock.json, то установка зависимостей не будет происходить, а сразу возьмётся из кеша. Это может существенно сократить время сборки, т.к. установка зависимостей может быть значительно долгой. Если этого не сделать, а просто сразу скопировать весь проект, а затем вызвать npm install, то при изменении любых файлов в проекте будет происходить установка зависимостей. Подробнее об этом можно почитать тут.

RUN npm run build. Запускает скрипт build из package.json. В данном случае это будетvue-cli-service build. Данная команда соберёт приложение для production с минификацией в директории /app/dist.

FROM nginx as production-stage. Здесь начинается второй этап сборки - production-stage. В качестве базового образа берётся официальный образ NGINX последней версии (т.к. тег явно не указан, то по умолчанию будет использоваться latest).

RUN mkdir /app. Создаём директорию /app.

COPY --from=build-stage /app/dist /app. С помощью --from устанавливаем в качестве исходного местоположения предыдущий этап сборкиbuild-stage и копируем файлы собранного приложения в production образ.

COPY nginx.conf /etc/nginx/nginx.conf. Копируем конфиг для NGINX в production образ.

Попробуем собрать данный докер-образ. Для этого необходимо поместить Dockerfile в корне проекта на Vue.js и выполнить команду docker build.

docker build -t list-keep-front .

В данной команде в качестве имени докер-образа мы передали list-keep-front, а в качестве текущего локального контекста задали текущую директорию ., в которой и должен находится Dockerfile.

Итак запускаем команду и... ничего не работает (по крайней мере на момент написания статьи). Сыпется какая-то ошибка. Хотя, вроде бы, всё сделано чётко по инструкции...
Вы всё сделали по инструкции и у вас ничего не работает?! Что ж, добро пожаловать в увлекательный мир разработки!

Пример ошибки при сборке Dockerfile
 => ERROR [build-stage 6/6] RUN npm run build                                                                                                                                                            4.2s
------
 > [build-stage 6/6] RUN npm run build:
#15 0.782
#15 0.782 > list-keep-front@0.1.0 build
#15 0.782 > vue-cli-service build
#15 0.782
#15 1.284
#15 1.285 -  Building for production...
#15 2.300 Error: error:0308010C:digital envelope routines::unsupported
#15 2.300     at new Hash (node:internal/crypto/hash:67:19)
#15 2.300     at Object.createHash (node:crypto:130:10)
#15 2.300     at module.exports (/app/node_modules/webpack/lib/util/createHash.js:135:53)
#15 2.300     at NormalModule._initBuildHash (/app/node_modules/webpack/lib/NormalModule.js:417:16)
#15 2.300     at handleParseError (/app/node_modules/webpack/lib/NormalModule.js:471:10)
#15 2.300     at /app/node_modules/webpack/lib/NormalModule.js:503:5
#15 2.300     at /app/node_modules/webpack/lib/NormalModule.js:358:12
#15 2.300     at /app/node_modules/loader-runner/lib/LoaderRunner.js:373:3
#15 2.300     at iterateNormalLoaders (/app/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
#15 2.300     at iterateNormalLoaders (/app/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
#15 2.300     at /app/node_modules/loader-runner/lib/LoaderRunner.js:236:3
#15 2.300     at runSyncOrAsync (/app/node_modules/loader-runner/lib/LoaderRunner.js:130:11)
#15 2.300     at iterateNormalLoaders (/app/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
#15 2.300     at Array.<anonymous> (/app/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
#15 2.300     at Storage.finished (/app/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:55:16)
#15 2.300     at /app/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:91:9
#15 4.161 /app/node_modules/loader-runner/lib/LoaderRunner.js:114
#15 4.161                       throw e;
#15 4.161                       ^
#15 4.161
#15 4.161 Error: error:0308010C:digital envelope routines::unsupported
#15 4.161     at new Hash (node:internal/crypto/hash:67:19)
#15 4.161     at Object.createHash (node:crypto:130:10)
#15 4.161     at module.exports (/app/node_modules/webpack/lib/util/createHash.js:135:53)
#15 4.161     at NormalModule._initBuildHash (/app/node_modules/webpack/lib/NormalModule.js:417:16)
#15 4.161     at handleParseError (/app/node_modules/webpack/lib/NormalModule.js:471:10)
#15 4.161     at /app/node_modules/webpack/lib/NormalModule.js:503:5
#15 4.161     at /app/node_modules/webpack/lib/NormalModule.js:358:12
#15 4.161     at /app/node_modules/loader-runner/lib/LoaderRunner.js:373:3
#15 4.161     at iterateNormalLoaders (/app/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
#15 4.161     at iterateNormalLoaders (/app/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
#15 4.161     at /app/node_modules/loader-runner/lib/LoaderRunner.js:236:3
#15 4.161     at context.callback (/app/node_modules/loader-runner/lib/LoaderRunner.js:111:13)
#15 4.161     at /app/node_modules/cache-loader/dist/index.js:147:7
#15 4.161     at /app/node_modules/graceful-fs/graceful-fs.js:61:14
#15 4.161     at FSReqCallback.oncomplete (node:fs:188:23) {
#15 4.161   opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
#15 4.161   library: 'digital envelope routines',
#15 4.161   reason: 'unsupported',
#15 4.161   code: 'ERR_OSSL_EVP_UNSUPPORTED'
#15 4.161 }
#15 4.161
#15 4.161 Node.js v17.3.0
------
executor failed running [/bin/sh -c npm run build]: exit code: 1

Внимательный читатель заметит, что мы копируем конфиг NGINX, который ещё не создали. Может быть всё дело в этом?! На самом деле нет. Если посмотреть логи с ошибкой, то можно увидеть, что всё валится на команде RUN npm run build, которая идёт задолго до копирования несуществующего конфига. Итак, что же делать? Как быть? Идём к нашему лучшему другу Google и ищем ответы на вопросы там. И узнаём, что это связано с проблемами совместимости Webpack (который используется во Vue.js) и Node.js 17, а именно какие-то команды для OpenSSL, которые использует Webpack, теперь не поддерживаются в Node.js. На сколько я понял, в 5-й версии Webpack эту проблему решили, но в 4-й решили не править. А Vue.js на текущий момент, даже в последних версиях, если я не ошибаюсь, использует Webpack 4, поэтому даже обновление на самую последнюю версию Vue.js, вероятно, не поможет (но я не пробовал).

Итак, как же в интернетах рекомендуют решать данную проблему? Один из вариантов - использовать более старую версию Node.js, например, 16. Но у меня этот вариант не заработал, я получал всё ту же ошибку. Возможно, это связано с тем, что, в связи с не так давно обнаруженной уязвимостью log4j, все докер-образы попересобрали и в итоге в 16-ой версии чего-то повыпиливали. Ещё один вариант, которым я и воспользовался - это установка для переменной окружения NODE_OPTIONS значения --openssl-legacy-provider, в результате чего будет использоваться устаревший OpenSSL провайдер, в котором будут команды, необходимые для Webpack. Сделать это можно, добавив в Dockerfile строку ENV NODE_OPTIONS=--openssl-legacy-provider.

Итоговый Dockerfile
FROM node:latest as build-stage
ENV NODE_OPTIONS=--openssl-legacy-provider
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build

FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

Конфигурация NGINX

Пример конфигурации NGINX можно взять с официального сайта Vue.js из того же примера, из которого мы брали пример Dockerfile. Итого, для конфигурации NGINX нам нужно создать в корне проекта на Vue.js файл nginx.conf со следующим содержимым.

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
  worker_connections  1024;
}
http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
  access_log  /var/log/nginx/access.log  main;
  sendfile        on;
  keepalive_timeout  65;
  server {
    listen       80;
    server_name  localhost;
    location / {
      root   /app;
      index  index.html;
      try_files $uri $uri/ /index.html;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root   /usr/share/nginx/html;
    }
  }
}

Данный конфиг обеспечивает запуск нашего приложения на порту 80 докер-образа и отображение http-запроса / на файл index.html. Т.е. при запросе / будет отдаваться, по сути, наше приложение на Vue.js.

Описание содержимого nginx.conf

Здесь, опять же, для самых любознательных, приведу более подробное описание того, что происходит в данном nginx.conf.

Конфиг NGINX представляет из себя, по сути, набор директив. Директивы бывают простые и блочные. Простая директива состоит из имени и параметров, разделённых пробелами, и оканчивается точкой с запятой. Блочная директива устроена так же, как и простая директива, но вместо точки с запятой после имени и параметров следует набор других директив, помещённых внутри фигурных скобок.

Итак, перейдём непосредственно к описанию нашего конфига.

user nginx. Пользователь, под которым будет запускаться NGINX в официальном докер-образе (без привилегий root).

worker_processes 1. Число рабочих процессов. Оптимальное значение зависит от множества факторов, включая число процессорных ядер, число жёстких дисков и т.п. В документации рекомендуют при затруднении в выборе правильного значения начать с установки его равным числу процессорных ядер. Также можно выставить значение auto для автоматического определения количества доступных процессорных ядер.

error_log /var/log/nginx/error.log warn. Конфигурация записи логов ошибок. Первый параметр - /var/log/nginx/error.log - задаёт файл, в котором будут хранится логи. В нём будут хранится все ошибки, которые произошли во время работы NGIINX. Второй параметр - warn - уровень логирования.

pid /var/run/nginx.pid. Задаёт файл, в котором будет храниться номер главного процесса.

events. В данной блочной директиве задаётся конфигурация, влияющая на обработку соединений.

worker_connections 1024. Максимальное число соединений, которые одновременно может открыть рабочий процесс. Следует иметь в виду, что в это число входят все соединения, в том числе, например, соединения с проксируемыми серверами, а не только соединения с клиентами.

http. В данной блочной директиве задаётся конфигурация HTTP-сервера.

include /etc/nginx/mime.types. Включает в конфигурацию файл mime.types, в котором находится достаточно полная таблица соответствий расширений имён файлов и MIME-типов ответов.

default_type application/octet-stream. Задаёт MIME-тип ответов по умолчанию.

log_format main '...'. Задаёт формат логов, гдеmain - название лога, а затем идёт строка самого лога. В этой строке можно использовать переменные. Если значение переменной не будет найдено, то в качестве значения в лог будет записываться символ -. Ниже перечислены переменные, которые используются в представленном формате логов.

  • $remote_addr - адрес клиента.

  • $remote_user - имя пользователя, использованное в Basic аутентификации.

  • $time_local - локальное время в Common Log Format.

  • $request - первоначальная строка запроса целиком.

  • $status - статус ответа.

  • $body_bytes_sent - число байт, переданное клиенту, без учёта заголовка ответа.

  • $http_имя - произвольный заголовок запроса. Последняя часть имени переменной соответствует имени заголовка, приведённому к нижнему регистру, с заменой символов тире на символы подчёркивания. В данном случае это относится к переменным $http_referer, $http_user_agent и $http_x_forwarded_for.

Пример лога в данном формате представлен ниже.

172.17.0.1 - - [08/Jan/2022:19:53:06 +0000] "GET /js/app.18156e39.js HTTP/1.1" 200 6365 "http://localhost:8081/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0
.4664.110 Safari/537.36" "-"

access_log /var/log/nginx/access.log main. Задаёт путь, формат и настройки записи логов доступа. В логи доступа записываются данные о запросах пользователей. В данном случае в качестве пути указан /var/log/nginx/access.log, а в качестве формата - main, который был определён ранее через log_format.

Кстати говоря, если вы вдруг захотите посмотреть логи в докер-образе и попробуете открыть файл /var/log/nginx/access.log или /var/log/nginx/error.log, то обнаружите, что эти файлы будут всегда пусты, но при этом можно заметить, что логи, которые должны писаться в эти файлы, пишутся в консоль. Происходит это потому, что логи запросов и ошибок пересылаются в сборщик логов docker. Делается это с помощью создания символических ссылок с /var/log/nginx/access.log на /dev/stdout и с /var/log/nginx/error.log на /dev/stderr. В Dockerfile, с помощью которого собирается официальный образ для NGINX, это реализовано следующим образом:

# forward request and error logs to docker log collector
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log \

sendfile on. Разрешает или запрещает использовать sendfile(). sendfile() является системным вызовом и влияет на производительность. В статьях по оптимизации работы NGINX рекомендуют включать данный параметр для повышения производительности, если NGINX будет раздавать локально расположенные статические файлы. Если же NGINX, например, используется как обратный прокси для выдачи страниц с сервера приложений, то sendfile() не будет использоваться, и этот параметр можно выключить.

keepalive_timeout 65. Задаёт таймаут, в течение которого keep-alive соединение с клиентом не будет закрыто со стороны сервера. Значение 0 запрещает keep-alive соединения с клиентами.

server. В данной блочной директиве задаётся конфигурация виртуального сервера. Виртуальных серверов может быть несколько.

listen 80. Задаёт адрес и порт, на которых сервер будет принимать запросы. Можно указать адрес и порт, либо только адрес или только порт. Также здесь можно выставить параметр default_server. Если у директивы есть параметр default_server, то сервер, в котором описана эта директива, будет сервером по умолчанию для указанной пары адрес:порт. Если же директив с параметром default_server нет, то сервером по умолчанию будет первый сервер, в котором описана пара адрес:порт.

server_name localhost. Задаёт имена виртуального сервера. Имена определяют, в каком блоке server будет обрабатываться запрос. Определяется это путём проверки заголовка запроса Host на соответствие именам сервера по определённым правилам. Если значение заголовка Host не соответствует ни одному из имён серверов или в заголовке запроса нет этого поля вовсе, NGINX направит запрос в сервер по умолчанию для этого порта. В нашем случае у нас только один сервер localhost, который и является сервером по умолчанию для порта 80.

location /. Данная блочная директива устанавливает конфигурацию в зависимости от URI запроса. location можно задать префиксной строкой или регулярным выражением. Регулярные выражения задаются либо с модификатором ~* для поиска совпадения без учёта регистра символов, либо с модификатором ~ для поиска с учётом регистра. Чтобы найти location, соответствующий запросу, вначале проверяются префиксные location’ы. Среди них ищется location с совпадающим префиксом максимальной длины и запоминается. Затем проверяются регулярные выражения в порядке их следования в конфигурационном файле. Проверка регулярных выражений прекращается после первого совпадения, и используется соответствующая конфигурация. Если совпадение с регулярным выражением не найдено, то используется конфигурация запомненного ранее префиксного location’а. Кроме того, с помощью модификатора = можно задать точное совпадение URI и location. При точном совпадении поиск сразу же прекращается. В данном случае используется префиксный location , который соответствует, в общем-то, всем запросам к серверу.

root /app. Задаёт корневую директорию для запросов. В данном случае это директория /app, в которую мы положили наше приложение на Vue.js.

index index.html. Определяет файлы, которые будут использоваться в качестве индекса. В данном случае мы будет отдавать index.html нашего приложения.

try_files $uri $uri/ /index.html. Проверяет существование файлов в заданном порядке и использует для обработки запроса первый найденный файл, причём обработка делается в контексте этого же location’а. Переменная $uri - это текущий URI запроса. С помощью слэша в конце имени можно проверить существование директории, например, $uri/. Т.е. в данном случае с помощью $uri сначала проверяем, существует ли файл с запрошенным именем. Если такой файл есть, то отдаём его, если его нет, то идём дальше. Далее, с помощью $uri/ проверяем, существует ли директория с запрошенным именем. Если да, то отдаём эту директорию, если нет, то идём дальше. При этом, в случае с директорией, будет отдаваться, насколько я понимаю, то, что указно в директиве index. Т.е. в данном случае в переданной директории будет искаться файл index.html (например, если в качестве директории передать /test/, то вернётся /test/index.html). Если такого файла в указаной директории нет (но сама директория при этом есть), то NGINX отдаст страницу с ошибокой 403. И в самом конце, если переданного файла/директории нет, отдаём index.html.

Небольшое отступление. Если при запуске докер-контенера NGINX порт 80, который обслуживает NGINX внутри конейтнера, связать с каким-то другим портом хоста, то можно заметить, что при обращении к директориям, которые есть внутри директории /app (которая, напомню, указана как корневая директорией для запросов), без / на конце запроса, будет происходить редирект на localhost без указания порта.

Например, для того чтобы запустить контейнер с именем list-keep-front из образа list-keep-front и связать порт 80, который обслуживает NGINX, с портом 8081 хоста, можно выполнить следующую команду:

docker run -it -p 8081:80 --rm --name list-keep-front list-keep-front

Флаг --rm автоматически удаляет контейнер после его остановки. -it - это совмещённые флаги -i и -t. С помощью -i STDIN поддерживается в открытом состоянии, даже если контейнер не подключён к STDIN. С помощью -t выделяется псевдотерминал, который соединяет используемый терминал с потоками STDIN и STDOUT контейнера. Т.е., по сути, -it нужен чтобы получить возможность взаимодействия с контейнером через терминал.

И теперь, если обратится, например, по этому адресу http://localhost:8081/js, то произойдёт редирект на http://localhost/js/. И т.к. наш контейнер запущен на порте 8081, то запрос по адресу http://localhost/js/ попросту некому обработать, и поэтому в браузере вы можете увидеть что-то подобное.

Если же в качестве адреса указать http://localhost:8081/js/ (со / на конце запроса), то данный запрос обработается и в ответ вернётся 403, т.к. файла index.html в директории /js/ нет.

Может показаться, что проблема заключается в том, что в самом контейнере NGINX обслуживает порт 80 и ничего не знает о порте, который вы указали при запуске контейнера. Правда, можно заметить, что в адресе, на который произошёл редирект, вообще не указан никакой порт, хотя в контейнере обслуживается порт 80. Но можно предположить, что это связано с тем, что порт 80 является портом по умолчанию для HTTP и может явно не указываться. И если в адресе он не указан, то обращение будет всё равно идти на этот порт, т.е., например, адрес http://localhost/ эквивалентен http://localhost:80/. И кажется, что достаточно изменить порт, который обслуживает NGINX, на порт который нам нужен с помощью listen 8081 и связать этот порт с портом 8081 нашего хоста. Если это сделать, то ничего не изменится и при обращении на http://localhost:8081/js нас будет всё так же редиректить на http://localhost/js/ без указания порта.

В общем, о данной проблеме и о возможных методах её решениях можно почитать тут. Если коротко, то проблема заключается в try_files, а точнее в параметре $uri/. И для решения этой проблемы нужно по-другому переписать try_files. Как вариант, можно просто убрать $uri/ из try_files и оставить только try_files $uri /index.html, т.е. мы будем отдавать либо конкретный файл $uri, либо /index.html. Т.к. всё приложение на Vue.js., по сути, находится в /index.html, то, думаю, этого будет достаточно. Также данную проблему можно решить, просто связав порт 80 внутри контейнера с портом 80 хоста.

docker run -it -p 80:80 --rm --name list-keep-front list-keep-front

Итак, вернёмся к описанию конфига.

error_page 500 502 503 504 /50x.html. Задаёт URI, который будет показываться для указанных ошибок, которые произошли во время работы NGINX.

location = /50x.html { root /usr/share/nginx/html; }. Устанавливает конфигурацию, согласно которой в ответ на запрос /50x.html будет отдаваться файл /usr/share/nginx/html/50x.html.

В принципе, в докер-образе NGINX уже лежит nginx.conf, и если хочется, то можно писать свой конфиг на его основе.

Дефолтовый конфиг из официального докер-образа NGINX

Конфиг в докер-образе NGINX состоит из двух файлов. Первый - это /etc/nginx/nginx.conf.

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

А второй - /etc/nginx/conf.d/default.conf, который подключается в /etc/nginx/nginx.conf с помощью include.

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

NGINX, кроме помощи в обслуживании самого Vue.js-приложения, может помочь нам в проксировании запросов, если это требуется. Например, в рассматриваемом приложении нужно проксировать на бэкенд все запросы, которые начинаются с /api/. При локальном запуске это делается с помощью devServer в конфиге vue.config.js.

module.exports = {
  devServer: {
    port: 8081,
    proxy: {
      '^/api/': {
        target: 'http://localhost:8082'
      }
    }
  }
}

Чтобы сделать проксирование через NGINX, необходимо в блок server добавить следующее:

    location /api/ {
      proxy_pass http://host.docker.internal:8082;
    }

С помощью proxy_pass задаётся адрес проксируемого сервера. Бэкенд локально запущен на порту 8082. В качестве адреса бэкенда указан host.docker.internal. Данный адрес можно использовать (на Windows) для подключения из контейнера к хосту. Этот адрес предназначен только для разработки и не нужно его использовать в production. В данном случае я его использую, только чтобы убедиться, что локально всё работает корректно.

Итоговый nginx.conf
user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
  worker_connections  1024;
}
http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
  access_log  /var/log/nginx/access.log  main;
  sendfile        on;
  keepalive_timeout  65;
  server {
    listen       80;
    server_name  localhost;
    location / {
      root   /app;
      index  index.html;
      try_files $uri $uri/ /index.html;
    }
    location /api/ {
      proxy_pass http://host.docker.internal:8082;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root   /usr/share/nginx/html;
    }
  }
}

Переменные окружения

Если в приложении используются переменные окружения, нужно добавить их в production сборку. Переменные окружения можно указать в специальных файлах в корне проекта:

.env                # загружается во всех случаях
.env.local          # загружается во всех случаях, игнорируется git
.env.[mode]         # загружается только в указанном режиме работы
.env.[mode].local   # загружается только в указанном режиме работы, игнорируется git

Для локальной сборки в корне проекта у меня лежит файл .env.local, который игнорируется git, со следующим содержимым:

VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth

Здесь указан адрес Keycloak, в который ходит приложение на Vue.js при локальном запуске.

Итак, для того чтобы добавить переменные окружения в production сборку, нам нужно понять, как правильно назвать файл .env.[mode]. Т.е. нам нужно узнать название нашего режима работы приложения. Что ж, обратимся к документации. Там сказано, что при сборке приложения командой vue-cli-service build, которую, напомню, мы и используем, используется режим работы production. Т.е. файл должен называться .env.production. Ниже приведено его содержимое.

VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth

В данном случае оно совпадает с содержимым .env.local, чтобы можно было локально проверить работоспособность приложения. Здесь не нужно использовать host.docker.internal, потому что запросы в Keycloak будут выполняться из js-кода (а точнее из либы keycloak-js) и не будут идти через NGINX. Т.е. они будут выполняться в локальной сети, где доступен наш localhost.

Сборка и публикация через GitHub Actions

Итак, теперь можно приступить к созданию GitHub Action. Для этого в GitHub-репозитории необходимо перейти на вкладку Actions.

На этой странице можно либо самостоятельно создать workflow,  либо выбрать из предложенных и видоизменить его требуемым образом. Итак, наш action должен уметь собирать докер-образ и публиковать его GitHub Packages. Для этих целей вполне подходит Publish Docker Container.

Нажимаем на Configure и попадаем на страницу создания нового action для проекта, на которой уже будет доступен код, который можно редактировать.

По умолчанию файл будет называться docker-publish.yml. Т.к. данный action будет заниматься сборкой проекта, предлагаю переименовать его в build.yml. Ниже представлен полный код данного action.

name: Docker

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

on:
  schedule:
    - cron: '44 21 * * *'
  push:
    branches: [ main ]
    # Publish semver tags as releases.
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ main ]

env:
  # Use docker.io for Docker Hub if empty
  REGISTRY: ghcr.io
  # github.repository as <account>/<repo>
  IMAGE_NAME: ${{ github.repository }}


jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      # This is used to complete the identity challenge
      # with sigstore/fulcio when running outside of PRs.
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      # Install the cosign tool except on PR
      # https://github.com/sigstore/cosign-installer
      - name: Install cosign
        if: github.event_name != 'pull_request'
        uses: sigstore/cosign-installer@1e95c1de343b5b0c23352d6417ee3e48d5bcd422
        with:
          cosign-release: 'v1.4.0'


      # Workaround: https://github.com/docker/build-push-action/issues/461
      - name: Setup Docker buildx
        uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

      # Login against a Docker registry except on PR
      # https://github.com/docker/login-action
      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Extract metadata (tags, labels) for Docker
      # https://github.com/docker/metadata-action
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      # Build and push Docker image with Buildx (don't push on PR)
      # https://github.com/docker/build-push-action
      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      # Sign the resulting Docker image digest except on PRs.
      # This will only write to the public Rekor transparency log when the Docker
      # repository is public to avoid leaking data.  If you would like to publish
      # transparency data even for private images, pass --force to cosign below.
      # https://github.com/sigstore/cosign
      - name: Sign the published Docker image
        if: ${{ github.event_name != 'pull_request' }}
        env:
          COSIGN_EXPERIMENTAL: "true"
        # This step uses the identity token to provision an ephemeral certificate
        # against the sigstore community Fulcio instance.
        run: cosign sign ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}

Данный action будет собирать и публиковать докер-образы: по указанному крону, при изменении ветки main и при создании тега с именем, соответствующим паттерну v*.*.*, а также прогонять сборку при создании Pull Request в ветку main. Кроме того, в данном action происходит подпись докер-образов с помощью cosign и сохранение этой подписи в GitHub Packages.

Описание содержимого сгенерированного docker-publish.yml

И опять же, для самых любознательных, приведу более подробный разбор автоматически сгенерированного файла docker-publish.yml.

name: Docker. Имя workflow будет отображаться на страничке Actions в GitHub.

# This workflow... Всё, что идёт после символа #, и находится на той же строке что и сам символ #, считается комментарием.

on описывает, какие события должны запускать данный workflow.

schedule. Запускает workflow по расписанию для дефолтовой ветки. По умолчанию это main. Время запуска можно настроить с помощью cron, используя POSIX cron синтаксис. Время указывается по UTC. Можно задавать несколько cron. В данном случае cron: '44 21 * * *' означает что workflow будет запускаться каждый день в 21:44 UTC.

push. Запускает workflow, когда происходит пуш коммита или тега. С помощью фильтра branches задаётся запуск workflow на определённых ветках. В данном случае branches: [ main ] устанавливает запуск workflow, когда происходит пуш в ветку main. Фильтр tags задаёт запуск workflow для определённых тегов. В данном случае tags: [ 'v*.*.*' ] устанавливает запуск workflow, когда присходит пуш тега, имя которого соответствует шаблону v*.*.*, т.е. соответствует спецификации SemVer.

pull_request. Запускает workflow, когда происходит создание Pull Request. В данном случае фильтр branches: [ main ] устанавливает запуск workflow, когда создаётсяPull Request в ветку main.

env. В этом блоке объявляются переменные окружения, которые можно использовать в своём workflow. В данном случае в качестве REGISTRY задаётся ghcr.io - адрес GitHub Packages. А в качестве IMAGE_NAME задаётся значение ${{ github.repository }}. Значение берётся из github-контекста и содержит <имя владельца>/<имя репозитория>. Для получения доступа к контексту используется синтаксис выражения - ${{ <expression> }}.

jobs. В этом блоке описываются job'ы (например, сборка, тестирование и т.п.). Их может быть несколько. По умолчанию они будут запускаться параллельно, но документация говорит, что можно запустить их последовательно. В данном случае есть лишь одна job, которая называется build.

runs-on: ubuntu-latest. В этом блоке указывается runner, т.е., по сути, виртуальная машина, на которой будет выполняться job. В данном случае это ubuntu-latest.

permissions. Позволяет изменить набор прав доступа, установленных по умолчанию для GITHUB_TOKEN. Этот токен создаётся в автоматическом режиме в начале каждого запуска workflow, и он доступен как секрет secrets.GITHUB_TOKEN, либо его можно получить из github-контекста как github.token. Разрешениеcontents: read нужно для получения кода из репозитория, packages: write - для публикации в GitHub Packages, а id-token: write - для возможности получения OIDC токена. В данном случае OIDC токен нужен для sigstore/fulcio, которая используется при формировании подписи для докер-образов.

steps. Блок, в котором описаны шаги, из которых состоит job. Каждый шаг является отдельным GitHub Action или shell-командой.

Далее рассмотрим шаги данного GitHub Action.

      - name: Checkout repository
        uses: actions/checkout@v2

С помощью name задаётся имя шага. Оно будет отображаться в логах выполнения job на GitHub.

С помощью uses можно выполнить указанный GitHub Action. В данном случае - это actions/checkout@v2, который вытягивает код из репозитория в runner.

      - name: Install cosign
        if: github.event_name != 'pull_request'
        uses: sigstore/cosign-installer@1e95c1de343b5b0c23352d6417ee3e48d5bcd422
        with:
          cosign-release: 'v1.4.0'

Этот шаг не выполняется при создании Pull Request. Это достигается с помощью if. В качестве условия в if на данном шаге передано github.event_name != 'pull_request'. Значение свойства github.event_name берётся из github-контекста и в нём содержится имя события, которое инициировало запуск workflow. Данный шаг устанавливает cosign в наш runner с помощью GitHub Action sigstore/cosign-installer. Это действие позволяет подписывать докер-образы с помощью cosign. В качестве версии GitHub Action здесь используется хеш коммита SHA -1e95c1de343b5b0c23352d6417ee3e48d5bcd422. С помощью with передаются параметры дляGitHub Action. В данном случае с помощью cosign-release задаётся версия cosign.

      - name: Setup Docker buildx
        uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

С помощью docker/setup-buildx-action на этом шаге происходит установка и настройка Docker Buildx. buildx - это плагин Docker CLI для расширенных возможностей сборки с помощью BuildKit. Например, с его помощью можно собирать мультиплатформенные образы.

      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

С помощью docker/login-action на этом шаге происходит логин в Docker Registry. Этот шаг не выполняется при создании Pull Request.

С помощью параметра registry задаётся адрес Docker Registry. В данном случае это ghcr.io - адрес GitHub Packages, который берётся из переменной окружения env.REGISTRY. По умолчанию, если адрес не будет установлен, будет использоваться адрес Docker Hub.

С помощью username задаётся имя пользователя для логина в Docker Registry. В данном случае оно берётся из свойства github-контекста github.actor, где содержится имя пользователя, который инициировал запуск workflow.

С помощью password задаётся пароль или токен личного доступа для логина в Docker Registry. В данном случае это GITHUB_TOKEN, который создаётся в автоматическом режиме в начале каждого запуска workflow.

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

GitHub Action docker/metadata-action позволяет извлекать метаданные из ссылок Git и событий GitHub, например такие, как теги (--tag) и метки (--label) для докера. С помощью параметра images задаётся список докер-образов, которые будут использоваться в качестве их имён. Если указать несколько имён, то, соответственно, на выходе будет получено несколького тегов. В данном случае с помощью переменных окружения${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} задаётся одно значение - ghcr.io/vanbv/list-keep-front. Также для данного шага задаётся id meta - уникальный идентификатор шага, с помощью которого можно обратиться к контексту шага. Например, с помощью этого контекста можно обратиться к выходным параметрам шага следующим образом: ${{ steps.meta.outputs }} .

Помимо явно указанных параметров есть ещё несколько важных, для которых в данном случае используются значения по умолчанию. Это параметры labels, tags и flavor.

По умолчанию в labels будут генерироваться метки в соответствии со спецификацией формата образов OCI на основе данных из вашего репозитория (например, таких как название проекта, лицензия и пр.). Если вам это не подходит, то с помощью данного параметра можно эти метки переопределить. Пример того, как это может выглядеть по умолчанию, представлен ниже.

  org.opencontainers.image.title=list-keep-front
  org.opencontainers.image.description=
  org.opencontainers.image.url=https://github.com/vanbv/list-keep-front
  org.opencontainers.image.source=https://github.com/vanbv/list-keep-front
  org.opencontainers.image.version=v1.0.0
  org.opencontainers.image.created=2022-01-22T09:31:27.670Z
  org.opencontainers.image.revision=3e3e0ee1aeb21aae68e76865937c2b78383aa1be
  org.opencontainers.image.licenses=

tags - это основной параметр этого GitHub Action, потому что от него зависит, как будут выглядеть выходные мета-данные. По умолчанию, если этот параметр не задать, он будет установлен следующим значением:

tags: |
  type=schedule
  type=ref,event=branch
  type=ref,event=tag
  type=ref,event=pr

Значение данного параметра представляет из себя список записей (строк), в свою очередь каждая запись представляет из себя список пар ключ-значение в формате CSV (пары разделены между собой запятыми). Формат каждой записи, а именно возможные атрибуты для этой записи, определяется с помощью type. Рассмотрим подробнее записи, которые определены по умолчанию.

type=schedule - запускается во время события schedule. Для данного типа записи можно задать атрибут pattern, в котором с помощью Handlebars можно задавать паттерн для тега, получаемого на выходе. Если этот параметр не задан, то его значение по умолчанию будет nightly. Т.е. во время события schedule, в данном случае, на выходе мы получим имя тега - nightly.

type=ref - обрабатывает события branch, tag, pr. Пример обработки событий представлен в таблице ниже.

Event

Ref

Output

pull_request

refs/pull/2/merge

pr-2

push

refs/heads/master

master

push

refs/heads/my/branch

my-branch

push tag

refs/tags/v1.2.3

v1.2.3

push tag

refs/tags/v2.0.8-beta.67

v2.0.8-beta.67

На самом деле это не совсем те события, которые используются для описания запуска workflow, - это события docker/metadata-action. Поэтому, например, для события schedule, которое запускает workflow по расписанию, будет подходить не только запись type=schedule, но и type=ref,event=branch. В результате чего в нашем конкретном примере на выходе мы получим два тега: один тегnightly, по type=schedule и один тег main, по type=ref,event=branch.

Параметр flavor определяет глобальное поведение для параметра tags. Например, с помощью него можно глобально определить префиксы и суффиксы для тегов с помощью соответствующих атрибутов prefix и suffix. Также у данного параметра присутствует атрибут latest, который имеет значение по умолчанию auto. Этот параметр отвечает за то, будет ли на выходе тег latest. Значение auto означает, что тег latest будет, если есть совпадение хотя бы по одной из следующих записей:

Запись type=ref,event=tag соответствует событию push tag. type=semver также соответствует событию push tag, но дополнительно тег должен соответствовать спецификации SemVer. type=match тоже соответствует событию push tag, но дополнительно тег проверяется на соответствие регулярному выражению в атрибуте pattern, который в данном случае не задан и, соответственно, этой записи не будет соответствовать ни один тег. Т.е. получается, что, в общем-то, тег latest будет при создании вообще любого тега. Правда, в самом начале нашего workflow было указано, что его нужно запускать только на теги git соответствующие спецификации SemVer, поэтому на другие теги наш workflow в принципе и не запустится.

      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

С помощью docker/build-push-action на этом шаге происходит сборка и публикация докер-образов в GitHub Packages. Для данного шага задаётся id build-and-push, с помощью которого можно обратиться к контексту шага.

С помощью параметра context в качестве контекста сборки задаётся текущая директория ., в которой была запущена команда сборки.

С помощью параметра push можно задать, нужно ли автоматически публиковать результат сборки докер-образов в Docker Registry. В данном случае публикация не будет происходить при создании Pull Request(при этом сборка происходить будет).

С помощью tags задаётся список тегов. В данном случае с помощью ${{ steps.meta.outputs.tags }} он берётся из выходных параметров предыдущего шага с id meta.

С помощью labels задаётся список мета-данных для образа. Этот список формируется из выходных параметров предыдущего шага meta с помощью ${{ steps.meta.outputs.labels }}.

     - name: Sign the published Docker image
        if: ${{ github.event_name != 'pull_request' }}
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: cosign sign ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}

На этом шаге происходит подпись докер-образа с помощью cosign. Этот шаг не выполняется при создании Pull Request. Подпись докер-образов нужна, чтобы можно было проверить их происхождение, т.е., по сути, определить, кем они были опубликованы. Для запуска cosign используется run. run позволяет запускать CLI-приложения.

Разберём подробнее, что делает данная команда. Итак, cosign sign указывает на то, что нужно подписать докер-образ, который задаётся с помощью ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}. В данном случае это будет ghcr.io/vanbv/list-keep-front. Также, с помощью @ после докер-образа, можно задать digest. digest представляет из себя SHA-256 хеш, по которому можно однозначно идентифицировать наш образ. В данном случае digest берется из выходных параметров предыдущего шага build-and-push, а именно из steps.build-and-push.outputs.digest.cosign подпишет этот образ и опубликует эту подпись в GitHub Packages, рядом с нашим собранным образом. Имя подписи будет выглядеть как-то так sha256-61d911ad3a5689dc2ed30a30a0f66be2fb72a59ba1bc41a5cb8516d5b74b7101.sig.

Также здесь задаётся переменная окружения COSIGN_EXPERIMENTAL. С помощью неё включаются эксперементальные функции, например, такие как интеграция с Rekor и создание подписи без ключа с помощью Fulcio.

Предлагаю немного упросить код данного GitHub Action. А именно убрать публикацию по крону, т.к. уже есть публикация на изменение ветки main, и этого, мне кажется, будет достаточно для получения актуальных сборок докер-образов. Также, думаю, можно убрать подпись докер-образов, т.к. собранные докер-образы я планирую использовать исключительно для себя. Кроме того, тогда можно убрать и блок permissions, т.к. прав доступа для GITHUB_TOKEN, которые предоставляются по умолчанию, будет достаточно.

Итоговый build.yml
name: build

on:
  push:
    branches: [ main ]
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}


jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Setup Docker buildx
        uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

После того, как мы запушим наш build.yml произойдёт запуск workflow, по завершении чего мы получим собранный докер-образ, который будет лежать в GitHub Packages. Увидеть, что у нас что-то опубликовалось, можно на странице Code репозитория.

Перейдя на страницу нашего Package, мы сможем увидеть наши опубликованные докер-образы.

Итак, видим, что на изменение ветки main наш workflow работает. Давайте проверим, как он работает на создание тега. Для этого на странице Code нашего репозитория жмём Create a new release.

На странице создания релиза у нас есть возможность создать новый тег. Что нам и нужно для запуска нашего workflow.

Заполняем все требуемые нам поля и жмём Publish release.

После завершения работы нашего workflow на странице наших Packages можно будет увидеть докер-образ с тегом 0.0.1, а так же latest, который опубликовался вместе с ним. Содержимое этих двух докер-образов совпадает.

Помимо рассмотренного способа публикации докер-образов в документации GitHub Packages рассматривается ещё один способ. Его я использовал при сборке Spring Boot приложения и описывал в данной статье.

Заключение

Что ж, вот и подошла к концу моя статья. Надеюсь, информация была вам интересна и полезна и не устарела, пока я писал и редактировал. И что хоть кто-нибудь заглянул, а то и прочитал информацию в спойлерах.

И ещё: берегите там себя.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события