Docker attēls vienas lapas lietojumprogrammas izplatīšanai

Vienas lapas lietojumprogramma (SPA) ir statisku JavaScript un HTML failu, kā arī attēlu un citu resursu kopa. Tā kā tie dinamiski nemainās, to publicēšana tiešsaistē ir ļoti vienkārša. Šim nolūkam ir pieejams liels skaits lētu un pat bezmaksas pakalpojumu, sākot ar vienkāršu GitHub Pages (un dažiem pat ar narod.ru) un beidzot ar CDN, piemēram, Amazon S3. Tomēr man vajadzēja kaut ko citu.

Man bija nepieciešams Docker attēls ar SPA, lai to varētu viegli palaist gan ražošanā kā daļa no Kubernetes klastera, gan aizmugurējā izstrādātāja mašīnā, kuram nav ne jausmas, kas ir SPA.

Esmu sev noteicis šādas attēla prasības:

  • lietošanas vienkāršība (bet ne montāža);
  • minimālais izmērs gan diska, gan RAM ziņā;
  • konfigurācija, izmantojot vides mainīgos, lai attēlu varētu izmantot dažādās vidēs;
  • visefektīvākā failu izplatīšana.

Šodien es jums pastāstīšu, kā:

  • zarnu nginx;
  • veidot brotli no avotiem;
  • iemācīt statiskos failus saprast vides mainīgos;
  • un, protams, kā no tā visa salikt Docker attēlu.

Šī raksta mērķis ir dalīties savā pieredzē un izprovocēt pieredzējušus kopienas locekļus uz konstruktīvu kritiku.

Attēla veidošana montāžai

Lai galīgais Docker attēls būtu mazs, jums jāievēro divi noteikumi: minimālais slāņu skaits un minimālistisks pamata attēls. Viens no mazākajiem bāzes attēliem ir Alpine Linux attēls, tāpēc es izvēlēšos to. Daži varētu iebilst, ka Alpine nav piemērota ražošanai, un viņiem varētu būt taisnība. Bet personīgi man ar viņu nekad nav bijušas problēmas un pret viņu nav nekādu argumentu.

Lai būtu mazāk slāņu, attēlu salikšu 2 posmos. Pirmais ir melnraksts; tajā paliks visas papildu utilītas un pagaidu faili. Un beigu posmā es pierakstīšu tikai pieteikuma galīgo versiju.

Sāksim ar palīgattēlu.

Lai sastādītu SPA aplikāciju, parasti nepieciešams node.js. Es ņemšu oficiālo attēlu, kas arī nāk ar npm un dzijas pakotņu pārvaldniekiem. Savā vārdā pievienošu node-gyp, kas nepieciešams dažu npm pakotņu uzbūvēšanai un Brotli kompresoru no Google, kas mums noderēs vēlāk.

Dockerfile ar komentāriem.

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

Jau šeit es cīnos par minimālismu, tāpēc tēlu saliek viena liela komanda.

Gatavo attēlu var atrast šeit: https://hub.docker.com/r/alexxxnf/spa-builder. Lai gan iesaku nepaļauties uz svešiem attēliem un kolekcionēt savējos.

nginx

Statiskā satura izplatīšanai varat izmantot jebkuru tīmekļa serveri. Esmu pieradis strādāt ar nginx, tāpēc tagad to izmantošu.

Nginx ir oficiāls Docker attēls, taču tam ir pārāk daudz moduļu vienkāršai statiskai izplatīšanai. Kuri no tiem ir iekļauti piegādē, to var redzēt īpaša komanda vai oficiālajā Dockerfile.

$ docker palaist --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

Par pamatu izmantošu Dockerfile, bet atstāšu tajā tikai to, kas nepieciešams statiskā satura izplatīšanai. Mana versija nevarēs darboties, izmantojot HTTPS, neatbalstīs autorizāciju un daudz ko citu. Bet mana versija varēs izplatīt failus, kas saspiesti ar Brotli algoritmu, kas ir nedaudz efektīvāks par gzip. Mēs saspiedīsim failus vienreiz; tas nav jādara uzreiz.

Šis ir Dockerfile, ar kuru es beidzu. Komentāri krievu valodā ir mani, angļu valodā - no oriģināla.

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

Es nekavējoties izlabošu nginx.conf, lai pēc noklusējuma būtu iespējoti gzip un brotli. Es iekļaušu arī kešatmiņas galvenes, jo mums nekad nebūs jāmaina statika. Un pēdējais pieskāriens būs visu 404 pieprasījumu novirzīšana uz index.html, tas ir nepieciešams navigācijai 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";
            }
        }
    }
}

Gatavo attēlu varat lejupielādēt šeit: https://hub.docker.com/r/alexxxnf/nginx-spa. Tas aizņem 10,5 MB. Sākotnējais nginx aizņēma 19,7 MB. Mana sportiskā interese ir apmierināta.

Statikas mācīšana, lai izprastu vides mainīgos

Kāpēc SPA var būt nepieciešami iestatījumi? Piemēram, lai norādītu, kuru RESTful API izmantot. Parasti vēlamās vides iestatījumi tiek pārsūtīti uz SPA izveides stadijā. Ja jums kaut kas jāmaina, jums būs jāpārveido lietojumprogramma. Es to nevēlos. Es vēlos, lai lietojumprogramma tiktu izveidota vienreiz CI posmā un konfigurēta tik daudz, cik nepieciešams CD posmā, izmantojot vides mainīgos.

Protams, statiskie faili paši nesaprot nekādus vides mainīgos. Tāpēc jums būs jāizmanto triks. Galīgajā attēlā es palaižu nevis nginx, bet speciālu čaulas skriptu, kas nolasīs vides mainīgos, ierakstīs tos statiskajos failos, saspiedīs un tikai pēc tam nodos vadību uz nginx.

Šim nolūkam Dockerfile nodrošina parametru ENTRYPOINT. Dosim viņam šādu skriptu (kā piemēru izmantojot 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-файлах
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 "$@"

Lai skripts veiktu savu darbu, iestatījumi ir jāieraksta js failos šādā formā: ${API_URL}.

Ir vērts atzīmēt, ka lielākā daļa mūsdienu SPA pievieno failiem jaucējvērtības, veidojot. Tas ir nepieciešams, lai pārlūkprogramma varētu droši saglabāt failu kešatmiņā ilgu laiku. Ja fails mainās, mainīsies tā jaucējfunkcija, kas savukārt liks pārlūkprogrammai vēlreiz lejupielādēt failu.

Diemžēl manā metodē konfigurācijas mainīšana, izmantojot vides mainīgos, neizraisa izmaiņas faila hash, kas nozīmē, ka pārlūkprogrammas kešatmiņa ir jāanulē citā veidā. Man šīs problēmas nav, jo dažādās vidēs tiek izvietotas dažādas konfigurācijas.

Galīgā attēla salikšana

Beidzot.

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

Tagad iegūto attēlu var salikt un izmantot jebkur.

Avots: www.habr.com