Docker-образ для раздачи Single Page Application

Single-page Application (SPA) – это набор статических JavaScript и HTML файлов, а так же картинок и других ресурсов. Поскольку они не изменяются динамически, опубликовать их в интернете очень просто. Для этого существует большое количество дешёвых и даже бесплатных сервисов, начиная с простого GitHub Pages (а для кого-то даже с narod.ru) и заканчивая CDN вроде Amazon S3. Однако мне нужно было другое.


Мне нужен был Docker-образ с SPA, чтобы его легко можно было запустить как в продакшене в составе Kubernetes-кластера, так и на машине back-end разработчика, который понятия не имеет, что такое SPA.


UPD 07.07.20: Посмотерть самому на то, что в итоге получилось, можно тут: https://github.com/alexxxnf/spa-docker-example


Я для себя определил следующие требования к образу:


  • простота в использовании (но не в сборке);
  • минимальный размер как с точки зрения диска, так и с точки зрения RAM;
  • настройка через переменные окружения, чтобы образ можно было использовать в разных средах;
  • максимально эффективная раздача файлов.

Сегодня я расскажу как:


  • выпотрошить nginx;
  • собрать brotli из исходников;
  • научить статические файлы понимать переменные окружения;
  • ну и конечно как собрать из всего этого Docker-образ.

Цель этой статьи поделиться моим опытом и спровоцировать опытных участников сообщества на конструктивную критику.


Сборка образа для сборки


Чтобы финальный Docker-образ получился маленьким по размеру, нужно придерживаться двух правил: минимум слоёв и минималистичный базовый образ. Одним из самых маленьких базовых образов является образ Alpine Linux, поэтому именно его я и выберу. Кто-то может возразить, что Alpine не подходит для продакшена и, возможно, окажется прав. Но лично у меня с ним никогда не возникало проблем и никаких аргументов против него нет.


Чтобы было поменьше слоёв, я буду собирать образ в 2 этапа. Первый – черновой, в нём останутся все вспомогательные утилиты и временные файлы. А в чистовой я запишу только финальную версию приложения.


Начнём со вспомогательного образа.


Для того, чтобы скомпилировать SPA-приложение, обычно, нужен node.js. Я возьму официальный образ в комплекте с которым так же есть пакетные менеджеры npm и yarn. От себя я добавлю node-gyp, который нужен для сборки некоторых npm-пакетов, и компрессор Brotli от Google, который пригодится нам позже.


Dockerfile с комментариями.
# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <amaydokin@gmail.com>"
ENV BROTLI_VERSION 1.0.7
# Пакеты, которые нужны, чтобы собрать из исходников Brotli
RUN apk add --no-cache --virtual .build-deps \
        bash \
        gcc \
        libc-dev \
        make \
        linux-headers \
        cmake \
        curl \
    && mkdir -p /usr/src \
    # Исходники Brotli скачиваем из официального репозитория
    && curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src \
    && cd /usr/src/brotli-$BROTLI_VERSION \
    # Компилируем Brotli
    && ./configure-cmake --disable-debug && make -j$(getconf _NPROCESSORS_ONLN) && make install \
    # Добавляем node-gyp
    && yarn global add node-gyp \
    # Убираем за собой мусор
    && apk del .build-deps && yarn cache clean && rm -rf /usr/src

Уже здесь я борюсь за минимализм, поэтому образ собирается одной большой командой.


Готовый образ можно найти тут: https://hub.docker.com/r/alexxxnf/spa-builder. Хотя я рекомендую не полагаться на чужие образы и собрать свой.


nginx


Для раздачи статики можно использовать любой web-сервер. Я привык работать с nginx, поэтому и сейчас буду использовать его.


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


$ docker run --rm nginx:1-alpine nginx -V
nginx version: nginx/1.17.9
built by gcc 8.3.0 (Alpine 8.3.0) 
built with OpenSSL 1.1.1d  10 Sep 2019
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --with-perl_modules_path=/usr/lib/perl5/vendor_perl --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer' --with-ld-opt=-Wl,--as-needed

