Wêneyê Docker ji bo belavkirina Serlêdana Yek Rûpelê

Serlêdana Yek-rûpel (SPA) komek pelên JavaScript û HTML-a statîk e, her weha wêne û çavkaniyên din. Ji ber ku ew bi dînamîk nayên guhertin, weşandina wan li ser înternetê pir hêsan e. Ji bo vê yekê hejmareke mezin karûbarên erzan û hetta belaş hene, ji Rûpelên GitHub-ê yên hêsan (û ji bo hinan jî bi narod.ru re) dest pê dikin û bi CDNek mîna Amazon S3-ê diqedin. Lêbelê, min tiştek din hewce bû.

Ji min re wêneyek Docker bi SPA re hewce bû da ku ew bi hêsanî hem di hilberînê de wekî beşek ji komek Kubernetes, hem jî li ser makîneya pêşdebirek paşîn a ku nizane SPA çi ye were destpêkirin.

Min daxwazên wêneya jêrîn ji bo xwe diyar kir:

  • hêsaniya karanînê (lê ne kombûnê);
  • Mezinahiya herî kêm hem di warê dîskê û hem jî RAM de;
  • veavakirina bi guhêrbarên jîngehê da ku wêne di hawîrdorên cûda de were bikar anîn;
  • belavkirina herî bikêr a pelan.

Îro ez ê ji we re bibêjim ka çawa:

  • gut nginx;
  • avakirina brotli ji çavkaniyên;
  • pelên statîk fêr bikin ku guhêrbarên jîngehê fam bikin;
  • û bê guman meriv çawa ji vê yekê wêneyek Docker berhev dike.

Armanca vê gotarê ew e ku ez ezmûna xwe parve bikim û endamên civakê yên xwedî ezmûn berbi rexneyên çêker vekêşim.

Avakirina wêneyek ji bo civînê

Ji bo ku hûn wêneya Docker-a paşîn bi mezinahî piçûktir bikin, hûn hewce ne ku du qaîdeyan bişopînin: kêmtirîn qat û wêneyek bingehîn a mînîmalîst. Yek ji piçûktirîn wêneyên bingehîn wêneya Alpine Linux e, ji ber vê yekê ya ku ez ê bibijêrim ev e. Dibe ku hin arguman bikin ku Alpine ji bo hilberînê ne maqûl e, û dibe ku ew rast bin. Lê ez bi xwe tu carî bi wî re tu pirsgirêk derneketiye û tu argumanên wî tune ne.

Ji bo ku kêmtir qatan hebin, ez ê wêneyê di 2 qonaxan de bicivînim. Ya yekem pêşnûmeyek e; hemî karûbarên alîkar û pelên demkî dê tê de bimînin. Û di qonaxa dawî de ez ê tenê guhertoya dawî ya serîlêdanê binivîsim.

Ka em bi wêneya alîkar dest pê bikin.

Ji bo ku hûn serîlêdanek SPA berhev bikin, hûn bi gelemperî hewceyê node.js in. Ez ê wêneya fermî bigirim, ku di heman demê de bi rêvebirên pakêta npm û yarn re jî tê. Li ser navê xwe, ez ê node-gyp, ku ji bo avakirina hin pakêtên npm hewce dike, û kompresorê Brotli ji Google, ku dê paşê ji me re kêrhatî be lê zêde bikim.

Dockerfile bi şîroveyan.

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

Jixwe li vir ez ji bo minimalîzmê şer dikim, ji ber vê yekê wêne ji hêla yek tîmek mezin ve hatî berhev kirin.

Wêneyê qedandî li vir tê dîtin: https://hub.docker.com/r/alexxxnf/spa-builder. Her çend ez pêşniyar dikim ku pişta xwe nedin wêneyên kesên din û yên xwe berhev bikin.

nginx

Hûn dikarin her serverek malperê bikar bînin da ku naveroka statîk belav bikin. Ez bi nginx re xebitîm, ji ber vê yekê ez ê niha bikar bînim.

