Aplicația Single-page (SPA) este un set de fișiere statice JavaScript și HTML, precum și imagini și alte resurse. Deoarece acestea nu se schimbă dinamic, publicarea lor pe internet este foarte simplă. Există o mulțime de servicii ieftine sau chiar gratuite, începând de la simplul GitHub Pages (sau chiar narod.ru pentru unii) până la CDN-uri precum Amazon S3. Totuși, aveam nevoie de altceva.
Îmi trebuia o imagine Docker cu SPA, astfel încât să poată fi ușor rulată atât în producție ca parte a unui cluster Kubernetes, cât și pe mașina programatorului back-end care nu are idee ce este un SPA.
Am stabilit următoarele cerințe pentru imagine:
- simplicity in use (but not in building);
- minimal size in terms of both disk space and RAM;
- configuration via environment variables, so that the image can be used in different environments;
- maximally efficient file distribution.
Astăzi voi povesti cum:
- să extrag nginx;
- să compilez brotli din surse;
- să învăț fișierele statice să înțeleagă variabilele de mediu;
- și bineînțeles cum să construiesc din toate acestea o imagine Docker.
Scopul acestui articol este să îmi împărtășesc experiența și să provoc participanții experimentați ai comunității la ocriticî constructivă.
Construirea imaginii pentru compilare
Pentru ca imaginea Docker finală să fie mică, trebuie să respect două reguli: minim de straturi și o imagine de bază minimalistă. Una dintre cele mai mici imagini de bază este imaginea Alpine Linux, așa că aceasta o voi alege. Unii ar putea argumenta că Alpine nu este potrivit pentru producție și poate că au dreptate. Dar personal, nu am avut niciodată probleme cu el, și nu am argumente împotriva lui.
Pentru a avea mai puține straturi, voi construi imaginea în 2 etape. Prima – o etapă de lucru, în care vor rămâne toate utilitarele auxiliare și fișierele temporare. Iar în etapa finală voi salva doar versiunea finală a aplicației.
Să începem cu imaginea auxiliară.
Pentru a compila o aplicație SPA, de obicei, este necesar node.js. Voi lua imaginea oficială care vine de asemenea cu managerii de pachete npm și yarn. De la mine voi adăuga node-gyp, care este necesar pentru compilarea unor pachete npm, și compresorul Brotli de la Google, care ne va fi util mai târziu.
Dockerfile cu comentarii.
# Базовый образ
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/srcAici deja mă lupt pentru minimalism, așa că imaginea se construiește printr-o singură comandă mare.
Imaginea finală o poți găsi aici: . Deși recomand să nu te bazezi pe imaginile altora și să îți creezi propria.
nginx
Pentru a servi statice, poți folosi orice server web. Eu sunt obișnuit să lucrez cu nginx, așadar voi folosi el acum.
Nginx are o imagine oficială Docker, însă pentru a servi simplu statice, are prea multe module incluse. Ce anume este inclus poate fi văzut cu o comandă specială sau în Dockerfile-ul oficial.
$ docker run —rm nginx:1-alpine nginx -V
versiunea nginx: nginx/1.17.9
construit de gcc 8.3.0 (Alpine 8.3.0)
construit cu OpenSSL 1.1.1d 10 Sep 2019
suport SNI TLS activat
argumente de configurare: --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-neededVoi folosi un Dockerfile de bază, dar voi lăsa doar ceea ce este necesar pentru a servi statice. Varianta mea nu va funcționa peste HTTPS, nu va suporta autentificarea și multe altele. Totuși, versiunea mea va putea servi fișiere comprimate cu algoritmul Brotli, care este puțin mai eficient decât gzip. Vom comprima fișierele o singură dată, nu este nevoie să o facem din mers.
Iată ce Dockerfile am obținut. Comentariile în română sunt ale mele, cele în engleză sunt din original.
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;"]Voi modifica imediat nginx.conf, astfel încât gzip și brotli să fie activate implicit. De asemenea, voi activa antetele de caching, deoarece vom servi statice care nu se schimbă niciodată. Iar ultimul detaliu va fi redirecționarea tuturor solicitărilor 404 către index.html, ceea ce este necesar pentru navigația în 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";
}
}
}
}Poți descărca imaginea gata aici: . Aceasta ocupă 10,5 MB. Originalul nginx ocupa 19,7 MB. Sunt mulțumit de curiozitatea mea.
Învățăm statica să înțeleagă variabilele de mediu
De ce ar avea nevoie un SPA de configurări? De exemplu, pentru a specifica ce API RESTful să folosească. De obicei, configurările pentru mediul dorit sunt transmise în SPA în momentul compilării. Dacă trebuie să schimb ceva, atunci va trebui să recompilăm aplicația. Eu nu vreau asta. Vreau ca aplicația să fie compilată o dată în etapa CI, iar configurată atât cât este nevoie, în etapa CD, folosind variabilele de mediu.
Desigur, fișierele statice nu înțeleg de la sine variabilele de mediu. Așadar, va trebui să recurgem la un truc. În imaginea finală voi rula nu nginx, ci un script shell special care va citi variabilele de mediu, le va scrie în fișierele statice, le va comprima și abia după aceea va preda controlul lui nginx.
Pentru aceasta, în Dockerfile este prevăzut parametrul ENTRYPOINT. Îi vom transmite un astfel de script (pe exemplul 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"
gata
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttfquot;); do
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
gata
}
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 "$@"
Pentru ca scriptul să își facă treaba, în fișierele js, configurările trebuie scrise în acest mod:${API_URL}.Merită menționat că majoritatea SPA-urilor moderne adaugă hash-uri fișierelor lor în timpul compilării. Acest lucru este necesar pentru ca browserul să poată salva fișierul în cache pentru o perioadă lungă. Dacă fișierul se schimbă, atunci hash-ul său se va schimba, ceea ce va determina browserul să descarce din nou fișierul.
Din păcate, în metoda mea, modificarea configurației prin variabile de mediu nu conduce la o schimbare a hash-ului fișierului, astfel că invalidarea cache-ului browserului trebuie realizată prin alt mod. Eu nu am această problemă, deoarece diferite configurații sunt desfășurate în medii diferite.
Colectăm imaginea finală
În sfârșit.
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;"]Acum, imaginea obținută poate fi construită și utilizată oriunde.
Sursa: habr.com