Я возьму за основу Dockerfile, но оставлю в нём только то, что нужно для раздачи статики. Мой вариант не сможет работать по HTTPS, не будет поддерживать авторизацию и многое другое. Зато моя версия сможет раздавай файлы, сжатые алгоритмом Brotli, который немного эффективнее, чем gzip. Сжимать файлы будем один раз, делать это на лету нет необходимости.


Вот какой Dockerfile у меня получился. Комментарии на русском – мои, на английском – из оригинала.


Dockerfile
# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <amaydokin@gmail.com>"
ENV NGINX_VERSION 1.16.0
ENV NGX_BROTLI_VERSION 0.1.2
ENV BROTLI_VERSION 1.0.7
RUN set -x \
    && addgroup -S nginx \
    && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
# Устанавливаем пакеты, которые нужны чтобы собрать nginx и модуль ngx_brotli к нему
    && apk add --no-cache --virtual .build-deps \
            gcc \
            libc-dev \
            make \
            linux-headers \
            curl \
    && mkdir -p /usr/src \
# Скачиваем исходники
    && curl -LSs https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz | tar xzf - -C /usr/src \
    && curl -LSs https://github.com/eustas/ngx_brotli/archive/v$NGX_BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src \
    && curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src \
    && rm -rf /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli/ \
    && ln -s /usr/src/brotli-$BROTLI_VERSION /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli \
    && cd /usr/src/nginx-$NGINX_VERSION \
    && CNF="\
            --prefix=/etc/nginx \
            --sbin-path=/usr/sbin/nginx \
            --modules-path=/usr/lib/nginx/modules \
            --conf-path=/etc/nginx/nginx.conf \
            --error-log-path=/var/log/nginx/error.log \
            --http-log-path=/var/log/nginx/access.log \
            --pid-path=/var/run/nginx.pid \
            --lock-path=/var/run/nginx.lock \
            --http-client-body-temp-path=/var/cache/nginx/client_temp \
            --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
            --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
            --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
            --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
            --user=nginx \
            --group=nginx \
            --without-http_ssi_module \
            --without-http_userid_module \
            --without-http_access_module \
            --without-http_auth_basic_module \
            --without-http_mirror_module \
            --without-http_autoindex_module \
            --without-http_geo_module \
            --without-http_split_clients_module \
            --without-http_referer_module \
            --without-http_rewrite_module \
            --without-http_proxy_module \
            --without-http_fastcgi_module \
            --without-http_uwsgi_module \
            --without-http_scgi_module \
            --without-http_grpc_module \
            --without-http_memcached_module \
            --without-http_limit_conn_module \
            --without-http_limit_req_module \
            --without-http_empty_gif_module \
            --without-http_browser_module \
            --without-http_upstream_hash_module \
            --without-http_upstream_ip_hash_module \
            --without-http_upstream_least_conn_module \
            --without-http_upstream_keepalive_module \
            --without-http_upstream_zone_module \
            --without-http_gzip_module \
            --with-http_gzip_static_module \
            --with-threads \
            --with-compat \
            --with-file-aio \
            --add-dynamic-module=/usr/src/ngx_brotli-$NGX_BROTLI_VERSION \
    " \
# Собираем
    && ./configure $CNF \
    && make -j$(getconf _NPROCESSORS_ONLN) \
    && make install \
    && rm -rf /usr/src/ \
# Удаляем динамический brotli модуль, оставляя только статический
    && rm /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so \
    && sed -i '$ d' /etc/apk/repositories \
# Bring in gettext so we can get `envsubst`, then throw
# the rest away. To do this, we need to install `gettext`
# then move `envsubst` out of the way so `gettext` can
# be deleted completely, then move `envsubst` back.
    && apk add --no-cache --virtual .gettext gettext \
    && mv /usr/bin/envsubst /tmp/ \
    && runDeps="$( \
        scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
            | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
            | sort -u \
            | xargs -r apk info --installed \
            | sort -u \
    )" \
    && apk add --no-cache $runDeps \
    && apk del .build-deps \
    && apk del .gettext \
    && mv /tmp/envsubst /usr/local/bin/ \
# Bring in tzdata so users could set the timezones through the environment
# variables
    && apk add --no-cache tzdata \
