Docker-billede til distribution af Single Page Application

Single-page Application (SPA) er et sæt statiske JavaScript- og HTML-filer samt billeder og andre ressourcer. Fordi de ikke ændrer sig dynamisk, er det meget nemt at udgive dem online. Der er et stort antal billige og endda gratis tjenester til dette, startende med en simpel GitHub Pages (og for nogle endda med narod.ru) og slutter med en CDN som Amazon S3. Jeg havde dog brug for noget andet.

Jeg havde brug for et Docker-image med SPA, så det nemt kunne lanceres både i produktion som en del af en Kubernetes-klynge og på maskinen hos en back-end-udvikler, der ikke aner, hvad SPA er.

Jeg har bestemt følgende billedkrav til mig selv:

  • brugervenlighed (men ikke montering);
  • minimumsstørrelse både med hensyn til disk og RAM;
  • konfiguration gennem miljøvariabler, så billedet kan bruges i forskellige miljøer;
  • den mest effektive distribution af filer.

I dag vil jeg fortælle dig hvordan:

  • tarm nginx;
  • bygge brotli fra kilder;
  • lære statiske filer at forstå miljøvariabler;
  • og selvfølgelig hvordan man samler et Docker-billede fra alt dette.

Formålet med denne artikel er at dele mine erfaringer og provokere erfarne medlemmer af samfundet til konstruktiv kritik.

Opbygning af et billede til montering

For at gøre det endelige Docker-billede lille i størrelse, skal du overholde to regler: et minimum af lag og et minimalistisk basisbillede. Et af de mindste basisbilleder er Alpine Linux-billedet, så det er det, jeg vælger. Nogle vil måske hævde, at Alpine ikke er egnet til produktion, og de kan have ret. Men personligt har jeg aldrig haft problemer med ham, og der er ingen argumenter imod ham.

For at have færre lag, vil jeg samle billedet i 2 trin. Den første er et udkast; alle hjælpeprogrammer og midlertidige filer forbliver i den. Og i sidste fase vil jeg kun skrive den endelige version af ansøgningen ned.

Lad os starte med hjælpebilledet.

For at kompilere en SPA-applikation har du normalt brug for node.js. Jeg tager det officielle billede, som også følger med npm og garnpakkemanagere. På mine egne vegne vil jeg tilføje node-gyp, som er nødvendig for at bygge nogle npm-pakker, og Brotli-kompressoren fra Google, som vil være nyttig for os senere.

Dockerfil med kommentarer.

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

Allerede her kæmper jeg for minimalisme, så billedet er sat sammen af ​​ét stort hold.

Det færdige billede kan findes her: https://hub.docker.com/r/alexxxnf/spa-builder. Selvom jeg anbefaler ikke at stole på andres billeder og samle dine egne.

Nginx

Du kan bruge enhver webserver til at distribuere statisk indhold. Jeg er vant til at arbejde med nginx, så jeg vil bruge det nu.

Nginx har et officielt Docker-billede, men det har for mange moduler til simpel statisk distribution. Hvilke der er inkluderet i leveringen kan ses af et særligt team eller i den officielle Dockerfile.

$ 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

Jeg vil bruge Dockerfilen som grundlag, men jeg vil kun efterlade det, der er nødvendigt for at distribuere statisk indhold. Min version vil ikke kunne fungere over HTTPS, vil ikke understøtte godkendelse og meget mere. Men min version vil være i stand til at distribuere filer komprimeret med Brotli-algoritmen, som er lidt mere effektiv end gzip. Vi komprimerer filer én gang; der er ingen grund til at gøre dette med det samme.

Dette er den Dockerfile, jeg endte med. Kommentarer på russisk er mine, på engelsk - fra originalen.

Dockerfil

# Базовый образ снова 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;"]

Jeg vil straks rette nginx.conf, så gzip og brotli er aktiveret som standard. Jeg vil også inkludere caching-headers, fordi vi vil have aldrig skiftende statisk. Og den sidste touch vil være at omdirigere alle 404 anmodninger til index.html, dette er nødvendigt for navigation i 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";
            }
        }
    }
}

Du kan downloade det færdige billede her: https://hub.docker.com/r/alexxxnf/nginx-spa. Den fylder 10,5 MB. Den originale nginx fyldte 19,7 MB. Min sportslige interesse er tilfredsstillet.

Undervisning i statik til at forstå miljøvariabler

Hvorfor kan indstillinger være nødvendige i SPA? For eksempel for at specificere hvilken RESTful API der skal bruges. Typisk overføres indstillinger for det ønskede miljø til SPA på byggestadiet. Hvis du skal ændre noget, bliver du nødt til at genopbygge applikationen. Jeg vil ikke have det. Jeg ønsker, at applikationen skal bygges én gang på CI-stadiet og konfigureres så meget som nødvendigt på CD-stadiet ved hjælp af miljøvariabler.

Selvfølgelig forstår statiske filer ikke selv nogen miljøvariabler. Derfor bliver du nødt til at bruge et trick. I det endelige billede vil jeg ikke starte nginx, men et specielt shell-script, der vil læse miljøvariabler, skrive dem til statiske filer, komprimere dem og først derefter overføre kontrol til nginx.

Til dette formål giver Dockerfilen parameteren ENTRYPOINT. Lad os give ham følgende script (ved at bruge Angular som eksempel):

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

For at scriptet kan udføre sit arbejde, skal indstillingerne skrives i js-filerne i denne form: ${API_URL}.

Det er værd at bemærke, at de fleste moderne SPA'er tilføjer hashes til deres filer, når de bygger. Dette er nødvendigt for at browseren sikkert kan cache filen i lang tid. Hvis filen ændrer sig, vil dens hash ændre sig, hvilket igen vil tvinge browseren til at downloade filen igen.

Desværre, i min metode, fører ændring af konfigurationen gennem miljøvariabler ikke til en ændring i fil-hash, hvilket betyder, at browserens cache skal ugyldiggøres på en anden måde. Jeg har ikke dette problem, fordi forskellige konfigurationer er implementeret i forskellige miljøer.

Sammensætning af det endelige billede

Endelig.

Dockerfil

# Первый базовый образ для сборки
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;"]

Nu kan det resulterende billede samles og bruges hvor som helst.

Kilde: www.habr.com