Imazhi i dokerit për shpërndarjen e aplikacionit me një faqe

Aplikacioni me një faqe (SPA) është një grup skedarësh statik JavaScript dhe HTML, si dhe imazhe dhe burime të tjera. Për shkak se ato nuk ndryshojnë në mënyrë dinamike, publikimi i tyre në internet është shumë i lehtë. Ka një numër të madh shërbimesh të lira dhe madje falas për këtë, duke filluar me një faqe të thjeshtë GitHub (dhe për disa edhe me narod.ru) dhe duke përfunduar me një CDN si Amazon S3. Megjithatë, më duhej diçka tjetër.

Më duhej një imazh Docker me SPA në mënyrë që të mund të lansohej lehtësisht si në prodhim si pjesë e një grupi Kubernetes, ashtu edhe në makinën e një zhvilluesi të fundit që nuk e ka idenë se çfarë është SPA.

Unë kam përcaktuar kërkesat e mëposhtme të imazhit për veten time:

  • lehtësia e përdorimit (por jo montimi);
  • madhësia minimale si për sa i përket diskut ashtu edhe RAM-it;
  • konfigurimi përmes variablave të mjedisit në mënyrë që imazhi të mund të përdoret në mjedise të ndryshme;
  • shpërndarja më efikase e skedarëve.

Sot do t'ju tregoj se si:

  • nginx e zorrëve;
  • ndërtoni brotli nga burimet;
  • të mësojë skedarët statikë për të kuptuar variablat e mjedisit;
  • dhe sigurisht si të montoni një imazh Docker nga e gjithë kjo.

Qëllimi i këtij artikulli është të ndaj përvojën time dhe të provokojë anëtarët me përvojë të komunitetit për kritika konstruktive.

Ndërtimi i një imazhi për montim

Për ta bërë imazhin përfundimtar Docker në madhësi të vogël, duhet t'i përmbaheni dy rregullave: një minimum shtresash dhe një imazh bazë minimalist. Një nga imazhet bazë më të vogla është imazhi Alpine Linux, kështu që unë do të zgjedh këtë. Disa mund të argumentojnë se Alpine nuk është e përshtatshme për prodhim dhe mund të kenë të drejtë. Por personalisht nuk kam pasur asnjëherë probleme me të dhe nuk ka asnjë argument kundër tij.

Për të pasur më pak shtresa, unë do ta mbledh imazhin në 2 faza. E para është një draft; të gjitha shërbimet ndihmëse dhe skedarët e përkohshëm do të mbeten në të. Dhe në fazën përfundimtare do të shkruaj vetëm versionin përfundimtar të aplikacionit.

Le të fillojmë me imazhin ndihmës.

Për të përpiluar një aplikacion SPA, zakonisht ju nevojitet node.js. Do të marr imazhin zyrtar, i cili vjen gjithashtu me menaxherët e paketave npm dhe fijeve. Në emrin tim, unë do të shtoj node-gyp, i cili nevojitet për të ndërtuar disa paketa npm dhe kompresorin Brotli nga Google, i cili do të jetë i dobishëm për ne më vonë.

Dockerfile me komente.

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

Tashmë këtu po luftoj për minimalizmin, kështu që imazhi është bashkuar nga një ekip i madh.

Imazhi i përfunduar mund të gjendet këtu: https://hub.docker.com/r/alexxxnf/spa-builder. Edhe pse unë rekomandoj të mos mbështeteni në imazhet e njerëzve të tjerë dhe të mbledhni imazhet tuaja.

nginx

Ju mund të përdorni çdo server në internet për të shpërndarë përmbajtje statike. Jam mësuar të punoj me nginx, kështu që do ta përdor tani.

Nginx ka një imazh zyrtar Docker, por ka shumë module për shpërndarje të thjeshtë statike. Cilat janë të përfshira në dorëzim mund të shihen nga një ekip special ose në dosjen zyrtare të Docker.

$ 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

Do të përdor Dockerfile si bazë, por do të lë në të vetëm atë që nevojitet për të shpërndarë përmbajtje statike. Versioni im nuk do të jetë në gjendje të funksionojë mbi HTTPS, nuk do të mbështesë autorizimin dhe shumë më tepër. Por versioni im do të jetë në gjendje të shpërndajë skedarë të ngjeshur me algoritmin Brotli, i cili është pak më efikas se gzip. Ne do të kompresojmë skedarët një herë; nuk ka nevojë ta bëjmë këtë menjëherë.

Ky është Dockerfile me të cilin përfundova. Komentet në rusisht janë të miat, në anglisht - nga origjinali.

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

Do të rregulloj menjëherë nginx.conf në mënyrë që gzip dhe brotli të jenë të aktivizuara si parazgjedhje. Do të përfshij gjithashtu titujt e memorizimit, sepse nuk do të kemi kurrë ndryshim statik. Dhe prekja e fundit do të jetë ridrejtimi i të gjitha 404 kërkesave në index.html, kjo është e nevojshme për lundrimin në SPA.

nginx.konf

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

Ju mund të shkarkoni imazhin e përfunduar këtu: https://hub.docker.com/r/alexxxnf/nginx-spa. Ajo merr 10,5 MB. Nginx origjinal zuri 19,7 MB. Interesi im sportiv është i kënaqur.

Mësimi i statikës për të kuptuar variablat e mjedisit

Pse mund të nevojiten cilësimet në SPA? Për shembull, në mënyrë që të specifikoni se cilin API RESTful të përdorni. Në mënyrë tipike, cilësimet për mjedisin e dëshiruar transferohen në SPA në fazën e ndërtimit. Nëse keni nevojë të ndryshoni diçka, do t'ju duhet të rindërtoni aplikacionin. Unë nuk e dua atë. Unë dua që aplikacioni të ndërtohet një herë në fazën CI dhe të konfigurohet aq sa është e nevojshme në fazën e CD-së duke përdorur variablat e mjedisit.

Natyrisht, vetë skedarët statikë nuk kuptojnë asnjë variabël mjedisi. Prandaj, do t'ju duhet të përdorni një truk. Në imazhin përfundimtar, nuk do të lëshoj nginx, por një skrip të veçantë shell që do të lexojë variablat e mjedisit, do t'i shkruajë ato në skedarë statikë, do t'i kompresojë dhe vetëm atëherë do të transferojë kontrollin në nginx.

Për këtë qëllim, Dockerfile ofron parametrin ENTRYPOINT. Le t'i japim atij skenarin e mëposhtëm (duke përdorur Angular si shembull):

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

Në mënyrë që skripti të bëjë punën e tij, cilësimet duhet të shkruhen në skedarët js në këtë formë: ${API_URL}.

Vlen të përmendet se shumica e SPA-ve moderne shtojnë hash në skedarët e tyre gjatë ndërtimit. Kjo është e nevojshme në mënyrë që shfletuesi të mund të ruajë në mënyrë të sigurt skedarin për një kohë të gjatë. Nëse skedari ndryshon, atëherë hash-i i tij do të ndryshojë, gjë që do të detyrojë shfletuesin të shkarkojë përsëri skedarin.

Fatkeqësisht, në metodën time, ndryshimi i konfigurimit përmes variablave të mjedisit nuk çon në një ndryshim në hash-in e skedarit, që do të thotë se cache e shfletuesit duhet të zhvlerësohet në një mënyrë tjetër. Unë nuk e kam këtë problem sepse konfigurime të ndryshme vendosen në mjedise të ndryshme.

Duke bashkuar imazhin përfundimtar

Së fundi.

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

Tani imazhi që rezulton mund të mblidhet dhe përdoret kudo.

Burimi: www.habr.com