Image Docker per a distribuzione di l'Applicazione Single Page

L'Applicazione di una sola pagina (SPA) hè un inseme di fugliali JavaScript statichi è HTML, è ancu l'imaghjini è altre risorse. Perchè ùn cambianu micca dinamicamente, a publicazione in linea hè assai faciule. Ci hè un gran numaru di servizii boni è ancu gratuiti per questu, cuminciendu cù una Pagine GitHub simplice (è per certi ancu cù narod.ru) è finisce cù un CDN cum'è Amazon S3. Tuttavia, avia bisognu di qualcosa altru.

Aviu bisognu di una maghjina Docker cù SPA in modu chì puderia esse facilmente lanciatu sia in produzzione cum'è parte di un cluster Kubernetes, sia in a macchina di un sviluppatore back-end chì ùn hà micca idea di ciò chì SPA hè.

Aghju determinatu i seguenti requisiti di l'imaghjini per mè stessu:

  • facilità d'usu (ma micca assemblea);
  • dimensione minima sia in termini di discu è RAM;
  • cunfigurazione per mezu di variabili di l'ambienti per chì l'imaghjini pò esse usatu in diversi ambienti;
  • a distribuzione più efficace di i schedari.

Oghje vi dicu cumu:

  • gut nginx;
  • custruì brotli da fonti;
  • insignà à i schedarii statichi per capiscenu e variabili di l'ambiente;
  • è di sicuru cumu assemble una maghjina Docker da tuttu questu.

U scopu di stu articulu hè di sparte a mo sperienza è pruvucà i membri di a cumunità sperimentati à a critica constructiva.

Custruì una maghjina per l'assemblea

Per fà l'imagine finale di Docker chjuca in grandezza, avete bisognu di aderisce à duie regule: un minimu di strati è una maghjina di basa minimalista. Una di l'imaghjini di basa più chjuche hè l'imaghjini di l'Alpine Linux, cusì hè ciò chì sceglite. Qualchidunu puderia argumentà chì l'Alpine ùn hè micca adattatu per a produzzione, è puderanu esse ghjustu. Ma personalmente, ùn aghju mai avutu prublemi cun ellu è ùn ci sò micca argumenti contru à ellu.

Per avè menu strati, aghjustà l'imaghjini in 2 tappe. U primu hè un abbozzu; tutte e utilità ausiliarie è i fugliali tempurane fermanu in questu. È in a tappa finale scriveraghju solu a versione finale di l'applicazione.

Accuminciamu cù l'imagine ausiliaria.

Per compilà una applicazione SPA, di solitu avete bisognu di node.js. Pigliu l'imagine ufficiale, chì vene ancu cù i gestori di pacchetti npm è filati. In u mo nome, aghju aghjunghje node-gyp, chì hè necessariu per custruisce qualchi pacchetti npm, è u compressore Brotli da Google, chì serà utile per noi dopu.

Dockerfile cù cumenti.

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

Dighjà quì mi batte per u minimalismu, cusì l'imaghjini hè riunitu da una grande squadra.

L'imagine finita pò esse truvata quì: https://hub.docker.com/r/alexxxnf/spa-builder. Ancu s'ellu vi ricumandemu micca di confià in l'imaghjini di l'altri è di cullà u vostru propiu.

nginx

Pudete utilizà qualsiasi servitore web per distribuisce u cuntenutu staticu. Sò abituatu à travaglià cù nginx, cusì l'aghju aduprà avà.

Nginx hà una maghjina Docker ufficiale, ma hà troppu moduli per una distribuzione statica simplice. Quali sò inclusi in a consegna pò esse vistu da una squadra speciale o in u Dockerfile ufficiale.

$ 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

Aduprà u Dockerfile cum'è una basa, ma lasciaraghju solu ciò chì hè necessariu per distribuisce u cuntenutu staticu. A mo versione ùn puderà micca travaglià nantu à HTTPS, ùn sustene micca l'autorizazione, è assai di più. Ma a mo versione serà capace di distribuisce i fugliali cumpressi cù l'algoritmu Brotli, chì hè un pocu più efficau cà gzip. Cumpresseremu i fugliali una volta; ùn ci hè bisognu di fà questu nantu à a mosca.

Questu hè u Dockerfile chì aghju finitu. I cumenti in russo sò i mio, in inglese - da l'uriginale.

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

Fixaraghju immediatamente nginx.conf in modu chì gzip è brotli sò attivati ​​per automaticamente. Includeraghju ancu intestazioni di caching, perchè ùn avemu mai cambiatu static. È u toccu finali serà di redirige tutte e dumande 404 à index.html, questu hè necessariu per a navigazione in u 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";
            }
        }
    }
}

Pudete scaricà l'imagine finita quì: https://hub.docker.com/r/alexxxnf/nginx-spa. Ci vole 10,5 MB. U nginx originale hà pigliatu 19,7 MB. U mo interessu sportivu hè cuntentu.

Insegnamentu di statics per capiscenu e variabili di l'ambiente

Perchè i paràmetri ponu esse necessarii in SPA? Per esempiu, per specificà quale API RESTful à aduprà. Di genere, i paràmetri per l'ambiente desideratu sò trasferiti à SPA in a fase di creazione. Sè avete bisognu di cambià qualcosa, avete da ricustruisce l'applicazione. Ùn vogliu micca. Vogliu chì l'appiecazione sia custruita una volta in u stadiu CI, è cunfigurata quant'è necessariu in u stadiu CD usendu variabili di l'ambiente.

Di sicuru, i schedari statichi stessi ùn capiscenu micca e variabili di l'ambiente. Per quessa, vi tuccherà à aduprà un truccu. In l'imaghjini finali, ùn aghju micca lanciatu nginx, ma un script di cunchiglia speciale chì leghje variabili di l'ambienti, scrivite à i schedarii statici, cumpressione, è solu dopu trasfirì u cuntrollu à nginx.

Per questu scopu, u Dockerfile furnisce u paràmetru ENTRYPOINT. Damu u seguente script (usendu Angular cum'è un esempiu):

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

Per chì u script faci u so travagliu, i paràmetri devenu esse scritti in i schedari js in questa forma: ${API_URL}.

Hè da nutà chì a maiò parte di i SPA muderni aghjunghjenu hashes à i so fugliali quandu custruiscenu. Questu hè necessariu per chì u navigatore pò cache in cache u schedariu per un bellu pezzu. Se u schedariu cambia, allora u so hash cambierà, chì à u turnu forzarà u navigatore à scaricà u schedariu di novu.

Sfortunatamente, in u mo metudu, cambià a cunfigurazione per mezu di variabili di l'ambienti ùn porta micca à un cambiamentu in u file hash, chì significa chì u cache di u navigatore deve esse invalidatu in qualchì altru modu. Ùn aghju micca stu prublema perchè diverse cunfigurazioni sò implementate in ambienti differenti.

Aghjunghjendu l'imaghjini finali

Infine.

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

Avà l'imaghjini resultanti ponu esse assemblati è utilizati in ogni locu.

Source: www.habr.com