Docker kép az egyoldalas alkalmazás terjesztéséhez

Az egyoldalas alkalmazás (SPA) statikus JavaScript- és HTML-fájlok, valamint képek és egyéb források halmaza. Mivel nem változnak dinamikusan, online közzétételük nagyon egyszerű. Számos olcsó, sőt ingyenes szolgáltatás létezik erre, kezdve egy egyszerű GitHub-oldallal (és egyesek számára még a narod.ru-val is) és egy CDN-nel, például az Amazon S3-mal bezárólag. Azonban valami másra volt szükségem.

Szükségem volt egy SPA-s Docker image-re, hogy könnyen elindítható legyen mind a Kubernetes-fürt részeként élesben, mind pedig egy háttérfejlesztő gépén, akinek fogalma sincs, mi az SPA.

A következő képkövetelményeket határoztam meg magamnak:

  • könnyű használat (de nem összeszerelés);
  • minimális méret mind a lemez, mind a RAM tekintetében;
  • konfiguráció környezeti változókon keresztül, hogy a kép különböző környezetekben használható legyen;
  • a fájlok leghatékonyabb elosztása.

Ma elmondom, hogyan:

  • bél nginx;
  • forrásokból készíts brotlit;
  • statikus fájlok megtanítása a környezeti változók megértésére;
  • és persze mindebből hogyan lehet Docker képet összeállítani.

Ennek a cikknek az a célja, hogy megosszam tapasztalataimat, és építő kritikára sarkallja a közösség tapasztalt tagjait.

Kép készítése összeszereléshez

Ahhoz, hogy a végső Docker-kép kis méretű legyen, két szabályt kell betartania: a minimális rétegszám és a minimalista alapkép. Az egyik legkisebb alapkép az Alpine Linux image, úgyhogy ezt választom. Egyesek azzal érvelhetnek, hogy az Alpine nem alkalmas termelésre, és igazuk is lehet. De személy szerint nekem soha semmi bajom nem volt vele és nincsenek ellene érvek.

Hogy kevesebb legyen a réteg, 2 lépésben állítom össze a képet. Az első egy piszkozat; minden segédprogram és ideiglenes fájl benne marad. A végső szakaszban pedig csak az alkalmazás végleges verzióját írom le.

Kezdjük a segédképpel.

Egy SPA-alkalmazás összeállításához általában a node.js fájlra van szükség. Felveszem a hivatalos képet, ami az npm és a yarn csomagkezelővel is jár. A magam nevében adom hozzá a node-gyp-et, ami néhány npm-csomag felépítéséhez szükséges, valamint a Google Brotli tömörítőjét, ami később hasznos lesz.

Dockerfile megjegyzésekkel.

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

Már itt is a minimalizmusért küzdök, így egy nagy csapat állítja össze az arculatot.

A kész kép itt található: https://hub.docker.com/r/alexxxnf/spa-builder. Bár azt javaslom, hogy ne hagyatkozzon mások képeire, és gyűjtse össze a sajátját.

nginx

Bármilyen webszervert használhat statikus tartalom terjesztésére. Megszoktam, hogy nginx-szel dolgozom, ezért most azt fogom használni.

Az Nginx-nek hivatalos Docker-képe van, de túl sok modult tartalmaz az egyszerű statikus terjesztéshez. Hogy melyiket tartalmazza a szállítás, azt egy speciális csapat vagy a hivatalos Dockerfile megtekintheti.

$ 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

A Dockerfile-t fogom alapul venni, de csak azt hagyom meg benne, ami a statikus tartalom terjesztéséhez szükséges. Az én verzióm nem fog tudni működni HTTPS-en keresztül, nem támogatja az engedélyezést és még sok más. De az én verzióm képes lesz terjeszteni a Brotli algoritmussal tömörített fájlokat, ami valamivel hatékonyabb, mint a gzip. A fájlokat egyszer tömörítjük, ezt nem kell menet közben megtenni.

Ez az a Dockerfile, amivel végeztem. Az orosz megjegyzések az enyémek, angolul - az eredetiből.

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

Azonnal kijavítom az nginx.conf fájlt, hogy a gzip és a brotli alapértelmezés szerint engedélyezve legyen. A gyorsítótárazási fejléceket is beleteszem, mert soha nem változtatunk statikusan. Az utolsó simítás pedig az lesz, hogy mind a 404 kérést átirányítjuk az index.html oldalra, ez szükséges az SPA-ban való navigáláshoz.

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

A kész képet innen tudod letölteni: https://hub.docker.com/r/alexxxnf/nginx-spa. 10,5 MB-ot foglal. Az eredeti nginx 19,7 MB-ot foglalt el. Sportos érdeklődésem elégedett.

Statika tanítása a környezeti változók megértéséhez

Miért lehet szükség beállításokra a SPA-ban? Például annak meghatározásához, hogy melyik RESTful API-t használja. A kívánt környezet beállításai általában az összeállítási szakaszban kerülnek át az SPA-ba. Ha módosítania kell valamit, akkor újra kell építenie az alkalmazást. Nem akarom. Azt szeretném, ha az alkalmazást egyszer a CI szakaszban készítenék el, és a CD szakaszban környezeti változók segítségével konfigurálják a szükséges mértékben.

Természetesen maguk a statikus fájlok sem értenek meg semmilyen környezeti változót. Ezért egy trükköt kell alkalmaznia. A végső képen nem az nginx-et fogom elindítani, hanem egy speciális shell scriptet, ami beolvassa a környezeti változókat, statikus fájlokba írja, tömöríti, és csak ezután adja át a vezérlést az nginx-nek.

Erre a célra a Dockerfile biztosítja az ENTRYPOINT paramétert. Adjuk meg neki a következő szkriptet (példaként az Angular használatával):

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

Ahhoz, hogy a szkript elvégezze a feladatát, a beállításokat a következő formában kell beírni a js fájlba: ${API_URL}.

Érdemes megjegyezni, hogy a legtöbb modern SPA kivonatokat ad a fájljaihoz építéskor. Ez azért szükséges, hogy a böngésző biztonságosan tárolhassa a fájlt hosszú ideig. Ha a fájl megváltozik, akkor a hash megváltozik, ami viszont arra kényszeríti a böngészőt, hogy újra letöltse a fájlt.

Sajnos az én módszeremben a konfiguráció környezeti változókon keresztül történő megváltoztatása nem vezet a fájl hash változásához, ami azt jelenti, hogy a böngésző gyorsítótárát más módon kell érvényteleníteni. Nincs ez a probléma, mert különböző konfigurációk vannak telepítve különböző környezetekben.

A végső kép összeállítása

Végül.

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

Most a kapott kép összeállítható és bárhol felhasználható.

Forrás: will.com