Привет, Хабр! Меня зовут Артём, я разрабатываю фронтенд систем управления сетью в YADRO. С Docker знаком давно и часто его использую. Но, когда столкнулся с задачами, где недостаточно просто скопировать шаблонный Docker-файл и подправить пару строчек, решил больше погрузиться в эту тему. 

В статье я разберу базовые вопросы, связанные с запуском Angular-приложений в контейнерах, — это поможет в дальнейшем справиться с более замысловатыми задачами. Текст будет полезен тем, кто уже что-то знает про Docker, но не углублялся в детали или хочет освежить знания на практике.

Коротко про Docker

Docker давно перестал быть таинственной технологией DevOps-специалистов и используется повсеместно для упрощения развертывания приложений, их масштабирования и управления контейнерами в различных средах — от разработки до продакшена. Зачастую ответственность за корректную работу приложения внутри контейнера и связанные конфигурации лежит на самих разработчиках, поэтому даже фронтендерам надо уметь работать с Docker. 

Если коротко, Docker — это инструмент виртуализации на уровне ОС, который позволяет создать независимое от окружения пространство для развертывания приложений. Также он значительно упрощает разработку, если требуется локально запустить код, требующий серьезной настройки окружения или множества зависимостей. 

А как это касается фронтендеров?

Предположим, у нас есть навороченный бэкенд и начинающий фронтенд-специалист — стажер Анатолий. Ему нужно запустить бэкенд локально, чтобы изменить цвет кнопки в интерфейсе. Без Docker задача непростая: скорее всего, надо поднять базу данных, мигрировать данные, установить определенный язык программирования и необходимые зависимости, а также настроить окружение. Если повезет, Толя поднимет API на своей машине через n лет дней. 

Но если бэкенд завернут в Docker, то ему достаточно будет установить Docker и запустить несколько команд, которые развернут необходимое окружение и запустят приложение в изолированном контейнере. Причем все пройдет корректно вне зависимости от конфигурации рабочей машины Анатолия — при условии, что используемые образы поддерживают необходимую архитектуру CPU. А после окончания работы контейнер можно будет легко «уничтожить». 

Docker и различные процессорные архитектуры

Docker-образы, созданные для x86_64, могут не работать на ARM или ARM64. Это особенно актуально для устройств с процессорами Apple Silicon. Многие базовые образы мультиархитектурны и автоматически подбирают правильную версию образа под архитектуру хоста. Но образ приложения, собранный на основе этих образов, уже не будет мультиархитектурным. Поэтому может появиться потребность пересобирать образ на устройствах с другой архитектурой CPU. 

История стажера Анатолия позволяет нам выделить ключевые и достаточно очевидные плюсы использования Docker:

  • Безопасность и устойчивость. Контейнер изолирован от основной системы, и, даже если мы сломали что-то внутри (например, снесли базу данных), для восстановления работы достаточно просто его перезапустить.

  • Экономия памяти. Мы не расходуем ресурсы системы на установку десятка языков программирования и т.д. Сами контейнеры, конечно, расходуют ресурсы, но удалить контейнер проще, чем удалить что-то из системы.

  • Простота и универсальность. Если где-то получилось запустить контейнер, то он точно так же запустится на любой другой платформе, которая поддерживает сам Docker и необходимую архитектуру. Какие технологии и как при этом используются в контейнере, не имеет значения.  

Если вы не знаете, при чем здесь портовые грузчики и контейнеры

В 1950-х годах предприниматель Малкольм Маклин придумал гениальную вещь — грузовой контейнер, который вскоре стал стандартом в области судоходных грузоперевозок. Дело в том, что раньше погрузка и разгрузка кораблей занимала очень много времени, так как сами грузы были разнообразными и упаковывали их по-разному. С появлением контейнеров ситуация кардинально изменилась. Стало абсолютно неважно какой у нас груз, какой он формы и как упакован. Главное, что он лежит в контейнере, который можно быстро погрузить и извлечь. 

Аналогичную задачу в мире разработки решает Docker. Собственно, отсюда и появилось название, логотип и терминология. В контексте разработки ПО контейнер — это стандартизированная и изолированная единица программного обеспечения, которая включает в себя приложение и все зависимости, необходимые для его работы как изолированного процесса в пространстве пользователя.

Что нам нужно знать, чтобы работать с Docker

