Dockerbild för distribution av Single Page Application

Enkeltsidesapplikation (SPA) – är en uppsättning statiska JavaScript- och HTML-filer, liksom bilder och andra resurser. Eftersom de inte ändras dynamiskt, är det mycket enkelt att publicera dem på internet. Det finns ett stort antal billiga och till och med gratis tjänster som erbjuder detta, från enkla GitHub Pages (och för vissa till och med narod.ru) till CDN-lösningar som Amazon S3. Men jag behövde något annat.

Jag behövde en Docker-avbildning med SPA, så att den enkelt kan startas både i produktion som en del av ett Kubernetes-kluster och på maskinen hos en back-end-utvecklare som inte har någon aning om vad en SPA är.

Jag har definierat följande krav på avbildningen:

  • enkelhet i användning (men inte i byggande);
  • minimalt storlek både vad gäller disk och RAM;
  • konfiguration genom miljövariabler, så att avbildningen kan användas i olika miljöer;
  • maximalt effektiv distribution av filer.

I dag kommer jag att berätta hur man:

  • avskalar nginx;
  • bygger brotli från källkod;
  • lär statiska filer att förstå miljövariabler;
  • och såklart hur man skapar en Docker-avbildning av allt detta.

Syftet med den här artikeln är att dela med mig av min erfarenhet och provocera erfarna medlemmar i gemenskapen till konstruktiv kritik.

Bygga avbildningen för konstruktionen

För att den slutliga Docker-avbildningen ska bli liten måste man följa två regler: minimalt antal lager och en minimalistisk basavbildning. En av de minsta basavbildningarna är Alpine Linux, så jag väljer den. Vissa kanske invänder mot att Alpine inte är lämpligt för produktion och kan ha rätt. Men personligen har jag aldrig haft problem med det och det finns inga argument emot det.

För att ha färre lager kommer jag att bygga avbildningen i två steg. Den första – råavbildningen, där alla hjälpprogram och temporära filer kommer att finnas kvar. Och i den rena kommer jag bara att spela in den slutgiltiga versionen av applikationen.

Låt oss börja med hjälpagens.

För att kompilera en SPA-applikation krävs vanligtvis node.js. Jag tar den officiella avbildningen, som också har pakethanterarna npm och yarn. Jag lägger till node-gyp, som behövs för att bygga vissa npm-paket, och Google Brotli-kompressorn, som kommer att vara till nytta senare.

Dockerfile med kommentarer.

# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <amaydokin@gmail.com>"
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

Redan här kämpar jag för minimalism, därför byggs avbildningen med ett stort kommando.

Den färdiga bilden kan hittas här: https://hub.docker.com/r/alexxxnf/spa-builder. Men jag rekommenderar att inte förlita sig på andras bilder utan att bygga din egen.

nginx

För att dela statiska filer kan du använda en valfri webbserver. Jag föredrar att arbeta med nginx, så jag kommer att använda det nu.

Nginx har en officiell Docker-bild, men för enkel distribution av statiska filer finns det alldeles för många moduler. Vilka som ingår i leveransen kan ses med ett särskilt kommando eller i den officiella Dockerfilen.

$ docker run —rm nginx:1-alpine nginx -V

nginx version: nginx/1.17.9
byggd av gcc 8.3.0 (Alpine 8.3.0) 
byggd med OpenSSL 1.1.1d 10 sep 2019
TLS SNI-stöd aktiverat
konfigurationsargument: --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

Jag kommer att använda en Dockerfil som grund, men jag kommer att lämna kvar endast det som behövs för att dela statiska filer. Min version kan inte köras över HTTPS, kommer inte att stödja autentisering och mycket mer. Men min version kan dela ut filer som är komprimerade med Brotli-algoritmen, vilket är något effektivare än gzip. Vi kommer att komprimera filerna en gång, det finns ingen anledning att göra det i realtid.

Så här ser min Dockerfil ut. Kommentarerna på ryska är mina, de på engelska är från originalet.

Dockerfile

# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <amaydokin@gmail.com>"
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;"]

Jag kommer genast att rätta nginx.conf så att gzip och brotli är aktiverade som standard. Jag kommer också att inkludera cache-headers, eftersom vi kommer att dela statiska filer som aldrig ändras. Som sista detalj kommer alla 404-förfrågningar att omdirigeras till index.html, vilket är nödvändigt för navigeringen 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 ladda ner den färdiga bilden här: https://hub.docker.com/r/alexxxnf/nginx-spa. Den tar upp 10,5 MB. Den ursprungliga nginx inkluderade 19,7 MB. Min sportiga nyfikenhet är tillfredsställd.

Vi lär oss statiska filer förstå miljövariabler

Varför kan inställningar behövas i SPA? Till exempel för att ange vilken RESTful API som ska användas. Vanligtvis överförs inställningar för rätt miljö till SPA under byggprocessen. Om man behöver ändra något, måste man bygga om applikationen. Det vill jag inte. Jag vill att applikationen byggs en gång i CI-stadiet och konfigureras så mycket som behövs i CD-stadiet med hjälp av miljövariabler.

Naturligtvis förstår inte statiska filer några miljövariabler. Därför måste vi gå på en trick. I den slutliga bilden kommer jag att köra en speciell shell-skript istället för nginx, som läser miljövariabler, skriver dem till de statiska filerna, komprimerar dem och endast efter det överlämnar kontrollen till nginx.

För detta finns det en parameter ENTRYPOINT i Dockerfile. Vi överlämnar ett sådant skript (i exemplet Angular):

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-файлах
för f i "$TARGET_DIR"/main*.js; gör
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" "$f"
done
}
komprimera () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttf

quot;); gör
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
done
}
om [ "$1" = 'nginx' ]; då
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
om [ ! -e "$FLAG_FILE" ]; då
echo "Kör init skript"
echo "Ersätter miljövariabler"
ersätt_variabler
echo "Komprimerar filer"
komprimera
touch $FLAG_FILE
echo "Klart"
fi
fi
exec "$@"


För att skriptet ska göra sitt jobb, måste inställningarna i js-filer skrivas på följande sätt: ${API_URL}.

Det bör noteras att de flesta moderna SPA vid byggprocessen lägger till hashar till sina filer. Detta är nödvändigt så att webbläsaren kan cache filerna under en längre tid. Om filen ändras kommer även dess hash att ändras, vilket i sin tur får webbläsaren att ladda ned filen igen.

Tyvärr leder min metod för att ändra konfigurationen via miljövariabler inte till någon förändring av filhashen, vilket innebär att webbläsarens cache måste invalidieras på något annat sätt. Jag har inget sådant problem eftersom olika konfigurationer implementeras i olika miljöer.

Samlar den slutliga bilden

Äntligen.

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

Nu kan den skapade bilden samlas och användas var som helst.

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster