Ühe lehe rakendus (SPA) on komplekt staatilisi JavaScripti ja HTML-faile, samuti pilte ja muid ressursse. Kuna need ei muutu dünaamiliselt, siis nende avaldamine internetis on väga lihtne. Selleks on palju odavaid ja isegi tasuta teenuseid, alustades lihtsast GitHub Pages'ist (mõnedele isegi narod.ru) ja lõpetades CDN-idega nagu Amazon S3. Kuid mul oli vaja midagi muud.
Mul oli vaja Docker-pilti SPAl, et seda saaks lihtsalt käivitada nii tootmises Kubernetes-klastris kui ka back-end arendaja masinal, kes ei tea, mis on SPA.
Olen määratlenud järgmised nõuded pildi jaoks:
- kasutamise lihtsus (kuid mitte koostamise);
- minimaalne suurus nii ketta kui ka RAMi mõttes;
- seadistamine keskkonnamuutujate kaudu, et pilti saaks kasutada erinevates keskkondades;
- failide maksimaalne efektiivne jagamine.
Täna räägin, kuidas:
- nginx'i tühjaks teha;
- brotli allikaid kokku panna;
- õpetada staatilisi faile mõistma keskkonnamuutujaid;
- ja loomulikult, kuidas kogu see asi Docker-pildiks kokku panna.
Selle artikli eesmärk on jagada oma kogemust ja provotseerida kogenud kogukonna liikmeid konstruktiivseks kriitikaks.
Pildi koostamine
Kuna lõplik Docker-pilt peab olema väike, tuleb järgida kahte reeglit: minimaalne kihtide arv ja minimalistlik alusplaat. Üks väiksemaid alusplaate on Alpine Linuxi pilt, seega valin just selle. Mõni võib väita, et Alpine ei sobi tootmisse ja võib-olla on tal õigus. Kuid isiklikult ei ole mul kunagi olnud probleeme ja mul ei ole mingeid argumente tema vastu.
Kihte vähendamiseks koostan pildi kahe etapi kaudu. Esimene – must versioon, milles jäävad kõik abivahendid ja ajutised failid. Puhas versioon sisaldab ainult rakenduse lõppversiooni.
Alustame abipildist.
SPAi rakenduse kompileerimiseks on tavaliselt vajalik node.js. Võtan ametliku pildi, millega on kaasas ka paketihaldurid npm ja yarn. Oma poolt lisan node-gyp'i, mida on vaja mõnede npm-pakettide koostamiseks, ning Google'i Brotli kompressori, mida vajame hiljem.
Dockerfile koos kommentaaridega.
# Базовый образ
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/srcJuba siin ma võitlen minimalistlike ideede eest, seega koondub kujundus ühte suuresse meeskonda.
Valmis kujunduse leiate siit: . Kui aga soovitan, siis ärge toetuge teiste kujundustele, vaid koostage oma.
nginx
Statika jagamiseks võite kasutada ükskõik millist veebi serverit. Olen harjunud töötama nginxiga, seega kasutan seda ka praegu.
Nginxil on ametlik Docker-kujundus, kuid statika lihtsaks jagamiseks on seal liiga palju mooduleid. Millised just on kaasatud, saab vaadata spetsiaalse käsu või ametliku Dockerfile'i kaudu.
$ docker run —rm nginx:1-alpine nginx -V
nginx versioon: nginx/1.17.9
koostatud gcc 8.3.0 (Alpine 8.3.0)
koostatud OpenSSL 1.1.1d 10. sept 2019
TLS SNI toe tugi lubatud
konfigureerimise argumendid: --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-neededVõtan aluseks Dockerfile'i, kuid jään sinna alles ainult need, mis on vajalikud statika jagamiseks. Minu versioon ei suuda töötada HTTPS-i kaudu, ei toeta autentimist ja palju muud. Kuid minu versioon suudab jagada faile, mis on tihendatud Brotli algoritmiga, mis on natuke efektiivsem kui gzip. Tihendame faile ainult üks kord, reaalajas pole vaja seda teha.
Selline Dockerfile mul välja tuli. Kommentaarid venekeelsed – minu omad, ingliskeelsed – originaalist.
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;"]Parandan kohe nginx.conf-i, et gzip ja brotli oleksid vaikimisi sisse lülitatud. Samuti lülitan sisse vahepealsete päiste, kuna meil jagatakse kunagi mitte muutuvaid statika faile. Viimase puudutatuna suunan kõik 404 päringud index.html-ile, see on vajalik SPA-s navigeerimiseks.
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";
}
}
}
}Valmis pilt on võimalik alla laadida siit: . See on 10,5 MB. Originaalne nginx oli 19,7 MB. Minu sportlik huvi on rahuldatud.
Õpime staatilisi faile muutujate keskkonda mõistma.
Miks on SPA-de jaoks vajalikud seaded? Näiteks selleks, et näidata, millist RESTful API-d kasutada. Tavaliselt edastatakse asjakohased seaded SPA-le kokkuvõtte etapis. Kui tuleb midagi muuta, on vajalik rakenduse uuesti kokku panna. Ma ei taha seda. Ma tahan, et rakendus koostataks üks kord CI etapis ja konfigureeritaks vajadusel CD etapis keskkonnamuutujate abil.
Jah, staatilised failid enda iseenesest ei mõista mingeid keskkonnamuutujaid. Seetõttu tuleb minna väikesele muudatusele. Lõpp-pildis ei käivita ma nginx-i, vaid spetsiaalset shell-skripti, mis loeb keskkonnamuutujad, salvestab need staatilistesse failidesse, tihendab need ja alles siis edastab juhtimise nginx-ile.
Selleks on Dockerfile-s ette nähtud parameeter ENTRYPOINT. Edastame sellele sellise skripti (näiteks 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"
done
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttfquot;); 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 "$@"
Kuna skript peab oma töö ära tegema, tuleb js-failides seadeid kirjutada järgmisel viisil:${API_URL}.Oluline on märkida, et enamik kaasaegseid SPA-sid lisab oma failidele koondsummasid. See on vajalik, et brauser saaks faili pika aja jooksul ohutult vahemällu salvestada. Kui fail peaks muutuma, muutub ka selle koondsumma, mis omakorda sunnib brauserit faili uuesti alla laadima.
Kahjuks ei muuda minu meetodil konfiguratsiooni muutmine keskkonna muutujate kaudu faili räsiväärtust, seega tuleb brauseri vahemälu invalidida kuidagi teisiti. Mul ei ole seda probleemi, kuna erinevad konfiguratsioonid paigaldatakse erinevatesse keskkondadesse.
Kogume lõplikku pilti
Lõfinally.
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üüd saab saadud pilti koguda ja kasutada igal pool.
Allikas: habr.com
