Dockerôfbylding foar distribúsje fan Single Page Application

Single-page Application (SPA) is in set fan statyske JavaScript- en HTML-bestannen, lykas ôfbyldings en oare boarnen. Om't se net dynamysk feroarje, is it publisearjen fan har online heul maklik. D'r binne in grut oantal goedkeape en sels fergese tsjinsten foar dit, begjinnend mei in ienfâldige GitHub-siden (en foar guon sels mei narod.ru) en einigje mei in CDN lykas Amazon S3. Ik hie lykwols wat oars nedich.

Ik hie in Docker-ôfbylding nedich mei SPA, sadat it maklik koe wurde lansearre sawol yn produksje as ûnderdiel fan in Kubernetes-kluster, en op 'e masine fan in back-end-ûntwikkelder dy't gjin idee hat wat SPA is.

Ik haw de folgjende ôfbyldingseasken foar mysels bepaald:

  • gemak fan gebrûk (mar net gearstalling);
  • minimale grutte sawol yn termen fan skiif en RAM;
  • konfiguraasje troch omjouwingsfariabelen sadat de ôfbylding kin brûkt wurde yn ferskate omjouwings;
  • de meast effisjinte ferdieling fan triemmen.

Hjoed sil ik jo fertelle hoe:

  • gut nginx;
  • bouwe brotli út boarnen;
  • lear statyske bestannen om omjouwingsfariabelen te begripen;
  • en fansels hoe't jo in Docker-ôfbylding kinne sammelje fan dit alles.

It doel fan dit artikel is om myn ûnderfining te dielen en erfarne leden fan 'e mienskip út te lokjen ta konstruktive krityk.

Bouwe in ôfbylding foar gearkomste

Om it definitive Docker-ôfbylding lyts yn grutte te meitsjen, moatte jo twa regels folgje: in minimum fan lagen en in minimalistyske basisôfbylding. Ien fan 'e lytste basisôfbyldings is de Alpine Linux-ôfbylding, dus dat sil ik kieze. Guon kinne stelle dat de Alpine net geskikt is foar produksje, en se kinne gelyk hawwe. Mar persoanlik haw ik noait problemen mei him hân en der binne gjin arguminten tsjin him.

Om minder lagen te hawwen, sil ik de ôfbylding yn 2 stadia sammelje. De earste is in ûntwerp; alle helpprogramma's en tydlike bestannen sille deryn bliuwe. En yn it lêste stadium sil ik allinich de definitive ferzje fan 'e applikaasje opskriuwe.

Litte wy begjinne mei it helpôfbylding.

Om in SPA-applikaasje te kompilearjen hawwe jo normaal node.js nedich. Ik nim de offisjele ôfbylding, dy't ek komt mei de npm- en garenpakketbehearders. Op myn eigen namme sil ik node-gyp tafoegje, dy't nedich is om guon npm-pakketten te bouwen, en de Brotli-kompressor fan Google, dy't letter foar ús nuttich wêze sil.

Dockerfile mei opmerkings.

# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <[email protected]>"
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

Hjir al fjochtsje ik foar minimalisme, dus it byld wurdt gearstald troch ien grut team.

De ôfmakke ôfbylding is hjir te finen: https://hub.docker.com/r/alexxxnf/spa-builder. Hoewol ik advisearje net te fertrouwe op ôfbyldings fan oare minsken en jo eigen te sammeljen.

nginx

Jo kinne elke webserver brûke om statyske ynhâld te fersprieden. Ik bin wend om te wurkjen mei nginx, dus ik sil it no brûke.

Nginx hat in offisjele Docker-ôfbylding, mar it hat tefolle modules foar ienfâldige statyske distribúsje. Hokker binne opnommen yn 'e levering kinne wurde sjoen troch in spesjaal team of yn' e offisjele 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

Ik sil de Dockerfile as basis brûke, mar ik sil der allinich yn litte wat nedich is om statyske ynhâld te fersprieden. Myn ferzje sil net kinne wurkje oer HTTPS, sil gjin autorisaasje stypje, en folle mear. Mar myn ferzje sil by steat wêze om te fersprieden triemmen komprimearre mei de Brotli algoritme, dat is wat effisjinter as gzip. Wy sille bestannen ien kear komprimearje; d'r is gjin need om dit op 'e flecht te dwaan.

Dit is de Dockerfile wêrmei't ik einige. Opmerkings yn it Russysk binne myn, yn it Ingelsk - fan it orizjineel.

dockerfile

# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <[email protected]>"
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;"]

Ik sil nginx.conf fuortendaliks reparearje sadat gzip en brotli standert ynskeakele binne. Ik sil ek caching-koppen opnimme, om't wy noait statysk feroarje sille. En de lêste touch sil wêze om alle 404-oanfragen troch te lieden nei index.html, dit is nedich foar navigaasje yn 'e 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";
            }
        }
    }
}

Jo kinne de ôfmakke ôfbylding hjir downloade: https://hub.docker.com/r/alexxxnf/nginx-spa. It nimt 10,5 MB op. De orizjinele nginx naam 19,7 MB op. Myn sportive belangstelling is tefreden.

Teaching statics te begripen omjouwingsfariabelen

Wêrom kinne ynstellingen nedich wêze yn SPA? Bygelyks, om oan te jaan hokker RESTful API te brûken. Typysk wurde ynstellings foar de winske omjouwing oerdroegen oan SPA op it boustadium. As jo ​​​​wat moatte feroarje, moatte jo de applikaasje opnij opbouwe. Ik wol it net. Ik wol dat de applikaasje wurdt boud ien kear op 'e CI-poadium, en konfigurearre safolle as nedich op' e CD-poadium mei help fan omjouwingsfariabelen.

Fansels begripe statyske bestannen sels gjin omjouwingsfariabelen. Dêrom moatte jo in trúk brûke. Yn 'e definitive ôfbylding sil ik nginx net starte, mar in spesjaal shell-skript dat omjouwingsfariabelen sil lêze, skriuw se nei statyske bestannen, komprimearje se, en pas dan kontrôle oer nei nginx.

Foar dit doel leveret de Dockerfile de ENTRYPOINT-parameter. Litte wy him it folgjende skript jaan (mei Angular as foarbyld):

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 "

quot;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


quot;); 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 "$@"

Om it skript syn wurk te dwaan, moatte de ynstellingen skreaun wurde yn 'e js-bestannen yn dit formulier: ${API_URL}.

It is de muoite wurdich op te merken dat de measte moderne SPA's hashes tafoegje oan har bestannen by it bouwen. Dit is nedich sadat de blêder it bestân foar in lange tiid feilich kin cache. As de triem feroaret, dan sil syn hash feroarje, wat op syn beurt de browser twingt om it bestân opnij te downloaden.

Spitigernôch, yn myn metoade, it feroarjen fan de konfiguraasje troch omjouwingsfariabelen liedt net ta in feroaring yn 'e triemhash, wat betsjut dat de browser-cache op in oare manier ûnjildich wêze moat. Ik haw dit probleem net, om't ferskate konfiguraasjes wurde ynset yn ferskate omjouwings.

It gearstallen fan it definitive byld

Úteinlik.

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;"]

No kin de resultearjende ôfbylding wurde gearstald en oeral brûkt.

Boarne: www.habr.com