Delwedd docwr ar gyfer dosbarthu Cais Tudalen Sengl

Mae Cymhwysiad Tudalen Sengl (SPA) yn set o ffeiliau JavaScript a HTML statig, yn ogystal â delweddau ac adnoddau eraill. Oherwydd nad ydyn nhw'n newid yn ddeinamig, mae'n hawdd iawn eu cyhoeddi ar-lein. Mae yna nifer fawr o wasanaethau rhad a hyd yn oed am ddim ar gyfer hyn, gan ddechrau gyda Tudalennau GitHub syml (ac i rai hyd yn oed gyda narod.ru) ac yn gorffen gyda CDN fel Amazon S3. Fodd bynnag, roedd angen rhywbeth arall arnaf.

Roeddwn i angen delwedd Docker gyda SPA fel y byddai'n hawdd ei lansio wrth gynhyrchu fel rhan o glwstwr Kubernetes, ac ar beiriant datblygwr pen ôl nad oes ganddo unrhyw syniad beth yw SPA.

Rwyf wedi pennu'r gofynion delwedd canlynol i mi fy hun:

  • rhwyddineb defnydd (ond nid cydosod);
  • maint lleiaf o ran disg a RAM;
  • cyfluniad trwy newidynnau amgylchedd fel y gellir defnyddio'r ddelwedd mewn gwahanol amgylcheddau;
  • dosbarthiad mwyaf effeithlon o ffeiliau.

Heddiw byddaf yn dweud wrthych sut:

  • nginx perfedd;
  • adeiladu brotli o ffynonellau;
  • addysgu ffeiliau statig i ddeall newidynnau amgylchedd;
  • ac wrth gwrs sut i gydosod delwedd Docker o hyn i gyd.

Pwrpas yr erthygl hon yw rhannu fy mhrofiad ac ysgogi aelodau profiadol o'r gymuned i feirniadaeth adeiladol.

Adeiladu delwedd ar gyfer cydosod

Er mwyn gwneud delwedd derfynol y Dociwr yn fach o ran maint, mae angen i chi gadw at ddwy reol: lleiafswm o haenau a delwedd sylfaen finimalaidd. Un o'r delweddau sylfaen lleiaf yw delwedd Alpine Linux, felly dyna beth fyddaf yn ei ddewis. Efallai y bydd rhai yn dadlau nad yw'r Alpaidd yn addas ar gyfer cynhyrchu, ac efallai eu bod yn iawn. Ond yn bersonol, nid wyf erioed wedi cael unrhyw broblemau ag ef ac nid oes unrhyw ddadleuon yn ei erbyn.

I gael llai o haenau, byddaf yn cydosod y ddelwedd mewn 2 gam. Drafft yw'r cyntaf; bydd yr holl gyfleustodau ategol a ffeiliau dros dro yn aros ynddo. Ac yn y cam olaf dim ond fersiwn terfynol y cais y byddaf yn ei ysgrifennu.

Gadewch i ni ddechrau gyda'r ddelwedd ategol.

Er mwyn llunio cais SPA, fel arfer mae angen nod.js arnoch. Byddaf yn cymryd y ddelwedd swyddogol, sydd hefyd yn dod gyda'r rheolwyr pecyn npm ac edafedd. Ar fy rhan fy hun, byddaf yn ychwanegu nod-gyp, sydd ei angen i adeiladu rhai pecynnau npm, a'r cywasgydd Brotli o Google, a fydd yn ddefnyddiol i ni yn ddiweddarach.

Ffeil Docker gyda sylwadau.

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

Eisoes dyma fi yn ymladd am finimaliaeth, felly mae'r ddelwedd yn cael ei rhoi at ei gilydd gan un tîm mawr.

Mae'r llun gorffenedig i'w weld yma: https://hub.docker.com/r/alexxxnf/spa-builder. Er fy mod yn argymell peidio â dibynnu ar ddelweddau pobl eraill a chasglu eich rhai eich hun.

nginx

Gallwch ddefnyddio unrhyw weinydd gwe i ddosbarthu cynnwys statig. Rydw i wedi arfer gweithio gyda nginx, felly byddaf yn ei ddefnyddio nawr.

Mae gan Nginx ddelwedd Dociwr swyddogol, ond mae ganddo ormod o fodiwlau ar gyfer dosbarthiad statig syml. Gellir gweld pa rai sydd wedi'u cynnwys yn y dosbarthiad gan dîm arbennig neu yn y Dockerfile swyddogol.

$ docker rhedeg --rm nginx: 1-alpaidd 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

Byddaf yn defnyddio'r Dockerfile fel sail, ond byddaf yn gadael ynddo dim ond yr hyn sydd ei angen i ddosbarthu cynnwys statig. Ni fydd fy fersiwn yn gallu gweithio dros HTTPS, ni fydd yn cefnogi awdurdodiad, a llawer mwy. Ond bydd fy fersiwn yn gallu dosbarthu ffeiliau wedi'u cywasgu â'r algorithm Brotli, sydd ychydig yn fwy effeithlon na gzip. Byddwn yn cywasgu ffeiliau unwaith; nid oes angen gwneud hyn ar y hedfan.

Dyma'r Dockerfile wnes i yn y diwedd. Mae sylwadau yn Rwsieg yn eiddo i mi, yn Saesneg - o'r gwreiddiol.

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

Byddaf yn trwsio nginx.conf ar unwaith fel bod gzip a brotli yn cael eu galluogi yn ddiofyn. Byddaf hefyd yn cynnwys penawdau caching, oherwydd ni fyddwn byth wedi newid yn statig. A'r cyffyrddiad olaf fydd ailgyfeirio pob un o'r 404 cais i index.html, mae hyn yn angenrheidiol ar gyfer llywio yn yr 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";
            }
        }
    }
}

Gallwch lawrlwytho'r llun gorffenedig yma: https://hub.docker.com/r/alexxxnf/nginx-spa. Mae'n cymryd hyd at 10,5 MB. Cymerodd y nginx gwreiddiol 19,7 MB. Mae fy niddordeb chwaraeon yn fodlon.

Addysgu statig i ddeall newidynnau amgylchedd

Pam y gallai fod angen gosodiadau mewn SPA? Er enghraifft, er mwyn nodi pa API RESTful i'w ddefnyddio. Yn nodweddiadol, mae gosodiadau ar gyfer yr amgylchedd dymunol yn cael eu trosglwyddo i SPA yn y cam adeiladu. Os oes angen i chi newid rhywbeth, bydd yn rhaid i chi ailadeiladu'r cais. Nid wyf am ei gael. Rwyf am i'r cais gael ei adeiladu unwaith ar y cam CI, a'i ffurfweddu cymaint ag sy'n angenrheidiol ar y cam CD gan ddefnyddio newidynnau amgylchedd.

Wrth gwrs, nid yw ffeiliau statig eu hunain yn deall unrhyw newidynnau amgylchedd. Felly, bydd yn rhaid i chi ddefnyddio tric. Yn y ddelwedd olaf, ni fyddaf yn lansio nginx, ond sgript cragen arbennig a fydd yn darllen newidynnau amgylchedd, yn eu hysgrifennu i ffeiliau statig, yn eu cywasgu, a dim ond wedyn yn trosglwyddo rheolaeth i nginx.

At y diben hwn, mae'r Dockerfile yn darparu'r paramedr ENTRYPOINT. Gadewch i ni roi'r sgript ganlynol iddo (gan ddefnyddio Angular fel enghraifft):

docwr-mynediad.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 "$@"

Er mwyn i'r sgript wneud ei gwaith, rhaid ysgrifennu'r gosodiadau yn y ffeiliau js yn y ffurflen hon: ${API_URL}.

Mae'n werth nodi bod y rhan fwyaf o SPAs modern yn ychwanegu hashes at eu ffeiliau wrth adeiladu. Mae hyn yn angenrheidiol er mwyn i'r porwr allu storio'r ffeil yn ddiogel am amser hir. Os bydd y ffeil yn newid, yna bydd ei hash yn newid, a fydd yn ei dro yn gorfodi'r porwr i lawrlwytho'r ffeil eto.

Yn anffodus, yn fy dull i, nid yw newid y ffurfweddiad trwy newidynnau amgylchedd yn arwain at newid yn yr hash ffeil, sy'n golygu bod yn rhaid annilysu storfa'r porwr mewn rhyw ffordd arall. Nid oes gennyf y broblem hon oherwydd bod ffurfweddiadau gwahanol yn cael eu defnyddio mewn gwahanol amgylcheddau.

Rhoi'r ddelwedd derfynol at ei gilydd

Yn olaf.

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

Nawr gellir cydosod y ddelwedd ganlyniadol a'i defnyddio yn unrhyw le.

Ffynhonnell: hab.com