Nginx xwedan wêneyek fermî ya Docker e, lê ji bo belavkirina statîk a hêsan gelek modulên wê hene. Kîjan yên ku di radestkirinê de cih digirin dikarin ji hêla tîmek taybetî an di Dockerfile ya fermî de werin dîtin.

$ 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

Ez ê Dockerfile wekî bingehek bikar bînim, lê ez ê tenê tiştê ku ji bo belavkirina naveroka statîk hewce dike tê de bihêlim. Guhertoya min dê nikaribe li ser HTTPS bixebite, dê destûrnameyê piştgirî neke, û hêj bêtir. Lê guhertoya min dê bikaribe pelên ku bi algorîtmaya Brotli ve hatî berhev kirin, ku hinekî ji gzip-ê bikêrtir e, belav bike. Em ê yek carî pelan berhev bikin; ne hewce ye ku meriv vê yekê bi lez bike.

Ev Dockerfile ye ku min pê re qedand. Şîroveyên bi rûsî yên min in, bi îngilîzî - ji orîjînal.

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

Ez ê tavilê nginx.conf sererast bikim da ku gzip û brotli ji hêla xwerû ve werin çalak kirin. Ez ê sernavên cachkirinê jî bigirim, ji ber ku em ê ti carî statîk neguherînin. Û pêwendiya dawîn dê bibe beralîkirina hemî 404 daxwazan ber bi index.html ve, ev ji bo navîgasyon li SPA-yê hewce ye.

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

Hûn dikarin wêneya qedandî li vir dakêşin: https://hub.docker.com/r/alexxxnf/nginx-spa. Ew 10,5 MB digire. Nginx a orîjînal 19,7 MB girt. Eleqeya min a werzîşê têr e.

Hînkirina statîkê ji bo têgihiştina guhêrbarên jîngehê

Çima dibe ku mîhengên li SPA hewce ne? Mînakî, ji bo ku hûn diyar bikin ka kîjan RESTful API bikar bînin. Bi gelemperî, mîhengên ji bo hawîrdora xwestinê di qonaxa çêkirinê de ji SPA re têne veguheztin. Heke hûn hewce ne ku tiştek biguhezînin, hûn ê neçar bin ku serîlêdanê ji nû ve ava bikin. Ez naxwazim. Ez dixwazim ku serîlêdan di qonaxa CI-ê de carekê were çêkirin, û di qonaxa CD-yê de bi karanîna guhêrbarên jîngehê bi qasî ku hewce be were mîheng kirin.

Bê guman, pelên statîk bixwe guhêrbarên hawîrdorê fam nakin. Ji ber vê yekê, hûn ê neçar bibin ku hîleyek bikar bînin. Di wêneya paşîn de, ez ê ne nginx dest pê bikim, lê skrîptek şêlek taybetî ya ku dê guhêrbarên hawîrdorê bixwîne, wan li pelên statîk binivîsîne, wan tevlihev bike, û tenê hingê kontrolê veguhezîne nginx.

Ji bo vê armancê, Dockerfile parametreya ENTRYPOINT peyda dike. Ka em skrîpta jêrîn bidin wî (bi karanîna Angular wekî mînak):

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

Ji bo ku skrîpt karê xwe bike, divê mîhengan di pelên js de bi vî rengî bêne nivîsandin: ${API_URL}.

Hêjayî gotinê ye ku piraniya SPA-yên nûjen dema ku ava dikin hashes li pelên xwe zêde dikin. Ev pêdivî ye ku gerok dikare bi ewlehî pelê ji bo demek dirêj veşêre. Ger pel biguhere, wê hingê haşa wê biguhere, ku di encamê de dê gerok neçar bike ku pelê dîsa dakêşîne.

Mixabin, di rêbaza min de, guheztina veavakirinê bi guhêrbarên hawîrdorê ve nahêle ku pelê pelê biguhezîne, ku tê vê wateyê ku cache gerok divê bi rengek din were betal kirin. Min ev pirsgirêk nîne ji ber ku mîhengên cihêreng li hawîrdorên cihêreng têne bicîh kirin.

Wêneyê dawî li hev dixin

Paşan.

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

Naha wêneya encam dikare were berhev kirin û li her deverê were bikar anîn.

Source: www.habr.com