Enkelt-sidet applikation (SPA) er en samling af statiske JavaScript- og HTML-filer, samt billeder og andre ressourcer. Da de ikke ændres dynamisk, er det meget nemt at offentliggøre dem på internettet. Der findes et stort antal billige og endda gratis tjenester, der spænder fra den enkle GitHub Pages (og for nogle endda fra narod.ru) til CDN som Amazon S3. Men jeg havde brug for noget andet.
Jeg havde brug for et Docker-image med SPA, så det nemt kunne køres både i produktionen som en del af et Kubernetes-klynge og på maskinen hos back-end udvikleren, som ikke har nogen idé om, hvad en SPA er.
Jeg har defineret følgende krav til image:
- simpel at bruge (men ikke at bygge);
- minimal størrelse både med hensyn til disk og RAM;
- konfiguration via miljøvariabler, så image kan bruges i forskellige miljøer;
- maksimalt effektiv distribution af filer.
I dag vil jeg fortælle hvordan:
- udtømme nginx;
- kompilere brotli fra kilden;
- lære de statiske filer at forstå miljøvariabler;
- og selvfølgelig hvordan man samler et Docker-image ud fra alt dette.
Målet med denne artikel er at dele min erfaring og provokere erfarne medlemmer af fællesskabet til konstruktiv kritik.
Bygning af image til bygning
For at det endelige Docker-image skal være lille i størrelse, skal man overholde to regler: minimum af lag og et minimalistisk base-image. Et af de mindste base-images er Alpine Linux-billedet, så det vælger jeg. Nogle kan indvende, at Alpine ikke egner sig til produktion, og det kan være sandt. Men personligt har jeg aldrig haft problemer med det, og der er ingen argumenter imod det.
For at have færre lag, vil jeg bygge image i 2 trin. Det første – rå, som vil indeholde alle hjælpemidler og midlertidige filer. Og i det rene vil jeg kun gemme den endelige version af applikationen.
Lad os starte med det hjælpeimage.
For at kompilere SPA-applikationen kræves som regel node.js. Jeg vil tage det officielle image, som også inkluderer pakkehåndtererne npm og yarn. Derudover vil jeg tilføje node-gyp, som er nødvendig for at bygge nogle npm-pakker, samt Brotli-kompressoren fra Google, som vil være nyttig senere.
Dockerfile med kommentarer.
# Базовый образ
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/srcAllerede her kæmper jeg for minimalisme, så image bygges med en stor enkelt kommando.
Den færdige billede kan findes her: . Selvom jeg anbefaler ikke at stole på andres billeder og samle din egen.
nginx
Til at servere statiske filer kan enhver webserver bruges. Jeg er vant til at arbejde med nginx, så jeg vil også bruge det nu.
Nginx har et officielt Docker-billede, men der er for mange moduler til simpel servering af statiske filer. Hvilke der er inkluderet kan ses med en speciel kommando eller i den officielle Dockerfile.
$ docker run —rm nginx:1-alpine nginx -V
nginx version: nginx/1.17.9
bygget af gcc 8.3.0 (Alpine 8.3.0)
bygget med OpenSSL 1.1.1d 10 Sep 2019
TLS SNI support aktiveret
konfigurationsargumenter: --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-neededJeg vil tage udgangspunkt i Dockerfile, men kun inkludere det, der er nødvendigt for at servere statiske filer. Min version vil ikke kunne køre via HTTPS, understøtte autorisation og meget mere. Til gengæld kan min version servere filer komprimeret med Brotli-algoritmen, som er lidt mere effektiv end gzip. Vi vil komprimere filerne én gang; det er ikke nødvendigt at gøre det on-the-fly.
Her er min Dockerfile. Kommentarer på russisk – mine, på engelsk – fra originalen.
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;"]Jeg vil straks rette nginx.conf, så gzip og brotli er aktiveret som standard. Jeg vil også aktivere caching-overskrifter, da vi vil servere statisk indhold, der aldrig ændres. Som det sidste vil jeg omdirigere alle 404-anmodninger til index.html, da dette er nødvendigt for navigation i 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";
}
}
}
}Du kan downloade det færdige billede her: . Den fylder 10,5 MB. Den originale nginx fyldte 19,7 MB. Min sportslige interesse er tilfredsstillet.
Vi lærer statikken at forstå miljøvariabler
Hvorfor kan der være behov for indstillinger i en SPA? For eksempel for at angive hvilken RESTful API der skal bruges. Normalt sendes indstillinger for det relevante miljø til SPA'en under byggefasen. Hvis der skal ændres noget, skal applikationen genopbygges. Det ønsker jeg ikke. Jeg vil have, at applikationen bygges én gang i CI-fasen, og konfigureres så meget som nødvendigt i CD-fasen ved hjælp af miljøvariabler.
Selvfølgelig forstår statiske filer ikke nogen miljøvariabler af sig selv. Derfor må vi finde på noget. I det endelige billede vil jeg ikke køre nginx, men et specielt shell-script, der læser miljøvariablerne, skriver dem til de statiske filer, komprimerer dem og derefter overgiver kontrollen til nginx.
For dette er der en ENTRYPOINT parameter i Dockerfile. Lad os videregive et sådant script (eksempel med 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 "Kører init script"
echo "Erstatter miljøvariabler"
replace_vars
echo "Komprimerer filer"
komprimer
touch $FLAG_FILE
echo "Færdig"
fi
fi
exec "$@"
For at scriptet kan udføre sin opgave, skal indstillingerne i js-filerne skrives på følgende måde:${API_URL}.Det er værd at bemærke, at de fleste moderne SPA'er ved bygning tilføjer hashes til deres filer. Dette er nødvendigt, så browseren kan cache filen i lang tid. Hvis filen ændres, ændres dens hash også, hvilket får browseren til at downloade filen igen.
Desværre fører ændring af konfigurationen via miljøvariabler ikke til en ændring af filens hash, og derfor skal browserens cache invalideres på en anden måde. Jeg har ikke dette problem, fordi forskellige konfigurationer implementeres i forskellige miljøer.
Vi samler det endelige billede
Endelig.
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;"]Nu kan det resulterende billede samles og bruges hvor som helst.
Kilde: habr.com
