Docker-bilde for distribusjon av enkeltsideapplikasjon

Single-page Application (SPA) er et sett med statiske JavaScript- og HTML-filer, samt bilder og andre ressurser. Fordi de ikke endres dynamisk, er det veldig enkelt å publisere dem på nettet. Det finnes et stort antall billige og til og med gratis tjenester for dette, som starter med en enkel GitHub-sider (og for noen til og med med narod.ru) og slutter med en CDN som Amazon S3. Men jeg trengte noe annet.

Jeg trengte et Docker-bilde med SPA slik at det enkelt kunne lanseres både i produksjon som en del av en Kubernetes-klynge, og på maskinen til en back-end-utvikler som ikke aner hva SPA er.

Jeg har bestemt følgende bildekrav for meg selv:

  • brukervennlighet (men ikke montering);
  • minimumsstørrelse både når det gjelder disk og RAM;
  • konfigurasjon gjennom miljøvariabler slik at bildet kan brukes i forskjellige miljøer;
  • den mest effektive distribusjonen av filer.

I dag skal jeg fortelle deg hvordan:

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

Hensikten med denne artikkelen er å dele min erfaring og provosere erfarne samfunnsmedlemmer til konstruktiv kritikk.

Bygge et bilde for montering

For å gjøre det endelige Docker-bildet lite i størrelse, må du følge to regler: et minimum av lag og et minimalistisk basisbilde. Et av de minste grunnbildene er Alpine Linux-bildet, så det er det jeg velger. Noen vil kanskje hevde at Alpine ikke er egnet for produksjon, og de kan ha rett. Men personlig har jeg aldri hatt noen problemer med ham og det er ingen argumenter mot ham.

For å ha færre lag, vil jeg sette sammen bildet i 2 trinn. Den første er et utkast; alle hjelpeverktøy og midlertidige filer vil forbli i den. Og i sluttfasen vil jeg bare skrive ned den endelige versjonen av applikasjonen.

La oss starte med hjelpebildet.

For å kompilere en SPA-applikasjon trenger du vanligvis node.js. Jeg tar det offisielle bildet, som også følger med npm og garnpakkeforvaltere. På egne vegne vil jeg legge til node-gyp, som er nødvendig for å bygge noen npm-pakker, og Brotli-kompressoren fra Google, som vil være nyttig for oss 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 kjemper jeg for minimalisme, så bildet er satt sammen av ett stort lag.

Det ferdige bildet finner du her: https://hub.docker.com/r/alexxxnf/spa-builder. Selv om jeg anbefaler å ikke stole på andres bilder og samle dine egne.

nginx

Du kan bruke hvilken som helst webserver til å distribuere statisk innhold. Jeg er vant til å jobbe med nginx, så jeg skal bruke det nå.

Nginx har et offisielt Docker-bilde, men det har for mange moduler for enkel statisk distribusjon. Hvilke som er inkludert i leveransen kan ses av et spesielt team eller i den offisielle Dockerfilen.

$ 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 bruke Dockerfilen som grunnlag, men jeg vil bare la det som trengs for å distribuere statisk innhold i den. Min versjon vil ikke kunne fungere over HTTPS, vil ikke støtte autorisasjon og mye mer. Men min versjon vil kunne distribuere filer komprimert med Brotli-algoritmen, som er litt mer effektiv enn gzip. Vi kommer til å komprimere filer én gang; det er ikke nødvendig å gjøre dette umiddelbart.

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

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

Jeg vil umiddelbart fikse nginx.conf slik at gzip og brotli er aktivert som standard. Jeg vil også inkludere caching-hoder, fordi vi vil ha aldri skiftende statisk. Og siste hånd vil være å omdirigere alle 404 forespørsler til index.html, dette er nødvendig for navigering 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 laste ned det ferdige bildet her: https://hub.docker.com/r/alexxxnf/nginx-spa. Den tar opp 10,5 MB. Den originale nginx tok opp 19,7 MB. Min sportslige interesse er tilfredsstilt.

Lære statikk for å forstå miljøvariabler

Hvorfor kan det være nødvendig med innstillinger i SPA? For eksempel for å spesifisere hvilket RESTful API som skal brukes. Vanligvis overføres innstillinger for ønsket miljø til SPA på byggestadiet. Hvis du trenger å endre noe, må du bygge applikasjonen på nytt. Jeg vil ikke ha det. Jeg vil at applikasjonen skal bygges én gang på CI-stadiet, og konfigureres så mye som nødvendig på CD-stadiet ved å bruke miljøvariabler.

Selvfølgelig forstår ikke statiske filer selv noen miljøvariabler. Derfor må du bruke et triks. I det endelige bildet vil jeg ikke starte nginx, men et spesielt skallskript som vil lese miljøvariabler, skrive dem til statiske filer, komprimere dem, og først deretter overføre kontrollen til nginx.

For dette formålet gir Dockerfilen ENTRYPOINT-parameteren. La oss gi ham følgende skript (med 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 skriptet skal gjøre jobben sin, må innstillingene skrives i js-filene i dette skjemaet: ${API_URL}.

Det er verdt å merke seg at de fleste moderne SPA-er legger til hashes til filene sine når de bygger. Dette er nødvendig for at nettleseren trygt skal kunne bufre filen i lang tid. Hvis filen endres, vil hashen endres, som igjen vil tvinge nettleseren til å laste ned filen igjen.

Dessverre, i min metode, fører ikke endring av konfigurasjonen gjennom miljøvariabler til en endring i filhashen, noe som betyr at nettleserbufferen må ugyldiggjøres på en annen måte. Jeg har ikke dette problemet fordi forskjellige konfigurasjoner er distribuert i forskjellige miljøer.

Setter sammen det endelige bildet

Endelig.

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

Nå kan det resulterende bildet settes sammen og brukes hvor som helst.

Kilde: www.habr.com