Enota aplikacija (SPA) je zbirka statičnih JavaScript in HTML datotek, pa tudi slik in drugih virov. Ker se ti ne spreminjajo dinamično, jih je zelo enostavno objaviti na spletu. Za to obstaja veliko poceni in celo brezplačnih storitev, od preprostega GitHub Pages (za nekatere celo z narod.ru) do CDN-jev, kot je Amazon S3. Vendar sem potreboval nekaj drugega.
Potreboval sem Docker slikovnik s SPA, da ga lahko enostavno zaženem tako v produkciji kot del Kubernetes gručne, kot tudi na računalniku back-end razvijalca, ki nima pojma, kaj je SPA.
Določil sem naslednje zahteve za slikovnik:
- enostavna uporaba (ne pa tudi sestavljanje);
- minimalna velikost tako z vidika diska kot RAM-a;
- nastavitev preko okolijskih spremenljivk, da se lahko slikovnik uporablja v različnih okoljih;
- maksimalno učinkovita distribucija datotek.
Danes vam bom povedal, kako:
- razstavljanje nginx;
- združevanje brotli iz izvorne kode;
- naučiti statične datoteke, da razumejo okoljske spremenljivke;
- in seveda, kako narediti Docker slikovnik iz vsega tega.
Namen tega članka je deliti moje izkušnje in spodbuditi izkušene člane skupnosti k konstruktivni kritiki.
Sestavljanje slikovnika za sestavo
Da bi bil končni Docker slikovnik majhen, je treba upoštevati dve pravili: minimum plasti in minimalistična osnovna slika. Eden izmed najmanjših osnovnih slik je slika Alpine Linux, zato jo bom izbral. Nekateri bi lahko ugovarjali, da Alpine ni primeren za produkcijo in morda imajo prav. A zame nikoli nisem imel težav z njo in ni nobenih argumentov proti njej.
Da bi bilo čim manj plasti, bom sliko sestavil v dveh korakih. Prvi je grob, v njem bodo ostali vsi pomožni pripomočki in začasne datoteke. V čisti različici bom zapisal le končno različico aplikacije.
Začnimo s pomožno sliko.
Za kompilacijo SPA aplikacije običajno potrebujemo node.js. Vzel bom uradno sliko, ki vsebuje tudi upravljalnike paketov npm in yarn. Dodal bom node-gyp, ki je potreben za sestavljanje nekaterih npm paketov, in Brotli kompressor od Googla, ki nam bo kasneje prišel prav.
Dockerfile z opombami.
# Базовый образ
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Že tukaj se borim za minimalizem, zato se slika sestavi z eno veliko ukazno vrstico.
Tukaj lahko najdete končno sliko: . Čeprav priporočam, da se ne zanašate na tuje slike in da ustvarite svojo.
nginx
Za strežbo statičnih datotek lahko uporabite kateri koli spletni strežnik. Jaz sem navajen delati z nginx, zato ga bom uporabil tudi zdaj.
Nginx ima uradno Docker sliko, vendar je v njej za preprosto strežbo statičnih datotek preveč modulov. Kakšni so moduli, ki so vključeni v dobavo, lahko preverite s posebno ukazno vrstico ali v uradnem Dockerfile.
$ docker run —rm nginx:1-alpine nginx -V
nginx verzija: nginx/1.17.9
onan zgrajen z gcc 8.3.0 (Alpine 8.3.0)
izgrajen z OpenSSL 1.1.1d 10 Sep 2019
podpora za TLS SNI omogočena
argumenti za konfiguracijo: --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-neededOsnoval bom na Dockerfile, vendar bom pustil le tisto, kar je potrebno za strežbo statičnih datotek. Moja različica ne bo delovala prek HTTPS, ne bo podpirala avtentikacije in še marsičesa drugega. Ampak moja različica bo lahko strežila datoteke, stisnjene z algoritemom Brotli, ki je nekoliko učinkovitejši kot gzip. Datoteke bomo stiskali enkrat, da bi to delali med prenosom, ni potrebe.
Tukaj je moja končna različica Dockerfile. Komentarji v ruščini – moji, v angleščini – iz izvirnika.
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;"]Takoj bom popravil nginx.conf, da bodo gzip in brotli privzeto vključeni. Prav tako bom vključil predpomnilniške glave, saj bomo strežili statične datoteke, ki se nikoli ne spreminjajo. In zadnji dotik bo preusmeritev vseh 404 poizvedb 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";
}
}
}
}Tukaj lahko prenesete pripravljen obraz: . Zaseda 10,5 MB. Originalni nginx je zasedal 19,7 MB. Moj športni interes je zadovoljen.
Učimo statiko, da razume okoljske spremenljivke
Zakaj so nastavitve v SPA potrebne? Na primer, da določimo, kateri RESTful API uporabiti. Običajno se nastavitve za zahtevano okolje prenesejo v SPA med fazo gradnje. Če je potrebno kaj spremeniti, bo treba ponovno zgraditi aplikacijo. To ne želim. Želim, da se aplikacija gradi enkrat v fazi CI, in da se konfigurira toliko, kolikor je potrebno, v fazi CD s pomočjo okoljskih spremenljivk.
Seveda statične datoteke same po sebi ne razumejo nobenih okoljskih spremenljivk. Zato bo treba priti do trika. V končnem obrazu ne bom zagnal nginx, temveč poseben shell skript, ki bo prebral okoljske spremenljivke, jih zapisal v statične datoteke, jih stisnil in šele nato prenesel nadzor nad nginx.
Za to je v Dockerfile predviden parameter ENTRYPOINT. Posredujmo mu tak skript (na primeru 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"
stisni
touch $FLAG_FILE
echo "Done"
fi
fi
exec "$@"
Da bo skript opravil svoje delo, morajo biti nastavitve v js-datotekah zapisane na ta način:${API_URL}.Pomembno je omeniti, da večina sodobnih SPA med gradnjo doda heše k svojim datotekam. To je potrebno, da lahko brskalnik brez skrbi predpomni datoteko za daljše obdobje. Če se datoteka vseeno spremeni, se bo spremenil tudi njen heš, kar pa bo brskalniku naložilo ponovno nalaganje datoteke.
Žal mi pri moji metodi sprememba nastavitve prek okolijskih spremenljivk ne privede do spremembe hash datoteke, zato je potrebno na drugačen način invalidirati predpomnilnik brskalnika. Te težave nimam, ker se različne nastavitve izvajajo v različnih okoljih.
Zberemo končno sliko
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;"]Sedaj lahko ustvarjeno sliko zberemo in uporabimo kjerkoli.
Izvor: habr.com
