Een Single-page Application (SPA) is een verzameling statische JavaScript- en HTML-bestanden, evenals afbeeldingen en andere middelen. Aangezien deze niet dynamisch worden gewijzigd, is het heel eenvoudig om ze op internet te publiceren. Hiervoor zijn er veel goedkope en zelfs gratis services, variërend van eenvoudige GitHub Pages (en voor sommigen zelfs van narod.ru) tot CDN's zoals Amazon S3. Maar ik had iets anders nodig.
Ik had een Docker-image met een SPA nodig, zodat het eenvoudig zou kunnen worden gestart, zowel in productie als op de machine van een back-end ontwikkelaar die geen idee heeft wat een SPA is.
Ik heb de volgende vereisten voor de image vastgesteld:
- gebruiksvriendelijkheid (maar niet bij het bouwen);
- minimale grootte, zowel qua schijfruimte als qua RAM;
- configuratie via omgevingsvariabelen, zodat de image in verschillende omgevingen kan worden gebruikt;
- maximaal efficiënte bestandsdistributie.
Vandaag vertel ik je hoe:
- nginx te elimineren;
- brotli uit de bronbestanden te compileren;
- statische bestanden te leren om omgevingsvariabelen te begrijpen;
- en natuurlijk hoe je daaruit een Docker-image kunt bouwen.
Het doel van dit artikel is mijn ervaring te delen en ervaren leden van de community uit te dagen tot constructieve kritiek.
Image bouwen voor assemblage
Om ervoor te zorgen dat de uiteindelijke Docker-image klein van formaat is, moeten we twee regels volgen: minimaal aantal layers en een minimalistische basisimage. Een van de kleinste basisimages is de Alpine Linux-image, dus die kies ik. Sommigen kunnen betwisten dat Alpine niet geschikt is voor productie en misschien hebben ze gelijk. Maar persoonlijk heb ik daar nooit problemen mee gehad en er zijn geen argumenten tegen.
Om fewer layers te hebben, ga ik de image in twee fasen bouwen. De eerste fase is ruw, hierin blijven alle hulpprogramma's en tijdelijke bestanden. In de schone versie zal ik alleen de uiteindelijke versie van de applicatie opnemen.
Laten we beginnen met de hulplijsten.
Om een SPA-toepassing te compilen, is meestal node.js nodig. Ik neem de officiële image, die ook pakketbeheerders npm en yarn bevat. Daar voeg ik node-gyp aan toe, dat nodig is voor de bouw van sommige npm-pakketten, en de Brotli-compressor van Google, die we later zullen gebruiken.
Dockerfile met opmerkingen.
# Базовый образ
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/srcHier vecht ik al voor minimalisme, dus de image wordt gebouwd met één grote opdracht.
Je kunt de kant-en-klare afbeelding hier vinden: . Hoewel ik aanbeveel niet afhankelijk te zijn van andermans beelden en je eigen op te bouwen.
met alles wat daarin al aanwezig is, en de inhoud van de map
Voor het serveren van statische bestanden kun je elke webserver gebruiken. Ik ben gewend om met nginx te werken, dus zal ik deze ook nu gebruiken.
Nginx heeft een officiële Docker-afbeelding, maar voor eenvoudige statische bestandservering zitten er te veel modules in. Welke precies zijn meegeleverd, kun je bekijken met een speciale opdracht of in het officiële Dockerfile.
$ docker run —rm nginx:1-alpine nginx -V
nginx versie: nginx/1.17.9
gebouwd door gcc 8.3.0 (Alpine 8.3.0)
gemaakt met OpenSSL 1.1.1d 10 sep 2019
TLS SNI-ondersteuning ingeschakeld
configureerargumenten: --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-neededIk neem de Dockerfile als basis, maar laat alleen de dingen staan die nodig zijn voor het serveren van statische bestanden. Mijn versie kan niet over HTTPS werken, ondersteunt geen autorisatie en nog veel meer. Maar mijn versie kan bestanden serviren die zijn gecomprimeerd met het Brotli-algoritme, dat iets efficiënter is dan gzip. We zullen de bestanden één keer comprimeren, on-the-fly compressie is niet nodig.
Dit is de Dockerfile die ik heb gemaakt. De opmerkingen in het Russisch zijn van mij, de Engelse zijn uit het origineel.
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;"]Ik zal direct de nginx.conf aanpassen zodat gzip en brotli standaard zijn ingeschakeld. Ik zal ook cachingheaders inschakelen, aangezien we statische bestanden serveren die nooit veranderen. En de laatste aanpassing is het doorverwijzen van alle 404-verzoeken naar index.html, wat nodig is voor navigatie in een 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";
}
}
}
}Download the ready-made image here: . It takes up 10.5 MB. The original nginx took 19.7 MB. My sporting interest is satisfied.
We teach static files to understand environment variables
Why might configurations be needed in an SPA? For example, to specify which RESTful API to use. Usually, the settings for the required environment are passed to the SPA during the build process. If you need to change something, you'll have to rebuild the application. I don't want that. I want the application to be built once during CI and configured as needed during CD using environment variables.
Of course, static files themselves do not understand any environment variables. So, we'll have to be clever. In the final image, I will not be running nginx, but a special shell script that will read the environment variables, write them to the static files, compress them, and only then hand control over to nginx.
For this, the ENTRYPOINT parameter is provided in the Dockerfile. Let's pass it this script (using Angular as an example):
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-файлах
voor f in "$TARGET_DIR"/main*.js; doe
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" "$f"
klaar
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttfquot;); doe
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
klaar
}
als [ "$1" = 'nginx' ]; dan
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
als [ ! -e "$FLAG_FILE" ]; dan
echo "Init-script draait"
echo "Vervangen van omgevingsvariabelen"
replace_vars
echo "Bestanden comprimeren"
compress
touch $FLAG_FILE
echo "Klaar"
fi
fi
exec "$@"
For the script to do its job, the settings in the js files should be written in this format:${API_URL}.It is worth noting that most modern SPAs add hashes to their files during the build process. This is necessary so that the browser can cache the file for a long time. If the file does change, its hash will change as well, which in turn will force the browser to download the file again.
Helaas, in mijn methode leidt het wijzigen van de configuratie via omgevingsvariabelen niet tot een wijziging van de bestandshash, wat betekent dat de browsercache op een andere manier moet worden ongeldig gemaakt. Ik heb dit probleem niet, omdat verschillende configuraties in verschillende omgevingen worden uitgerold.
We verzamelen het definitieve beeld.
Eindelijk.
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 de resulterende afbeelding worden verzameld en overal gebruikt worden.
Bron: habr.com