# 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
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
STOPSIGNAL SIGTERM
CMD ["nginx", "-g", "daemon off;"]

Я сразу же поправлю nginx.conf, чтобы gzip и brotli были включены по умолчанию. Так же включу кэширующие заголовки, ведь у нас будет раздаваться никогда не меняющаяся статика. И последним штрихом будет переадресация всех 404 запросов на index.html, это необходимо для навигации в SPA.


nginx.conf
user nginx;
worker_processes  1;
error_log /var/log/nginx/error.log warn;
pid       /var/run/nginx.pid;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
events {
    worker_connections 1024;
}
http {
    include      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;
    gzip_static   on;
    brotli_static on;
    server {
        listen      80;
        server_name localhost;
        charset utf-8;
        location / {
            root html;
            try_files $uri /index.html;
            etag on;
            expires max;
            add_header Cache-Control public;
            location = /index.html {
                expires 0;
                add_header Cache-Control "no-cache, public, must-revalidate, proxy-revalidate";
            }
        }
    }
}

Скачать готовый образ можно здесь: https://hub.docker.com/r/alexxxnf/nginx-spa. Он занимает 10,5 МБ. Оригинальный nginx занимал 19,7 МБ. Мой спортивный интерес удовлетворён.


Учим статику понимать переменные окружения


Для чего в SPA могут понадобится настройки? Например, для того, чтобы указать какой RESTful API использовать. Обычно настройки для нужного окружения передаются в SPA на этапе сборки. Если нужно что-то поменять, то придётся пересобрать приложение. Я этого не хочу. Я хочу чтобы приложение собиралось один раз на стадии CI, а конфигурировалось столько, сколько нужно, на стадии CD с помощью переменных окружения.


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


Для этого в Dockerfile предусмотрен параметр ENTRYPOINT. Передадим ему вот такой скрипт (на примере Angular):


docker-entrypoint.sh
#!/bin/sh
set -e
FLAG_FILE="/configured"
TARGET_DIR="/etc/nginx/html"
replace_vars () {
  ENV_VARS=\'$(awk 'BEGIN{for(v in ENVIRON) print "$"v}')\'
  # В Angular ищем плейсхолдеры в main-файлах
  for f in "$TARGET_DIR"/main*.js; do
    # envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
    echo "$(envsubst "$ENV_VARS" < "$f")" > "$f"
  done
}
compress () {
  for i in $(find "$TARGET_DIR" | grep -E "\.css$|\.html$|\.js$|\.svg$|\.txt$|\.ttf$"); do
    # Используем максимальную степень сжатия
    gzip -9kf "$i" && brotli -fZ "$i"
  done
}
if [ "$1" = 'nginx' ]; then
  # Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
  if [ ! -e "$FLAG_FILE" ]; then
    echo "Running init script"
    echo "Replacing env vars"
    replace_vars
    echo "Compressing files"
    compress
    touch $FLAG_FILE
    echo "Done"
  fi
fi
exec "$@"

Чтобы скрипт сделал своё дело, в js-файлах настройки надо писать вот в таком виде: ${API_URL}.


Стоит отметить, что большинство современных SPA при сборке добавляют к своим файлам хэши. Это нужно, чтобы браузер мог смело закэшировать файл на длительный срок. Если файл всё-таки изменится, то изменится и его хэш, что в свою очередь заставит браузер скачать файл заново.


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


Собираем финальный образ


Наконец-то.


