Aplikacioni me një faqe (SPA) është një grup skedash statike JavaScript dhe HTML, si dhe imazhesh e burimesh të tjera. Duke qenë se ato nuk ndryshojnë dinamikisht, publikimi i tyre në internet është shumë i thjeshtë. Përdoren shumë shërbime të lira dhe madje falas, duke filluar nga GitHub Pages (madje edhe nga narod.ru për disa) dhe duke përfunduar me CDN si Amazon S3. Megjithatë, unë kisha nevojë për diçka tjetër.
Më nevojitej një imazh Docker me SPA, që mund të niste lehtë si në prodhim brenda një klasteri Kubernetes, ashtu edhe në makinën e zhvilluesit të back-end që nuk di asgjë për SPA.
Unë përcaktoja këto kërkesa për imazhin:
- lehtësinë e përdorimit (por jo të ndërtimit);
- madhësinë minimale si në aspektin e diskut ashtu edhe në atë të RAM;
- konfigurimin përmes variablave të mjedisit, në mënyrë që imazhi të mund të përdoret në mjedise të ndryshme;
- distribuimin sa më efektiv të skedarëve.
Sot do të tregoj se si:
- të zbrazim nginx;
- të ndërtokojmë brotli nga burimet;
- të bëjmë që skedarët statikë të kuptojnë variablat e mjedisit;
- dhe natyrisht se si të ndërtojmë një imazh Docker nga e gjithë kjo.
Qëllimi i këtij artikulli është të ndaj përvojën time dhe të stimulloj anëtarët e përvojshëm të komunitetit për kritikë konstruktive.
Ndërtimi i imazhit për ndërtim
Për të siguruar që imazhi përfundimtar i Docker të jetë i vogël në madhësi, ne duhet të respektojmë dy rregulla: sa më pak shtresa dhe një imazh bazë minimalistik. Një nga imazhet më të vogla bazë është imazhi Alpine Linux, prandaj do të zgjedh atë. Disa mund të kundërshtojnë se Alpine nuk është i përshtatshëm për prodhim dhe ndoshta do të kenë të drejtë. Por personalisht, unë kurrë nuk kam pasur probleme me të dhe nuk ka asnjë argument kundër tij.
Për të pasur sa më pak shtresa, unë do ta ndërtroj imazhin në 2 etapa. E para – brutale, në të do të mbeten të gjitha utilitetet ndihmës dhe skedarët përkohëshem. Ndërsa në imazhin përfundimtar do të regjistroj vetëm versionin përfundimtar të aplikacionit.
Le të fillojmë me imazhin ndihmës.
Për të kompiluar një aplikacion SPA, zakonisht, nevojitet node.js. Unë do të marr imazhin zyrtar, i cili gjithashtu ka menaxherët e paketave npm dhe yarn. Nga ana ime, do të shtoj node-gyp, i cili është i nevojshëm për ndërtimin e disa paketave npm, dhe kompresorin Brotli nga Google, i cili do të na nevojitet më vonë.
Dockerfile me komente.
# Базовый образ
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/srcKëtu unë po luftoj për minimalizëm, prandaj imazhi ndërtohet nga një ekip i madh.
Imazhi i gatshëm mund të gjendet këtu: . Megjithatë, unë rekomandoj të mos mbështeteni tek imazhet e të tjerëve dhe të krijoni tuajin.
nginx
Për shpërndarjen e statikës, mund të përdorni çdo server web. Unë kam zakon të punoj me nginx, prandaj edhe tani do ta përdor atë.
Nginx ka një imazh zyrtar Docker, megjithatë për shpërndarjen e thjeshtë të statikës ka shumë module të panevojshme. Cilat janë përfshirë në ofertë mund të shikoni me një komandë speciale ose në Dockerfile zyrtar.
$ docker run —rm nginx:1-alpine nginx -V
versioni nginx: nginx/1.17.9
ndërtuar nga gcc 8.3.0 (Alpine 8.3.0)
ndërtuar me OpenSSL 1.1.1d 10 Sht 2019
mbështetje TLS SNI e aktivizuar
argumentet e konfigurimit: --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-neededDo të marr si bazë Dockerfile-in, por do të lë vetëm atë që nevojitet për shpërndarjen e statikës. Versioni im nuk do të mund të punojë me HTTPS, nuk do të mbështesë autorizimin dhe shumëçka tjetër. Megjithatë, versioni im do të mund të shpërndajë skedarë të kompresuar me algoritmin Brotli, i cili është disi më efikas se gzip. Do ta kompresojmë skedarin një herë, nuk ka nevojë ta bëjmë këtë në fluks.
Këtu është Dockerfile që kam përfunduar. Komentet në rusisht – janë të miat, në anglisht – nga origjinali.
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;"]Menjëherë do ta rregulloj nginx.conf, që gzip dhe brotli të jenë të aktivizuara si parazgjedhje. Po ashtu do të aktivizoj header-at e caching, pasi do të kemi statikë që nuk do të ndryshojë kurrë. Dhe si hapin e fundit do të redirektoj të gjitha kërkesat 404 në index.html, kjo është e nevojshme për navigimin 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";
}
}
}
}Shkarko pamjen e gatshme mund ta gjejë këtu: . Ai zë 10.5 MB. Nginx original zinte 19.7 MB. Interesi im sportiv është plotësuar.
Mësojmë statikën të kuptojë variablat e ambientit
Për çfarë kanë nevojë këto konfigurime në SPA? Për shembull, për të treguar se cili RESTful API duhet përdorur. zakonisht, konfigurimet për ambientin përkatës dërgohen në SPA gjatë fazës së ndërtimit. Nëse duhet të ndryshoj diçka, do të duhet të rindërtoj aplikacionin. Unë nuk e dua këtë. Unë dua që aplikacioni të ndërtohet vetëm një herë në fazën e CI, dhe të konfigurohet sa herë që është e nevojshme, në fazën CD nëpërmjet variablave të mjedisit.
Natyrisht, skedarët statikë vetë nuk kuptojnë asnjë variabël mjedisor. Prandaj, do të duhet të përdor një hile. Në imazhin përfundimtar do të krijoj jo nginx, por një skript të veçantë shell, i cili do të lexojë variablave të mjedisit, do t'i shkruajë ato në skedarët statikë, do t'i comprimojë dhe pas kësaj do t'i kalojë kontrollin nginx.
Për këtë, në Dockerfile është parashikuar parametri ENTRYPOINT. Do t'i kalojmë një skript të tillë (me shembull 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"
kompresim
touch $FLAG_FILE
echo "Done"
fi
fi
exec "$@"
Për të bërë që skripti të kryejë punën e tij, konfigurimet në skedarët js duhet të shkruhen në këtë format:${API_URL}.Është e rëndësishme të theksohet se shumica e SPA-ve moderne, kur grumbullohen, shtojnë hash-e në skedarët e tyre. Kjo është e nevojshme për që shfletuesi të mund të ruajë skedarin për një kohë të gjatë. Nëse skedari ndryshon, bëhet edhe hash-i i tij, që në të njëjtën kohë do të detyrojë shfletuesin të shkarkojë përsëri skedarin.
Fatkeqësisht, në metodën time, ndryshimi i konfigurimit përmes variablave të mjedisit nuk çon në ndryshimin e hash-it të skedarit, dhe pra, për të invalide keshin e shfletuesit duhet të përdoret një mënyrë tjetër. Unë nuk kam këtë problem, sepse konfigurime të ndryshme shpërndahen në mjedise të ndryshme.
Grumbullojmë imazhin përfundimtar
Më në fund.
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;"]Tani imazhi i krijuar mund të grumbullohet dhe të përdoret kudo.
Burimi: habr.com