Поскольку я не знаю, насколько вы хорошо знакомы с Docker, думаю, что будет не лишним вспомнить и разобрать некоторые основы. В частности, освежить в памяти, что такое Dockerfile и образ.

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

Docker-образ — шаблон для создания Docker-контейнеров. Представляет собой исполняемый пакет, содержащий все необходимое для запуска приложения: код, среду выполнения, библиотеки, переменные окружения, файлы конфигурации и другие сущности.

Каждый образ состоит из серии слоев, которые представляют собой изменения или команды, выполненные над базовым образом. Важный аспект работы слоев — это их кэширование. При сборке образа Docker проверяет каждый слой и его сохраненную версию. Если команда (и все команды до нее) не изменились, Docker использует кэшированный слой, а не выполняет команды заново. Технология значительно ускоряет сборку и минимизирует дублирование данных.

Более подробную информацию и все необходимые гайды можно найти в официальной документации.

Теперь, немного вспомнив основы, разберем пример. Вернемся к нашему стажеру и завернем в Docker какой-нибудь бэкенд: 

// backend/index.js

const port = process.env.PORT || 3000;
const express = require('express');
const http = require('http');

const app = express();

app.get('/message', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.status(200).send({message: 'Hello, world!'});
});

const server = http.createServer(app);
server.listen(port, () => {
  console.log('listening on *:' + port);
});

Собственно, здесь мы поднимаем http-сервер с помощью express на 3000-ом порту и обрабатываем всего лишь один запрос /message — ничего сложного. Теперь нужно создать Dockerfile в корне нашего бэкенда.

# backend/Dockerfile

# Определяем базовый образ
FROM node:20-alpine

# Устанавливаем рабочий каталог контейнера
WORKDIR /app

# Копируем package.json и package-lock.json
# из локальной папки внутрь контейнера
COPY package*.json .

# Устанавливаем зависимости
RUN npm ci

# Копируем все локальные файлы в контейнер
COPY . .

# Определяем команду, которая вызовется при запуске контейнера
CMD ["node", "index.js"]

# Обозначаем порт, который приложение использует внутри контейнера
EXPOSE 3000

Разберем каждую из команд.

FROM node:20-alpine

Базовый образ — это Docker-образ, который служит основой для создания нашего образа. В данном случае используется образ на основе Alpine — легковесного дистрибутива Linux с установленной node:20. Alpine-образы часто позиционируют как предпочтительные с точки зрения размеров, безопасности и эффективности, но это не всегда справедливо. Также, помимо Alphine, есть slim-образы, которые в некоторых случаях могут быть предпочтительней. Подробнее про разные образы node можно почитать тут и тут. Если вы сталкивались с какими-нибудь подводными камнями при выборе образа, напишите о своем опыте в комментариях.

WORKDIR /app и COPY package*.json .

Команда WORKDIR определяет рабочую директорию внутри нашего контейнера (в нашем базовом образе нет папки app, поэтому в данном случае она будет создана автоматически). Все дальнейшие действия будут выполняться относительно этой папки. 

Команда COPY позволяет скопировать необходимые файлы из нашего локального контекста внутрь контейнера. Первый аргумент задает, что мы копируем, и использует путь относительно контекста, в котором мы будем запускать сборку образа. В данном случае используется шаблон package*.json: благодаря ему скопируется все, что начинается с package и заканчивается на .json. В больших проектах нужно быть аккуратней с шаблонами — можно скопировать много лишнего. 

Второй аргумент устанавливает, куда именно мы копируем внутрь контейнера относительно рабочей директории. В качестве второго аргумента также можно было бы указать абсолютный путь /app — ничего бы не изменилось. В итоге мы создали в контейнере папку /app и скопировали туда файлы package.json и package-lock.json.

RUN npm ci и COPY . .

Команда RUN выполнит установку зависимостей в рабочей директории контейнера. А затем с помощью команды COPY . . мы скопируем оставшиеся файлы приложения внутрь контейнера. Возникает закономерный вопрос: зачем нам две команды COPY? Почему мы отдельно копировали package.json и package-lock.json, если можно сразу скопировать все?

Да, так можно сделать, и оно будет работать. Но, разбивая копирование на два слоя, мы:

  • оптимизируем кэширование слоев (поскольку package.json и package-lock.json меняются не так часто, как src нашего приложения),

  • уменьшаем размер самих слоев, 

  • улучшаем читабельность Dockerfile (есть явная секция для работы с зависимостями).

Для больших приложений это может быть важно.

CMD ["node", "index.js"]

CMD определяет команду, которая будет выполнена по умолчанию при запуске контейнера. Во время сборки образа команда вызываться не будет. Если указаны несколько команд CMD, то будет выполнена только последняя. В нашем случае мы просто вызываем node index.js.

EXPOSE 3000

Команда EXPOSE предназначена для документирования того, какие порты использует приложение внутри контейнера. При этом сама по себе она никак не влияет ни на сам контейнер, ни на сборку образа и, в нашем случае, будет просто служить напоминанием (на самом деле системы оркестрации контейнеров могут использовать такие объявления, но это выходит за рамки данной статьи).

Теперь, когда мы разобрались с сущностями Dockerfile, давайте запустим бэкенд в контейнере:

# Собираем образ
docker build ./backend

# Запускаем контейнер на основе собранного образа
docker run -p 3000:3000 f9a8re

# Здесь 'f9a8re' - префикс идентификатора образа,
# который можно найти с помощью команды docker images

Аргумент ./backend задает путь к контексту сборки, о котором я упоминал в описании команды COPY. Контекстом сборки должен быть корень нашего бэкенда, чтобы относительные пути при копировании не сломались. Если указать неверный путь, то нужные файлы не найдутся и сборка упадет. В нашем случае мы запускаем команды в корне репозитория, в котором лежат директории ./backend и ./frontend. Если запускать сборку сразу из папки backend, то контекстом сборки в таком случае необходимо указать текущую папку: docker build ..

Также важно отметить, что флаг -p для команды docker run как раз устанавливает связь между портами хоста и контейнера. В нашем случае бэкенд, который слушает 3000-ый порт контейнера, будет доступен для внешнего мира на 3000-ом порту хоста. При запуске контейнера без этого флага мы просто не сможем достучаться до нашего API извне.

После запуска контейнера мы можем обратиться к localhost:3000/message и убедиться, что все работает. А значит, стажее Анатолий может спать спокойно. 

В данном примере Dockerfile лежит в корне контекста сборки, поэтому Docker находит его самостоятельно, без указания пути к файлу. Если это не так, то, помимо контекста сборки, необходимо передавать флаг -f — путь к Dockerfile.

Пример запуска приложения на ванильной ноде помог нам вспомнить или узнать базовые команды Docker. Теперь можно погружаться в детали, которые появляются при работе c Angular.

Docker для Angular

Давайте посмотрим, как нам обернуть в контейнер фронтенд. В качестве фронтенда я буду использовать дефолтную заглушку, которая создается при ng new (или npx create-nx-workspace для следующих разделов) — структура и наполнение проекта не играет роли. Попробуем сделать все по аналогии с бэкендом.

# frontend/Dockerfile

FROM node:20-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci
COPY . .

EXPOSE 4200
CMD ["npm", "start"]

Чтобы не запутаться в ID образов для сборки и запуска, воспользуемся тегами (флаг -t):

docker build -t frontend:latest .\frontend
docker run -p 4200:4200 frontend:latest

Теперь на созданном образе есть тег latest, который будет обозначать последнюю версию сборки нашего приложения и избавит от необходимости искать нужный образ вручную. 

Когда мы запустим контейнер и попробуем открыть localhost:4200, мы увидим, что нам не удалось получить доступ к сайту, как будто приложение не запущено. Проблема в том, что npm-скрипт вызывает ng serve, который по умолчанию привязывает приложение к сетевому интерфейсу loopback.  Это означает, что доступ к приложению возможен только через localhost и мы не сможем обратиться к нашему приложению извне контейнера. Чтобы решить эту проблему воспользуемся флагом --host 0.0.0.0:

# frontend/Dockerfile

FROM node:20-alpine
WORKDIR /app

COPY package*.json ./
# Чтобы вызвать ng serve, нужно глобально установить @angular/cli
RUN npm install -g @angular/cli@17
RUN npm ci
COPY . .

EXPOSE 4200
CMD ng serve --host 0.0.0.0

Теперь все заработает, но у такой реализации несколько недостатков:

  • Команда COPY . ., помимо прочего, копирует еще и node_modules, хотя мы устанавливаем зависимости при сборке образа (этот недостаток также справедлив для примера с бэкендом).

  • Решение не подходит для production-сред, так как запуск приложения через ng serve небезопасен, не использует оптимизации и обладает низкой производительностью.

