Single-page Application (SPA) është një grup skedarësh statikë JavaScript dhe HTML, si dhe imazhesh dhe burimesh të tjera. Duke qenë se ata nuk ndryshojnë dinamikisht, publikimi i tyre në internet është shumë i thjeshtë. Për këtë ekziston një sërë shërbimesh të lira dhe madje falas, duke filluar nga GitHub Pages (dhe për disa madje edhe nga narod.ru) e deri te CDN si Amazon S3. Megjithatë, unë kisha nevojë për diçka tjetër.
Më nevojitej një Docker-ima me SPA, që të mund të aktivizohej lehtësisht si në prodhim brenda një klasteri Kubernetes, ashtu edhe në makinën e zhvilluesit back-end, i cili nuk ka asnjë idenë se çfarë është SPA.
Unë për vete kam përcaktuar kërkesat e mëposhtme për imagen:
- thjeshtësi në përdorim (por jo në ndërtim);
- përmasë minimale në aspektin e diskut, ashtu si dhe në atë të RAM-it;
- konfigurim përmes variablave të mjedisit, në mënyrë që imazhi të mund të përdoret në mjedise të ndryshme;
- shpërndarje maksilarisht efektive të skedarëve.
Sot do të flasë se si:
- të zbrazim nginx;
- të ndërtojmë brotli nga burimi;
- të mësojmë skedarët statikë të kuptojnë variablat e mjedisit;
- dhe sigurisht se si të ndërtojmë një Docker-ima nga gjithçka.
Qëllimi i këtij artikulli është të ndaj përvojën time dhe të nxis pjesëmarrësit e eksperiencës në komunitet për kritika konstruktive.
Ndërtimi i imazhit për ndërtim
Që Docker-ima përfundimtar të rezultojë i vogël në përmasa, duhet të ndjek dy rregulla: minimum katrorësh dhe një imazh bazë minimalist. Një nga imazhet më të vogla të bazës është imazhi Alpine Linux, prandaj do ta zgjedh atë. Disa mund të kundërshtojnë që Alpine nuk i përshtatet prodhimit dhe ndoshta kanë të drejtë. Por për vete nuk kam pasur asnjë herë probleme me të dhe nuk ka asnjë argument ndaj tij.
Për të pasur më pak katrorë, do të ndërtoj imazhin në 2 faza. E para – përgatitëse, në të cilën do të mbeten të gjitha veglat ndihmëse dhe skedarët përkohësishëm. Ndërsa në imazhin e pastër do të ndaj vetëm versionin përfundimtar të aplikacionit.
Le të fillojmë me imazhin ndihmës.
Për të kompaktuar aplikacionin SPA, zakonisht nevojitet node.js. Do të marr imazhin zyrtar me të cilin gjithashtu ka menaxherë paketa npm dhe yarn. Nga ana ime do të shtoj node-gyp, i cili nevojitet për ndërtimin e disa paketimeve 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/srcGjithë këtu po luftoj për minimalizmin, prandaj imazhi po ndërtohet nga një ekip i madh.
Imazhi i gatshëm mund të gjendet këtu: . Megjithatë, rekomandoj të mos mbështeteni në imazhet e të tjerëve dhe të krijoni të tuajin.
nginx
Për shpërndarjen e statikës mund të përdorni çdo server web. Unë jam mësuar të punoj me nginx, prandaj do ta përdor edhe tani.
Nginx ka një imazh zyrtar Docker, megjithatë për shpërndarjen e thjeshtë të statikës ka shumë module. Çfarë përfshihet në dorëzim mund të shikohet me një komandë të veçantë ose në Dockerfile-in 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 Shtator 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 ta marr për bazë Dockerfile-in, por do të lë aty vetëm atë që është e nevojshme për shpërndarjen e statikës. Versioni im nuk do të mund të punojë për HTTPS, nuk do të mbështesë autorizimin dhe shumë gjëra të tjera. Megjithatë, versioni im do të mund të shpërndajë skedarë të kompresuar me algoritmin Brotli, që është pak më efektiv se gzip. Do të kompresojmë skedarët një herë, nuk është e nevojshme ta bëjmë këtë në fluks.
Ky është Dockerfile-i që kam krijuar. Komentet në rusisht janë të miat, ato në anglisht janë 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 të rregulloj nginx.conf, në mënyrë që gzip dhe brotli të jenë të aktivizuara si parazgjedhje. Po ashtu do të aktivizoj tituj të keqinj, sepse do të kemi statikë që asnjëherë nuk ndryshon. Dhe si një detaj përfundimtar do të bëj që të gjitha kërkesat 404 të ridrejtohen 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 imazhin e gatshëm mund ta bëni këtu: . Ai zë 10.5 MB. Origjinali nginx zinte 19.7 MB. Interesi im sportiv është i kënaqur.
Mësojmë se si statikë të kuptojë variablat e ambientit
Për çfarë mund të nevojiten konfigurimet në SPA? Për shembull, për të përcaktuar se cilin API RESTful të përdorim. Zakonisht, konfigurimet për ambientin e nevojshëm kalohen në SPA në fazën e ndërtimit. Nëse duam të ndryshojmë diçka, do të duhet ta rindërtojmë aplikacionin. Unë nuk dua këtë. Dua që aplikacioni të ndërtohet një herë në fazën CI, dhe të konfigurohet sa herë nevojitet, në fazën CD me ndihmën e variablave të ambientit.
Sigurisht, skedarët statikë vetë nuk kuptojnë asnjë variabël ambienti. Prandaj, do të duhet të shkojmë në një mashtrim. Në imazhin përfundimtar do të mbaj një skript të veçantë shell, i cili do të lexojë variablat e ambientit, do t'i shkruajë ato në skedarët statikë, do t'i comprime dhe vetëm pastaj do t'i kalojë kontrollin nginx.
Për këtë, në Dockerfile parashikohet parametri ENTRYPOINT. T'i kalojmë atij një skript të tillë (në shembullin e 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-файлах
për f në "$TARGET_DIR"/main*.js; bëj
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" "$f"
done
}
kompreso () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttfquot;); bëj
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
done
}
nëse [ "$1" = 'nginx' ]; atëherë
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
nëse [ ! -e "$FLAG_FILE" ]; atëherë
echo "Duke ekzekutuar skriptin e inicializimit"
echo "Duke përmirësuar variablat e mjedisit"
ndërro_variablat
echo "Duke kompresuar skedarët"
kompreso
touch $FLAG_FILE
echo "Përfunduar"
fi
fi
exec "$@"
Për ta bërë skripti punën e vet, konfigurimet në skedarët js duhet të shkruhen në këtë mënyrë:${API_URL}.Duhet theksuar se shumica e SPA-ve moderne gjatë ndërtimit i shtojnë skedarëve të tyre hash-e. Kjo është për të siguruar që shfletuesi të mund të ruajë skedarin për një periudhë të gjatë. Nëse skedari ndryshon, atëherë edhe hash-i i tij do të ndryshojë, e cila nga ana e saj do ta detyrojë shfletuesin të shkarkohet përsëri.
Fatkeqësisht, në metodën time, ndryshimi i konfiguracionit përmes variablave të mjedisit nuk çon në ndryshimin e hash-it të skedarit, kështu që duhet përdorur një mënyrë tjetër për të invaliduar cache-n e shfletuesit. Unë nuk kam këtë problem, sepse konfigurata të ndryshme zhvillohen në ambiente të ndryshme.
Po 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 gjetur mund të grumbullohet dhe të përdoret kudo.
Burimi: habr.com
