Docker Bild fir Verdeelung vun Single Page Applikatioun

Single-page Application (SPA) ass e Set vu statesche JavaScript an HTML Dateien, souwéi Biller an aner Ressourcen. Well se net dynamesch änneren, ass se online ze publizéieren ganz einfach. Et ginn eng grouss Zuel vu bëlleg an esouguer gratis Servicer fir dës, ugefaange mat engem einfache GitHub Säiten (a fir e puer souguer mat narod.ru) an Enn mat engem CDN wéi Amazon S3. Ech hunn awer eppes anescht gebraucht.

Ech brauch en Docker-Bild mat SPA fir datt et einfach an der Produktioun als Deel vun engem Kubernetes-Cluster gestart ka ginn, an op der Maschinn vun engem Back-End Entwéckler deen keng Ahnung huet wat SPA ass.

Ech hunn déi folgend Bildfuerderunge fir mech selwer bestëmmt:

  • Einfachheet vun benotzen (awer net Assemblée);
  • Minimum Gréisst souwuel am Sënn vun Disk an RAM;
  • Konfiguratioun duerch Ëmweltvariablen, sou datt d'Bild a verschiddenen Ëmfeld benotzt ka ginn;
  • déi effizient Verdeelung vu Dateien.

Haut wäert ech Iech soen wéi:

  • gutt nginx;
  • bauen brotli aus Quellen;
  • léiere statesch Dateien fir Ëmfeldvariablen ze verstoen;
  • an natierlech wéi een en Docker Bild aus all deem zesummesetzt.

Den Zweck vun dësem Artikel ass meng Erfahrung ze deelen an erfuerene Gemeinschaftsmemberen zu konstruktiv Kritik ze provozéieren.

Bauen e Bild fir Assemblée

Fir dat lescht Docker-Bild kleng a Gréisst ze maachen, musst Dir zwee Reegelen halen: e Minimum vu Schichten an e minimalistescht Basisbild. Ee vun de klengste Basisbiller ass den Alpine Linux Bild, also dat ass wat ech wielen. E puer kënne plädéieren datt d'Alpine net fir d'Produktioun gëeegent ass, a si kënne richteg sinn. Awer perséinlech hunn ech ni Problemer mat him an et gi keng Argumenter géint hien.

Fir manner Schichten ze hunn, sammelen ech d'Bild an 2 Etappen. Déi éischt ass en Entworf; all Hëllefs-Utilities an temporär Dateie bleiwen dran. An an der leschter Etapp wäert ech nëmmen déi lescht Versioun vun der Applikatioun opschreiwen.

Loosst eis mam Hilfsbild ufänken.

Fir eng SPA Applikatioun ze kompiléieren, brauch Dir normalerweis node.js. Ech huelen dat offiziellt Bild, dat och mat den npm- a Garnpaketmanager kënnt. Op mengem Numm wäert ech Node-Gyp addéieren, wat gebraucht gëtt fir e puer npm Packagen ze bauen, an de Brotli Kompressor vu Google, deen eis spéider nëtzlech wäert sinn.

Dockerfile mat Kommentaren.

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

Schonn hei kämpfen ech fir de Minimalismus, also gëtt d'Bild vun enger grousser Equipe zesummegesat.

Dat fäerdegt Bild fannt Dir hei: https://hub.docker.com/r/alexxxnf/spa-builder. Och wann ech recommandéieren net op aner Leit hir Biller ze vertrauen an Är eege ze sammelen.

nginx

Dir kënnt all Webserver benotze fir statesch Inhalt ze verdeelen. Ech si gewinnt mat nginx ze schaffen, also wäert ech et elo benotzen.

Nginx huet en offiziellen Docker Bild, awer et huet ze vill Moduler fir einfach statesch Verdeelung. Wéi eng an der Liwwerung abegraff sinn, kënne vun engem speziellen Team oder an der offizieller Dockerfile gesi ginn.

$ 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

Ech wäert d'Dockerfile als Basis benotzen, awer ech loossen et nëmmen dat wat néideg ass fir statesch Inhalt ze verdeelen. Meng Versioun wäert net fäeg sinn iwwer HTTPS ze schaffen, wäert keng Autorisatioun ënnerstëtzen, a vill méi. Awer meng Versioun wäert fäeg sinn Dateien ze verdeelen, déi mam Brotli Algorithmus kompriméiert sinn, wat e bësse méi effizient ass wéi gzip. Mir kompriméieren d'Dateien eemol; et ass net néideg dëst op der Flucht ze maachen.

Dëst ass den Dockerfile mat deem ech opgehalen hunn. Kommentaren op Russesch sinn meng, op Englesch - aus dem Original.

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

Ech fixéieren direkt nginx.conf sou datt gzip a brotli als Standard aktivéiert sinn. Ech wäert och Caching Header enthalen, well mir wäerten ni statesch änneren. An déi lescht Touch wäert sinn all 404 Ufroen op index.html ze redirectéieren, dëst ass néideg fir d'Navigatioun am 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";
            }
        }
    }
}

Dir kënnt dat fäerdegt Bild hei eroflueden: https://hub.docker.com/r/alexxxnf/nginx-spa. Et hëlt 10,5 MB. Den ursprénglechen nginx huet 19,7 MB opgeholl. Mäi sportlechen Interessi ass zefridden.

Statik léieren fir Ëmfeldvariablen ze verstoen

Firwat kënnen Astellungen am SPA gebraucht ginn? Zum Beispill, fir ze spezifizéieren wéi eng RESTful API ze benotzen. Typesch ginn Astellunge fir dat gewënscht Ëmfeld op SPA an der Baustadium transferéiert. Wann Dir eppes muss änneren, musst Dir d'Applikatioun nei opbauen. Ech wëll et net. Ech wëll d'Applikatioun eemol op der CI Etapp gebaut ginn, an konfiguréiert sou vill wéi néideg op der CD Etapp benotzt Ëmwelt Verännerlechen.

Natierlech verstinn statesch Dateie selwer keng Ëmfeldvariablen. Dofir musst Dir en Trick benotzen. Am leschte Bild wäert ech net nginx starten, awer e spezielle Shell-Skript deen Ëmfeldvariablen liest, schreift se op statesch Dateien, kompriméiert se, an nëmmen dann d'Kontroll op nginx iwwerdroen.

Fir dësen Zweck gëtt den Dockerfile den ENTRYPOINT Parameter. Loosst eis him de folgende Skript ginn (mat Angular als Beispill):

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

Fir de Skript seng Aarbecht ze maachen, mussen d'Astellungen an de js Dateien an dëser Form geschriwwe ginn: ${API_URL}.

Et ass derwäert ze notéieren datt déi meescht modern SPAs Hashes op hir Dateien addéieren beim Bauen. Dëst ass néideg fir datt de Browser d'Datei fir eng laang Zäit sécher kache kann. Wann d'Datei ännert, da ännert säin Hash, wat am Tour de Browser forcéiert d'Datei erëm erofzelueden.

Leider, a menger Method, d'Ännerung vun der Konfiguratioun duerch Ëmfeldvariablen féiert net zu enger Ännerung vum Dateihash, dat heescht datt de Browser-Cache op eng aner Manéier ongëlteg muss ginn. Ech hunn dëse Problem net well verschidde Konfiguratiounen a verschiddenen Ëmfeld agesat ginn.

D'Finale Bild zesummesetzen

Endlech.

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

Elo kann dat entstinn Bild gesammelt an iwwerall benotzt ginn.

Source: will.com