Для решения первой проблемы воспользуемся .dockerignore — файлом, который определяет, что должно быть исключено из контекста сборки. Чем меньше контекст, тем меньше и быстрее сборка, поэтому исключать надо все весомое и ненужное для запуска приложения.

# frontend/.dockerignore

node_modules
dist
.git
.vscode

Вторую проблему решить несколько сложнее. В production-средах приложение собирается, а затем статика раздается через веб-сервер — к примеру, nginx. Так и поступим:

# frontend/Dockerfile

# Stage 1: Сборка приложения
FROM node:20-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm install -g @angular/cli@17
RUN npm ci
COPY . .
RUN ng build

# Stage 2: Запуск nginx
FROM nginx:alpine

# Меняем конфиг nginx-а на собственный
COPY nginx.conf /etc/nginx/nginx.conf

# Копируем собранное приложение из предыдущего этапа в рабочую директорию nginx
COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html
EXPOSE 8080

# Запускаем nginx
CMD ["nginx", "-g", "daemon off;"]

Теперь мы используем два базовых образа, каждый из которых определяет свой этап работы. Обратите внимание на команду COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html. Она копирует сборку нашего приложения между этапами по алиасу build, который присваивается первому этапу в строке FROM node:20-alpine AS build. Также важно отметить, что конечная директория, в которой окажется сборка после запуска ng build (у меня это /app/dist/frontend/browser), зависит от конфигурации приложения.

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

# frontend/nginx.conf

events{}
http {
  include /etc/nginx/mime.types;
  server {
    listen 8080;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;
    location / {
      try_files $uri $uri/ /index.html;
    }
  }
}

Отлично! Теперь мы умеем запускать наш фронтенд с помощью docker run -p 8080:8080 frontend:latest и можем использовать эту реализацию в продакшене. Это фундамент, который точно пригодится в каком-то виде для любого Angular-приложения. 

Далее мы рассмотрим более специфичные задачи и сложности, которые могут возникнуть в корпоративных проектах, — в зависимости от инфраструктуры конкретного проекта.

А если у нас NX?

NX — один из самых популярных инструментов управления монорепозиториями, часто используется даже в standalone-проектах благодаря ряду удобных инструментов. Если вы тоже используете NX, все, что надо сделать, — переключиться на nx cli для сборки приложения. 

Не будем дублировать здесь nginx.conf и .dockerignore, так как они останутся без существенных изменений, но перепишем Dockerfile:

# nx-frontend/Dockerfile

FROM node:20-alpine AS build
WORKDIR /app

COPY package*.json nx.json ./
COPY scripts ./

# Устанавливаем nx
RUN npm install -g nx@18
RUN npm ci
COPY . .

# Собираем приложение
RUN nx build nx-frontend

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist/nx-frontend/browser /usr/share/nginx/html
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

Теперь, помимо package*.json, мы копируем еще и nx.json, а вместо ng build используем nx build. Также мы добавили копирование папки scripts, в которой могут содержаться скрипты, которые вызываются в npm-скрипте postinstall. Postinstall также будет запущен и при сборке образа может ее уронить, если необходимые файлы не были скопированы.

Однако если эта postinstall-логика необходима только для разработки и не требуется для работы приложения (например, это может быть настройка git-хуков), то разумнее не копировать папку scripts и использовать npm ci --ignore-scripts.

А если у нас свой npm-registry?

Зачастую для корпоративной разработки используются локальные реестры — они обеспечивают безопасность и централизованное управление зависимостями. Для использования стороннего npm-registry в корне проекта создается файл .npmrc, в котором, помимо прочих настроек, указывается значение registry — url используемого npm-реестра. При выполнении каких-либо операций npm автоматически считывает эти настройки. 

Получается, чтобы добиться такого поведения при сборке образа, достаточно просто добавить копирование файла .npmrc в наш Dockerfile. 

На самом деле, если для установки зависимостей используется npm ci (а при использовании корпоративных реестров это чаще всего так), копировать .npmrc не нужно. Все потому, что package-lock.json уже содержит пути к пакетам из нашего реестра и любые файлы конфигурации будут проигнорированы.

Но есть другая сложность: вероятно, такой реестр не является общедоступным и, помимо включенного VPN, для корректной работы npm потребуется SSL-сертификат. 

Локально это не проблема: либо сертификат у вас установлен на уровне системы, либо в корневом .npmrc (обычно он находится в папке пользователя) задается cafile=global/path/to/certificate. В первом случае даже при сборке контейнера все будет хорошо: Docker будет использовать системные SSL-сертификаты. А вот во втором случае ничего не получится, так как в процессе сборки образа про существование этого корневого .npmrc мы ничего не знаем. 

В целом, конечно, можно поддерживать только первый вариант, но работа с сертификатами в разных системах ведется по-разному. Кроме того, часто оказывается, что локально легче использовать глобальный .npmrc, чем разбираться, почему сертификаты криво установились. Поэтому хотелось бы избавиться от требования, что сертификат должен быть обязательно установлен. 

Есть несколько довольно несложных способов это сделать:

  • Отключить проверку SSL перед установкой зависимостей с помощью npm config set strict-ssl false — самое простое решение, но не хотелось бы жертвовать безопасностью, даже если при текущем положении дел это не представляет угрозы.

  • Залить сертификат в корень нашего репозитория и копировать в контейнер — рабочий вариант, но тоже не совсем безопасный, много копий сертификатов в разных репозиториях лучше не плодить.

  • Копировать сертификат в контейнер по абсолютному пути — мы могли бы ввести правило, что сертификат должен лежать в системе по некоторому константному пути, по которому он и будет копироваться при сборке. Опять же, не лучшее решение, так как мы жертвуем универсальностью.

Все простые варианты накладывают ограничения или не совсем безопасны. Можно сделать хитрее. 

Мы знаем, что есть некоторый сертификат, который где-то лежит (будем считать, что это просто файл в служебном репозитории). Почему бы нам не скачать этот файл прямо внутри Dockerfile? Чтобы смоделировать эту ситуацию, я заведу условный cert.crt в корне демонстрационного проекта и буду скачивать его из удаленного репозитория с помощью wget. Чтобы получить прямую ссылку на этот файл, достаточно нажать на кнопку "Raw" его представления в удаленном репозитории. Обновим наш Dockerfile:

# nx-frontend/Dockerfile

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json nx.json ./
COPY scripts ./

# Скачиваем сертификат
RUN wget -O ./cert.crt \
   https://raw.githubusercontent.com/outsidious/docker-for-angular/main/cert.crt

# Устанавливаем сертификат
RUN npm config set cafile ./cert.crt

RUN npm install -g nx@18
RUN npm ci
COPY . .
RUN nx build nx-frontend

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist/nx-frontend/browser /usr/share/nginx/html
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Наверняка, у вас возникнет желание проверить, что файл скачался корректно, с помощью RUN cat ./cert.crt. Для этого лучше всего выполнять сборку с флагами --no-cache и --progress plain. Так вы получите удобный вывод без прогресс-бара и избежите кеширования слоя (если слой закеширован, то вывода может не быть). 

А если нужно работать с access-токенами?

«Серьезно? Ты думаешь, что можно вот так вот взять и просто скачать файл из корпоративного репозитория?» — спросите вы меня и будете абсолютно правы. Едва ли в такой репозиторий можно получить доступ без аутентификации. 

Хорошо, что все популярные хостинги поддерживают простую работу с access-токенами. Мы можем сгенерировать такой токен в настройках профиля и подставлять его в нужный хедер в нашем запросе. Возникает вопрос: откуда и каким образом нам получать этот токен при сборке образа? 

Вариант, когда пользователь вручную подставляет токен через --build-arg, отметаем сразу — это неудобно и небезопасно. В данном случае нам поможет встроенный механизм build-секретов — наиболее предпочтительное решение для работы с подобными данными на этапе сборки. Причем это решение позволяет протягивать секреты как из файлов, так и из системных переменных.

Мы рассмотрим вариант с системной переменной: определим переменную GITHUB_TOKEN, которую будем подтягивать как build-секрет. Инструкцию по установке системных переменных можно найти здесь.

# nx_frontend/Dockerfile

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json nx.json ./
COPY scripts ./

# Скачиваем сертификат
RUN --mount=type=secret,id=GITHUB_TOKEN \
   wget -O ./cert.crt \
   --header "Authorization: Bearer $(cat /run/secrets/GITHUB_TOKEN)" \
   https://raw.githubusercontent.com/outsidious/docker-for-angular/main/cert.crt
# Устанавливаем сертификат
RUN npm config set cafile ./cert.crt

RUN npm install -g nx@18
RUN npm ci
COPY . .
RUN nx build nx-frontend

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist/nx-frontend/browser /usr/share/nginx/html
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

