Docker-bildo por distribuado de Unupaĝa Apliko

Unupaĝa Apliko (SPA) estas aro de senmovaj JavaScript kaj HTML-dosieroj, same kiel bildoj kaj aliaj rimedoj. Ĉar ili ne ŝanĝiĝas dinamike, publikigi ilin interrete estas tre facila. Estas granda nombro da malmultekostaj kaj eĉ senpagaj servoj por ĉi tio, komencante per simpla GitHub Pages (kaj por iuj eĉ kun narod.ru) kaj finiĝante per CDN kiel Amazon S3. Tamen mi bezonis ion alian.

Mi bezonis Docker-bildon kun SPA, por ke ĝi povu facile esti lanĉita kaj en produktado kiel parto de Kubernetes-areo, kaj sur la maŝino de malantaŭa programisto, kiu ne scias, kio estas SPA.

Mi determinis por mi la sekvajn bildajn postulojn:

  • facileco de uzo (sed ne muntado);
  • minimuma grandeco kaj laŭ disko kaj RAM;
  • agordo per mediovariabloj tiel ke la bildo povas esti uzata en malsamaj medioj;
  • la plej efika distribuado de dosieroj.

Hodiaŭ mi rakontos al vi kiel:

  • gut nginx;
  • konstrui brotli el fontoj;
  • Instruu senmovajn dosierojn kompreni mediajn variablojn;
  • kaj kompreneble kiel kunmeti bildon de Docker el ĉio ĉi.

La celo de ĉi tiu artikolo estas dividi mian sperton kaj provoki spertajn komunumajn membrojn al konstruiva kritiko.

Konstruante bildon por kunigo

Por fari la finan Docker-bildon malgranda en grandeco, vi devas aliĝi al du reguloj: minimumo de tavoloj kaj minimumisma baza bildo. Unu el la plej malgrandaj bazaj bildoj estas la bildo de Alpine Linukso, do tion mi elektos. Iuj povus argumenti, ke la Alpa ne taŭgas por produktado, kaj ili eble pravas. Sed persone, mi neniam havis problemojn kun li kaj ne ekzistas argumentoj kontraŭ li.

Por havi malpli da tavoloj, mi kunvenos la bildon en 2 stadioj. La unua estas skizo; ĉiuj helpaj iloj kaj provizoraj dosieroj restos en ĝi. Kaj en la fina etapo mi nur skribos la finan version de la aplikaĵo.

Ni komencu per la helpa bildo.

Por kompili SPA-aplikaĵon, vi kutime bezonas node.js. Mi prenos la oficialan bildon, kiu ankaŭ venas kun la npm kaj yarn-pakaĵadministriloj. Propre mi aldonos node-gyp, kiu estas necesa por konstrui kelkajn npm-pakaĵojn, kaj la Brotli-kompresoron de Guglo, kiu estos utila al ni poste.

Dockerfile kun komentoj.

# Базовый образ
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

Jam ĉi tie mi batalas por minimumismo, do la bildo estas kunmetita de unu granda teamo.

La preta bildo troveblas ĉi tie: https://hub.docker.com/r/alexxxnf/spa-builder. Kvankam mi rekomendas ne fidi al aliaj bildoj kaj kolekti viajn proprajn.

nginx

Vi povas uzi ajnan retservilon por distribui statikan enhavon. Mi kutimas labori kun nginx, do mi uzos ĝin nun.

Nginx havas oficialan bildon de Docker, sed ĝi havas tro multajn modulojn por simpla senmova distribuo. Kiuj estas inkluzivitaj en la transdono povas esti viditaj de speciala teamo aŭ en la oficiala 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

Mi uzos la Dockerfile kiel bazon, sed mi lasos en ĝi nur tion, kion oni bezonas por distribui statikan enhavon. Mia versio ne povos funkcii per HTTPS, ne subtenos rajtigon, kaj multe pli. Sed mia versio povos distribui dosierojn kunpremitajn per la algoritmo Brotli, kiu estas iomete pli efika ol gzip. Ni kunpremos dosierojn unufoje; ne necesas fari tion sur la flugo.

Ĉi tiu estas la Dockerfile, kun kiu mi finis. Komentoj en la rusa estas miaj, en la angla - el la originalo.

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

Mi tuj riparos nginx.conf por ke gzip kaj brotli estu ebligitaj defaŭlte. Mi ankaŭ inkluzivos kaŝmemorajn kapliniojn, ĉar ni neniam havos ŝanĝiĝantan statikan. Kaj la fina tuŝo estos redirekti ĉiujn 404 petojn al index.html, tio estas necesa por navigado en la 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";
            }
        }
    }
}

Vi povas elŝuti la finitan bildon ĉi tie: https://hub.docker.com/r/alexxxnf/nginx-spa. Ĝi okupas 10,5 MB. La origina nginx okupis 19,7 MB. Mia sporta intereso estas kontentigita.

Instruado de statiko por kompreni mediajn variablojn

Kial povus bezoni agordojn en SPA? Ekzemple, por specifi kiun RESTful API uzi. Tipe, agordoj por la dezirata medio estas transdonitaj al SPA en la konstrustadio. Se vi bezonas ŝanĝi ion, vi devos rekonstrui la aplikaĵon. Mi ne volas ĝin. Mi volas, ke la aplikaĵo estu konstruita unufoje ĉe la CI-stadio, kaj agordita tiom multe kiom necesas ĉe la KD-stadio uzante mediovariablojn.

Kompreneble, statikaj dosieroj mem ne komprenas iujn ajn mediovariablojn. Sekve, vi devos uzi lertaĵon. En la fina bildo, mi ne lanĉos nginx, sed specialan ŝelan skripton, kiu legos mediovariablojn, skribos ilin al senmovaj dosieroj, kunpremos ilin, kaj nur tiam transdonos kontrolon al nginx.

Por ĉi tiu celo, la Dockerfile disponigas la ENTRYPOINT-parametron. Ni donu al li la sekvan skripton (uzante Angular kiel ekzemplon):

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 "$@"

Por ke la skripto faru sian laboron, la agordoj devas esti skribitaj en la js-dosieroj en ĉi tiu formo: ${API_URL}.

Indas noti, ke plej modernaj SPA-oj aldonas haŝojn al siaj dosieroj dum konstruado. Ĉi tio estas necesa por ke la retumilo povu sekure konservi la dosieron dum longa tempo. Se la dosiero ŝanĝiĝos, tiam ĝia hash ŝanĝiĝos, kio siavice devigos la retumilon elŝuti la dosieron denove.

Bedaŭrinde, laŭ mia metodo, ŝanĝi la agordon per medio-variabloj ne kondukas al ŝanĝo en la dosierhaŝiŝo, kio signifas, ke la retumila kaŝmemoro devas esti malvalidigita alimaniere. Mi ne havas ĉi tiun problemon ĉar malsamaj agordoj estas deplojitaj en malsamaj medioj.

Kunmeti la finan bildon

Fine.

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

Nun la rezulta bildo povas esti kunvenita kaj uzata ie ajn.

fonto: www.habr.com