Dockerjeva slika za distribucijo enostranske aplikacije

Enostranska aplikacija (SPA) je niz statičnih datotek JavaScript in HTML ter slik in drugih virov. Ker se ne spreminjajo dinamično, je njihova objava na spletu zelo enostavna. Za to obstaja veliko poceni in celo brezplačnih storitev, začenši s preprostimi stranmi GitHub (in za nekatere celo z narod.ru) in konča s CDN, kot je Amazon S3. Vendar sem potreboval nekaj drugega.

Potreboval sem Dockerjevo sliko s SPA, da bi jo bilo mogoče enostavno zagnati v produkciji kot del gruče Kubernetes in na stroju zalednega razvijalca, ki nima pojma, kaj je SPA.

Zase sem določil naslednje slikovne zahteve:

  • enostavnost uporabe (vendar ne montaže);
  • najmanjša velikost diska in RAM-a;
  • konfiguracijo prek spremenljivk okolja, tako da je sliko mogoče uporabiti v različnih okoljih;
  • najbolj učinkovito distribucijo datotek.

Danes vam bom povedal, kako:

  • gut nginx;
  • graditi brotli iz virov;
  • naučiti statične datoteke razumeti spremenljivke okolja;
  • in seveda kako iz vsega tega sestaviti sliko Dockerja.

Namen tega članka je deliti svoje izkušnje in spodbuditi izkušene člane skupnosti h konstruktivni kritiki.

Gradnja slike za montažo

Da bo končna Dockerjeva slika majhna, se morate držati dveh pravil: najmanj plasti in minimalistična osnovna slika. Ena najmanjših osnovnih slik je slika Alpine Linux, zato jo bom izbral. Nekateri bi morda trdili, da Alpine ni primeren za proizvodnjo, in morda imajo prav. Osebno pa nikoli nisem imel težav z njim in ni nobenih argumentov proti njemu.

Da bo manj plasti, bom sliko sestavil v 2 stopnjah. Prvi je osnutek, v njem bodo ostali vsi pomožni pripomočki in začasne datoteke. In v končni fazi bom le še zapisal končno različico aplikacije.

Začnimo s pomožno sliko.

Za prevajanje aplikacije SPA običajno potrebujete node.js. Vzel bom uradno sliko, ki je priložena tudi upraviteljem paketov npm in yarn. V svojem imenu bom dodal node-gyp, ki je potreben za gradnjo nekaterih paketov npm, in kompresor Brotli iz Googla, ki nam bo kasneje koristil.

Docker datoteka s komentarji.

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

Že tukaj se borim za minimalizem, tako da podobo sestavlja ena velika ekipa.

Končano sliko najdete tukaj: https://hub.docker.com/r/alexxxnf/spa-builder. Čeprav priporočam, da se ne zanašate na slike drugih ljudi in zbirate svoje.

nginx

Za distribucijo statične vsebine lahko uporabite kateri koli spletni strežnik. Navajen sem delati z nginxom, zato ga bom zdaj uporabljal.

Nginx ima uradno sliko Dockerja, vendar ima preveč modulov za preprosto statično distribucijo. Kateri so vključeni v dostavo, si lahko ogleda posebna ekipa ali v uradni datoteki 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

Za osnovo bom uporabil Dockerfile, vendar bom v njem pustil le tisto, kar je potrebno za distribucijo statične vsebine. Moja različica ne bo mogla delovati prek HTTPS, ne bo podpirala avtorizacije in še veliko več. Toda moja različica bo lahko distribuirala datoteke, stisnjene z algoritmom Brotli, ki je nekoliko bolj učinkovit kot gzip. Datoteke bomo stisnili enkrat; tega ni treba početi sproti.

To je Dockerfile, ki sem ga dobil. Komentarji v ruščini so moji, v angleščini - iz izvirnika.

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

Takoj bom popravil nginx.conf, tako da bosta gzip in brotli privzeto omogočena. Vključil bom tudi glave za predpomnjenje, ker se ne bomo nikoli spreminjali statike. In zadnji dotik bo preusmeritev vseh 404 zahtev na index.html, to je potrebno za navigacijo v 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";
            }
        }
    }
}

Končano sliko lahko prenesete tukaj: https://hub.docker.com/r/alexxxnf/nginx-spa. Zavzame 10,5 MB. Prvotni nginx je zasedel 19,7 MB. Moje športno zanimanje je potešeno.

Poučevanje statike za razumevanje spremenljivk okolja

Zakaj so morda potrebne nastavitve v SPA? Na primer, da bi določili, kateri API RESTful uporabiti. Običajno se nastavitve za želeno okolje prenesejo v SPA v fazi gradnje. Če boste morali kaj spremeniti, boste morali znova sestaviti aplikacijo. Nočem tega. Želim, da je aplikacija zgrajena enkrat na stopnji CI in konfigurirana, kolikor je potrebno, na stopnji CD z uporabo spremenljivk okolja.

Seveda same statične datoteke ne razumejo nobenih spremenljivk okolja. Zato boste morali uporabiti trik. Na končni sliki ne bom zagnal nginxa, ampak posebno lupinsko skripto, ki bo prebrala spremenljivke okolja, jih zapisala v statične datoteke, jih stisnila in šele nato prenesla nadzor na nginx.

V ta namen datoteka Dockerfile zagotavlja parameter ENTRYPOINT. Dajmo mu naslednji skript (z uporabo Angularja kot primera):

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

Da bi skript lahko opravil svoje delo, morajo biti nastavitve zapisane v datotekah js v tej obliki: ${API_URL}.

Omeniti velja, da večina sodobnih SPA dodaja zgoščene vrednosti svojim datotekam pri gradnji. To je potrebno, da lahko brskalnik dolgo časa varno predpomni datoteko. Če se datoteka spremeni, se bo spremenila tudi njena zgoščena vrednost, kar bo brskalnik prisililo, da znova prenese datoteko.

Na žalost pri moji metodi spreminjanje konfiguracije prek spremenljivk okolja ne vodi do spremembe zgoščene vrednosti datoteke, kar pomeni, da je treba predpomnilnik brskalnika razveljaviti na drug način. Te težave nimam, ker so v različnih okoljih nameščene različne konfiguracije.

Sestavljanje končne slike

Končno.

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

Zdaj lahko nastalo sliko sestavite in uporabite kjer koli.

Vir: www.habr.com