Dockerfile
# Первый базовый образ для сборки
FROM alexxxnf/spa-builder as builder
# Чтобы эффктивнее использовать кэш Docker-а, сначала устанавливаем только зависимости
COPY ./package.json ./package-lock.json /app/
RUN cd /app && npm ci --no-audit
# Потом собираем само приложение
COPY . /app
RUN cd /app && npm run build -- --prod --configuration=docker

# Второй базовый образ для раздачи
FROM alexxxnf/nginx-spa
# Забираем из первого образа сначала компрессор
COPY --from=builder /usr/local/bin/brotli /usr/local/bin
# Потом добавляем чудо-скрипт
COPY ./docker/docker-entrypoint.sh /docker-entrypoint.sh
# И в конце забираем само приложение
COPY --from=builder /app/dist/app /etc/nginx/html/
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

Теперь получившийся образ можно собрать и использовать где-угодно.

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 45

    –2
    • envsubst может работать как простой шаблонизатор по env переменным (без кучи кода с awk).
    • самый лучший (ИМХО) контейнер для статики скачивает архив со статикой с s3 перед стартом (занимает секунды), а пересобирать вообще не нужно. Можно скачивать вместе с nginx конфигом, если меняется от проекта к проекту.
      • если используется оркестратор вроде k8s или nomad, то можно вообще не собирать свой образ, а просто в манифесте переписать /etc/nginx/nginx.conf и entrypoint с wget для стандартного образа.

    wget -qO- $ASSETS_URL | tar xvz -C /srv
    exec nginx

    Статика редко весит больше 20-50мб в архиве. Поэтому скачивать перед стартом — малая кровь за такой простой подход.

      0

      Как раз недавно изучал подобный сценарий. Обнаружилось, что в обычном образе nginx нет ни wget, ни curl, а вот в alpine-варианте есть wget. Можно пойти ещё дальше: статику предварительно сжать и раздавать её со смонтированного RAM-диска (tmpfs). И всё это со стандартным образом nginx-alpine.

        +2
        Скачивание статики перед стартом рушит всю суть контейнеров. Вам придется поддерживать две сущности — версии контейнеров и версии этой самой статики. Это неудобно и приводит к ошибкам. Ну и скачивание может отвалиться в самый неудобный момент, даже с S3.

        Честно говоря вообще не понимаю рвение к экономии на образах. Чтобы он весил гигабайты нужно прям постараться.
          0

          У меня нет проблем с тем, чтобы хранить ссылку на архив вместо ссылки на image. А вот проблемы, когда у вас 20-50-100 контейнеров со статикой и их нужно обновить все, возникает постоянно.

            0
            Я все же не пойму о каких проблемах идет речь?

            Ну есть некий docker.io/nginx:1.2.3 b 100500 его реплик. Обновили версию — обновились контейнеры, все логично.

            Если у вас 100500 контейнеров, которые что-то там выкачивают, вы не можете быть уверены что обновление пройдет удачно, т.к. где-то оно скачается, где-то может быть ошибка. В итоге вы получите то же самое, только большими усилиями. И зачем?
              0

              Если оно не скачается, то и не запустится. А rolling update откатит все обратно. Точно так же новый image может быть недоступен. Распределенные registry работают поверх s3 кстати и говорить, что registry надежнее не совсем правильно.


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


              PS s3 я имею в виду s3 compatible storage, которое есть у всех приличных облаков или больших кластеров (ceph, minio, etc)

                +1
                Вы берете одну точку отказа и добавляете в нее вторую точку отказа вместе с необходимостью следить за соотношением версий. На мой взгляд — это просто не имеет смысла. Если мы просто выкачиваем статику, то куда проще ее хранить в образе же.
                  –2

                  Точка отказа s3? С отказом s3 откажет и registry (только если они не пользуются разными s3). high available registry всегда используют внешнее хранилище, s3 самое простое.

                    +2
                    Точка отказа в самом факте сценария «для запуска контейнера надо что-то скачать». Контейнер должен быть автономный, в идеале. Понятно что ваш пример с подключением к БД не будет автономный, но уж статические файлы можно положить в контейнер.

                    Представьте что у вас на мышке два провода и ни один не должен порваться «просто потому что было удобнее питание сделать отдельно». Они не должны рваться, все верно, но зачем они нужны, когда можно все совместить?
                      0

                      Да, уволюсь завтра, экспертиза ниже плинтуса =)

                        0
                        Это не экспертиза, это опыт работы с различной реализацией статики в некрупных проектах
                0
                Ну есть некий docker.io/nginx:1.2.3 b 100500 его реплик. Обновили версию — обновились контейнеры, все логично.

                У вас это
                myregistry.com/myproject1:v2
                myregistry.com/myproject2:v2
                myregistry.com/myproject3:v2
                myregistry.com/myproject4:v2
                Которые from nginx:1.2.3 с разной статикой.


                И если вам нужно внести правки в базовый образ или конфиг nginx для всех проектов, то это правка всех проектов и пересборка всех images (в большой компании с большим количеством проектов это затянется на месяца без острой необходимости)

                  0
                  В моей реализации базовый контейнер nginx не содержит статики, он содержит как раз nginx. У вас не может быть одного образа со статикой на все-все проекты.

                  Если у вас так много зависимостей, что они месяц будут собираться из-за пересбора образа со статикой — вам надо менять архитектуру, ну правда. Какая-то надуманная ситуация. Статика добавляется в финальный образ.
                    0

                    Я понял, кажется, поинт:
                    Наши контейнеры для каждого проекта со статикой содержат
                    FROM nginx:1.16.1 в докерфайле в репе проекта
                    Выходит 1.16.2 с закрытием уязвимости. Нам надо пройтись по всем репам, по всем докерфайлам, изменить на 1.16.2, закоммитить, запушить, отдать на код-ревью, сбилдить, тегнуть новый образ, протестировать, изменить конфиги докера (ещё коммиты, пуш, ревью) и т. д. В случае с общим образом и подкачиваемым контент ом, нужно только конфиги докера изменять.

                      0

                      Да, все так. В случае уязвимости еще можно всех напрячь и быстро перекатить, но если там ничего не горит, а поменять хочеться… то это будет изменяться месяцами (завести таску на все проекты в джире и ждать пока низкоприоритетная таска выполнится через все процессы, что вы описали).

                        0
                        Я видимо просто не работал с такими неповоротливыми проектами, где обновление версии nginx будет длиться месяц.

                        Хотя все же не понимаю в чем будет разница с выкачиванием/хранением статики, т.к. и там и там образ менять придется.
                          0

                          Характерно для проектов, где много репозиториев, каждым из которых владеют разные команды со своими бэклогами.


                          Разница в том, что обычно нужно прогнать по всему флоу сначала изменения докерфайла, а потом изменения конфигов докера (docker-compose, swarm, k8s, ...), а в варианте с выкачкой только второе. Процессы разработки и изменения базового образа не пересекаются, не блочат друг друга

                            0
                            Но это же не базовый образ. Может я как-то не так объясняю, давайте на своем, живом примере.

                            Есть образ с nginx, в который внесены некоторые корректировки. Это отдельный «базовый» образ docker-nginx:1.0

                            В этом образе нет никакой статики, там просто голый nginx. Даже конфигов нет. Этот образ меняется раз в тыщу лет, когда выходит апдейт самого nginx, например.

                            В проекте (k8s через helm) есть деплоймент, который берет этот образ с указанием версии, моунтит в него конфиги из k8s configmap и запускает. Для дева там запускается прям базовый образ с локальной шареной директорией для статики.

                            Для прода собирается так (реальный Dockerfile):
                            FROM registry.gitlab.com/base/docker-nginx:1.0
                            ADD ./public /usr/share/nginx/html/public


                            Это билдится в CI/CD гитлаба для конкретного проекта.
                            Т.е. работает это так:
                            — Базовый образ не меняется почти никогда
                            — Для смены конфигов — меняем configmap
                            — Для смены статики пушим статику в репо, образ билдится, деплоится для конкретного проекта

                            Получаем образы вида project-nginx:1/2/3/4/5 в каждый из которых вшита статика его версии.

                            Если у вас много команд, каждая из которых живет на разных планетах — им это все не мешает, у них у каждой свои образы. Не нужно обновлять базовый образ? Не обновляйте, дело ваше.

                            Я просто не понимаю что такое «прогнать по всему флоу». Если вы меняете статику — вы меняете проект. Это в любом случае коммит, пуш, ревью и все что вы там еще делаете.
                            А если у вас статика просто где-то валяется «во вне», то я не знаю зачем ее вообще выкачивать в докере, просто раздавайте через cdn и все.
                              0

                              Вот, ситуация, обновили базовый образ. Теперь нужно (могут быть нюансы):


                              1. Поменять в каждой репе докерфайл на FROM registry.gitlab.com/base/docker-nginx:1.0.1
                              2. Закоммитить, запушить, создать PR
                              3. Отревьювить изменения
                              4. Сбилдить и залить в регистри с новым тегом
                              5. Протестить
                              6. Изменить в деплоймент/values тег
                                7...

                              В случае с подтягиванием файлов, шаги 1-5 не нужны, сразу начинаем с деплоймент/values

                                0
                                Но это же все делается при любых изменениях в проекте. Причем шаги 3 и далее делаются автоматически (ну разве что выкат на прод по кнопке).

                                Шаг 1 — это поменять один символ.
                                Шаг 2 — это Ctrl + K, Enter в IDEA.
                                Ну PR может быть как-то вручную создавать, хотя не все работают через PR.

                                И каким это образом мы, в случае с подтягиванием файлов, начинаем сразу с шага 6?
                                Ссылка на выкачивание статики всегда одинаковая что ли? Тогда это убивает всю идею версионности на корню. Как понять какая версия статики сейчас запущена?

                                Пусть даже мы оставляем это убийство. У вас 100 реп, в каждой запускается «базовый образ». Его обновили. Вы там делаете деплой по latest тегу, что ли? Вряд ли. Тогда точно так же придется править 100 реп.
                                  0

                                  Ссылка на выкачивание статики берется оттуда же, откуда ссылка на image. Разницы нет.


                                  Конкретно про спор.
                                  FROM registry.gitlab.com/base/docker-nginx:1.0
                                  ADD ./public /usr/share/nginx/html/public
                                  Такой вариант хороший, кроме выше описанной ситуации, когда нужно обновить в куче реп это одну строчку (в больших компаниях реал сложно). Но если маунтить конфиг, то редко нужно.


                                  Касательно подхода с выкачиванием статики… я вспомнил зачем это ввел. Была идея написать k8s оператор, чтобы одним деплойментом nginx сервить всю статику, потому что печально наблюдать 100+ контейнеров nginx в кластере.

                                    0
                                    Была идея написать k8s оператор, чтобы деплойментом nginx сервить всю статику, потому что печально наблюдать 100+ контейнеров nginx в кластере.

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

                                      0

                                      PV это уже stateful + PV нужен ReadWriteMany (я редко работаю с такими, может ReadOnlyMany, но вообще не сталкивался с ним), такие хранилища обычно медленные. Со скачиванием просто получается реаликация с мастером в виде s3.

                                        0

                                        Хоть так, хоть так стейт у вас будет, просто в одном случае пользуетесь нативными для k8s механизмами (под капотом, кстати, тот же s3 может быть), а в другом самописными. Плюсы есть и у того, и у другого варианта.

                                          0

                                          Ссылка на s3 это как ссылка на внешнее апи или подключение к базе. Из-за этого сервис не приобретает state. Это stateless.


                                          Подход с другой стороны
                                          Сможете ли вы запустить 1кк контейнеров с общим PV? -> без дикого гемора нет (так себя ведут stateful)
                                          Сможете ли вы запускать 1кк контейнеров с выкачиванием? -> да (так себя ведут stateless)

                                    0

                                    Я об единственном кейсе сейчас: обновление базового образа, сами статические файлы не менялись, ссылка не менялась. Всё что нужно изменить таг в чарте/деплойменте. С билдом статики в контейнере нам нужно сделать 200 изменений на 100 реп: 100 в докерфайлах и 100 в чартах, со скачиванием — 100, только в чартах

                        +1

                        По-моему, эта проблема характерна для любых приложений в Docker.


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


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

                          0

                          Если используем FROM scratch, то проблемы такой нет :) реально такое только с golang видел/делал


                          А так, да, проблема есть и вариантов для её решения много, какие-то лучше подходят в одной ситуации, какие-то в другой.

                  0
                  • По умолчанию envsubst заменяет в файле всё, что начинается на $. Чтобы он заменял только реально существующие переменные окружения, ему надо передать их список, который я и получаю с awk.
                  • Использование S3 мне не нравится с точки зрения атомарности релиза и версионирования. Хотя, наверняка, я просто не умею его правильно готовить.
                    0

                    Насчет второго пункта — там все тоже самое. Нет никакой разницы… передавать куда-то ссылку на image или ссылку на архив или ссылку на image+архив вместе.

                    0

                    Странный для меня подход. В чём плюсы?


                    Минусы, навскидку:


                    • лишняя инфраструктура, которой нужно управлять (следить за доступностью, занятым ресурсам, делать бэкапы и проверять их работу)
                    • усложнение версионирования, причём их два появляется: версия контейнера и версия самих файлов, нужно следить за обеими, чтобы инициировать передеплой
                    • возможные юридические или технические ограничения, в частности на трансграничную передачу данных или использование публичных облак в принципе
                    • две процедуры билда

                    Это может иметь какой-то смысл в случае использования сторонних образов, когда свой репозиторий образов вообще не создаёшь, а инфраструкта для билда статики на S3 и её версионирования уже есть. Но в общем случае плюсы мне не понятны, хотя сам использую публичные образы nginx с примонтированными конфигами и, если надо, контентом как фронт для php-fpm. Но то монтирование, а не скачивание. Статика всё равно билдится в наши образы, вопрос лишь какие из них.

                      +1

                      Я выше писал часть из этого...


                      • s3 я имею в виду s3 compatible storage, которое есть у всех приличных облаков или больших кластеров (ceph, minio, etc на своих серверах)
                      • ваш личный HA registry (3 копии) скорее всего будет использовать этот самый s3 как внешнее хранилище https://docs.docker.com/registry/storage-drivers/s3/
                      • версией image рулят DevOps парни, версией статики фронтендщики, разделение ролей и тп

                      У нас все, что касается разворачивание лежит в отдельном репозитории. Туда не могут лезть все. От команды конкретного проекта требуется только отдать артефакт. Будет это image или архив не так важно (фронтенщикам проще s3, мало кто хорошо умеет собрать образ).


                      PS Вместо s3 можно использовать артефакты CI/CD у gitlab или еще кого (они так же лежат обычно на s3 https://docs.gitlab.com/ee/administration/job_artifacts.html#s3-compatible-connection-settings). Тогда думать вообще про хранилище не нужно. Но у нас исторически напрямую.

                    +1

                    2 вопроса:


                    1. Почему не собрать модуль Brothli в оригинальном образе и просто не скопировать его в финальный, опять же на основе оригинального?
                    2. Почему envsubst, а не dockerize?
                      0
                      1. Так можно сделать там, где нужен полноценный nginx, хотя может возникнуть вопрос бинарной совместимости модуля и nginx. В данном же случае задача была сделать nginx как можно меньше, поэтому пришлось его пересобрать убрав всё, что мне показалось ненужным.
                      2. Про dockerize я просто не знал. Спасибо за наводку!
                      0

                      После прочтения лично у меня остался один вопрос: зачем там brotli?
                      Ведь для девелопмента он не нужен точно, а на production статика раздается с CDN.

                        0

                        Не всем нужен CDN, если клиенты из одного города, то это деньги на ветер. Нормальные CDN стоят дороже раздачи со своих серверов.

                          0

                          Так если не надо CDN, так как все в одном городе, то и сжимать не сильно нужно.
                          А сам CDN может быть абсолютно бесплатным. Главное захотеть. Подсказка: cf

                            0

                            Я недавно знакомому отключил его для сайта Самары. Результат -100мс пинга. Бесплатно того не стоит.

                        –1
                        Какого размера получился результирующий образ?

                        Если нас интересует компактность образа именно в проде, то вижу возможность для оптимизации. node-gyp и инструменты сборки нативного кода, как правило, нужны на этапе установки но не нужны после этого. Если использовать multi-stage build, то можно убрать эти зависимости из финального образа. Недавно удалось выиграть за счет multistage более 300MB размера образа (~1.6GB -> ~1.25GB)
                          0

                          Можно ещё со squash поиграться

                          0
                          1. непонятно, стоит ли овчинка выделки. Время потрачено, размер образа уменьшен, но задачу можно было решить добавлением 10МБ RAM на каждый контейнер, я же правильно понимаю?
                          2. find умеет фильтровать по имени и выполнять команды, то есть для того же compress не надо городить цикл и надеяться на то, что будут имена файлов без пробелов, например.
                          3. Перевод с buildtime configuration на runtime configuration ̶н̶а̶д̶о̶ по моему мнению, лучше делать на уровне приложения, а не костылить баш скрипты. Тогда в образе будет исключительно nginx и сборка SPA, а конфиг подмонтировать в виде файла. Пример для angular: https://juristr.com/blog/2018/01/ng-app-runtime-config/. Сжатие при этом тоже можно перенести в этап сборки, то есть бинарник бротли тоже не понадобится, да и вообще отдельный entrypoint не будет нужен
                            –1
                            1. Всё верно. Но время уже потрачено и результат есть, поэтому теперь я его просто переиспользую.
                            2. Когда появится возможность, я попробую сделать через один find.
                            3. В статье речь скорее про delivery-time configuration, а не runtime configuration.
                            0
                            … в составе Kubernetes-кластера

                            # Флаг нужен, чтобы выполнить скрипт только при самом первом запуске

                            В кубере нет второго запуска контейнера. И каждый перезапуск будет выполнятся сжатие файлов, а если заданы лимиты на pod, тогда запуск может длиться достаточно долго. Вполне возможно, что и не завершится из-за лимитов на память и случится ООМ.
                            Вообще непонятно зачем выполнять сжатие файлов в готовом!!! контейнере. Выполняйте это в процессе сборки образа.
                            На продовых кластерах очень часто осуществляется запуск контейнеров от non-root пользователя. При запуске pod свалится в CrashloopBackOff из-за прав на директории/файлы и конфигурации nginx.conf.
                            А иногда встречается политика readOnlyRootFilesystem: true, запрещающая создавать rw-слой и в этом случае тоже получаем CrashloopBackOff.
                            А что если понадобится подправить nginx.conf? Пересобирать базовый образ?

                            Так что в случае с запуском в кубере это будет или дев-кластер или локальный minikube.
                              0
                              Ну вот по поводу изменения настройки RESTfull API в самой статике я бы не согласился. Аякс запросы не должны идти прямо на бекенд, они должны проксироваться самим нджинксом. Для этого существует reverse-proxy. Таким образом и статику не нужно настраивать и не будет проблем с CORS на бекенде(выключать CORS на продакшене запрещено).

                              Более того в современных фронтовых фреймворках (которые создают SPA) стоит аналогичная конфигурация проксирования в вебпаке, таким образом приложение у девелопера не будет сильно отличаться от прода. И настройка всего этого предельно проста.
                                0

                                Замена значений в скомпилированных .js файлах не будет, например, работать, если используется SRI (механизм, позволяющий браузерам по подписи проверить что файл не изменили по пути).

                                Only users with full accounts can post comments. Log in, please.