В строке RUN --mount=type=secret,id=GITHUB_TOKEN мы монтируем токен через --mount=type=secret, а затем можем извлечь его по ID и дальше работать как с файлом. Теперь, чтобы запустить фронтенд в контейнере, нужно выполнить сборку с указанием секрета и системной переменной, из которой он должен подтягиваться:

docker build -t frontend:latest ./nx-frontend --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN
docker run -p 8080:8080 frontend:latest

Даже в упрощенном варианте получились довольно громоздкие команды, которые неудобно использовать на практике. Можно, конечно, завернуть их в какой-нибудь makefile, но это не избавит нас от необходимости каждый раз разбираться в этом нагромождении параметров, если нужно что-то изменить.

Что если мы хотим удобнее?

На помощь может прийти Docker Compose — инструмент для работы с мультиконтейнерными приложениями. Заведем в корне проекта файл compose.yml:

# ./compose.yml

services:
 frontend:
   image: frontend:latest
   restart: on-failure
   ports:
     - "8080:8080"
   build:
     context: ./nx-frontend
     args:
       GITHUB_TOKEN: ${GITHUB_TOKEN}

Здесь указывается конфигурация сервиса frontend, который должен использовать одноименный образ с тегом latest, перезапускать контейнер в случае сбоя, пробрасывать необходимые порты и использовать папку ./nx-frontend в качестве контекста.

Проблема заключается в том, что Docker Compose не умеет напрямую работать с build-секретами, как мы делали это при сборке контейнера вручную. А чтобы использовать секреты стандартными способами, придется сохранять наш токен в файл. Однако есть и смежное преимущество: мы можем напрямую обратиться к системной переменной из compose.yml и прокинуть ее как параметр сборки. 

Дисклеймер: Не рекомендуется использовать данное решение для работы с секретами, так как аргументы сборки в некоторых случаях могут быть найдены среди метаданных образа. Но если получить доступ к Docker-образам извне невозможно, то такой подход допустим. 

Docker Сompose v2

В июле 2023 года появилась новая версия Docker Сompose, которая отличается от предшественника новым интерфейсом командной строки и улучшенной производительностью. Одним из обновлений стало избавление от символа «-», используемого в качестве разделителя. В долгоживущих проектах можно столкнуться с использованием старой версии. Соответственно, все команды будут начинаться с префикса docker-compose вместо docker compose, а конфигурационный файл будет называться docker-compose.yml.

Поправим наш Dockerfile для работы с передаваемым аргументом:

# nx_frontend/Dockerfile

FROM node:20-alpine AS build
WORKDIR /app

COPY package*.json nx.json ./
COPY scripts ./

ARG GITHUB_TOKEN
RUN wget -O ./cert.crt \
   --header "Authorization: Bearer $GITHUB_TOKEN" \
   https://raw.githubusercontent.com/outsidious/docker-for-angular/main/cert.crt
RUN npm config set cafile ./cert.crt

RUN npm install -g nx@18
RUN npm ci
COPY . .
RUN nx build nx-frontend

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist/nx-frontend/browser /usr/share/nginx/html
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

Теперь для основной работы с нашим контейнером достаточно нескольких команд без неочевидных параметров и флагов:

docker compose build   # Сборка образа
docker compose up      # Запуск контейнера (если образ не был собран, то он предварительно соберется)
docker compose down    # Остановка и удаление поднятых контейнеров и сетей

На самом деле Docker Compose в первую очередь предназначен для управления контейнерами и их взаимодействием, поэтому в данном кейсе совсем не раскрывается мощь этого инструмента, но мы на этом остановимся. 

Вместо заключения

Теперь вы знаете, с какими сложностями можно столкнуться при докеризации Angular-приложений в корпоративной среде и как их можно решить. Для дальнейшего погружения в эту тему можно подробнее изучить Docker Compose, системы оркестрации контейнеров и связанные инструменты управления. В процессе работы над текстом я понял, что материала получается слишком много, и не стал освещать некоторые темы, начиная от настройки веб-сервера и оптимизаций сборки и заканчивая сетевыми конфигурациями и взаимодействием сервисов. Если статья вас заинтересовала , пишите в комментариях, нужно ли продолжение.

Демонстрационный проект из статьи можно найти тут. А еще заглядывайте ко мне в Telegram-канал «Недовольный мидл» — веду там заметки из жизни фронтендера.

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