Un'applicazione single-page (SPA) è un insieme di file statici JavaScript e HTML, oltre a immagini e altre risorse. Poiché non vengono modificati dinamicamente, pubblicarli su Internet è molto semplice. Esistono molti servizi economici e persino gratuiti, che vanno da GitHub Pages (e per alcuni, anche da narod.ru) fino a CDN come Amazon S3. Tuttavia, avevo bisogno di qualcos'altro.
Avevo bisogno di un'immagine Docker con SPA, in modo che potesse essere facilmente avviata sia in produzione all'interno di un cluster Kubernetes, sia sulla macchina di uno sviluppatore back-end che non ha idea di cosa sia una SPA.
Ho definito i seguenti requisiti per l'immagine:
- facilità d'uso (ma non nella compilazione);
- dimensioni minime sia in termini di spazio su disco che di RAM;
- configurazione tramite variabili d'ambiente, affinché l'immagine possa essere utilizzata in diversi ambienti;
- distribuzione dei file massimamente efficiente.
Oggi vi parlerò di come:
- svuotare nginx;
- compilare brotli dai sorgenti;
- insegnare ai file statici a comprendere le variabili d'ambiente;
- e naturalmente come raccogliere tutto ciò in un'immagine Docker.
L'obiettivo di questo articolo è condividere la mia esperienza e stimolare i membri più esperti della comunità a una critica costruttiva.
Creazione dell'immagine per la compilazione
Per ottenere un'immagine Docker finale di dimensioni contenute, è necessario seguire due regole: il minimo di stratificazioni e un'immagine base minimalista. Uno degli immagini base più piccoli è Alpine Linux, quindi sceglierò proprio quello. Alcuni potrebbero obiettare che Alpine non è adatto per la produzione e, forse, avrebbero ragione. Ma personalmente non ho mai avuto problemi con esso e non ci sono argomenti contrarie.
Per ridurre il numero di stratificazioni, assemblerò l'immagine in due fasi. La prima è una bozza, in cui rimarranno tutti gli strumenti ausiliari e i file temporanei. Nella seconda, invece, registrerò solo la versione finale dell'applicazione.
Iniziamo con l'immagine ausiliaria.
Per compilare un'applicazione SPA, in genere è necessario node.js. Io utilizzerò l'immagine ufficiale che include anche i gestori di pacchetti npm e yarn. Aggiungerò node-gyp, che è necessario per la compilazione di alcuni pacchetti npm, e il compressore Brotli di Google, che ci sarà utile in seguito.
Dockerfile con commenti.
# Базовый образ
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/srcGià qui combatto per il minimalismo, quindi l'immagine viene assemblata con un'unica grande istruzione.
Puoi trovare l'immagine pronta qui: . Anche se consiglio di non fare affidamento sulle immagini degli altri e di costruire la propria.
nginx
Per servire statiche, puoi usare qualsiasi server web. Sono abituato a lavorare con nginx, quindi userò quello anche adesso.
Nginx ha un'immagine Docker ufficiale, tuttavia per una semplice erogazione di statiche ci sono troppi moduli. Puoi vedere quali sono inclusi nella fornitura usando un comando speciale o nel Dockerfile ufficiale.
$ docker run —rm nginx:1-alpine nginx -V
versione nginx: nginx/1.17.9
compilato da gcc 8.3.0 (Alpine 8.3.0)
compilato con OpenSSL 1.1.1d 10 Set 2019
supporto TLS SNI abilitato
argomenti di configurazione: --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-neededPrenderò come base il Dockerfile, ma lascerò solo quello che serve per servire statiche. La mia versione non sarà in grado di funzionare tramite HTTPS, non supporterà l'autenticazione e molto altro. Tuttavia, la mia versione sarà in grado di servire file compressi con l'algoritmo Brotli, che è leggermente più efficiente rispetto a gzip. Comprimeremo i file una sola volta, non è necessario farlo al volo.
Ecco quale Dockerfile ho ottenuto. I commenti in russo sono miei, quelli in inglese sono dell'originale.
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;"]Correggerò subito nginx.conf per assicurarmi che gzip e brotli siano abilitati per default. Attiverò anche le intestazioni di caching, dato che serviremo statiche che non cambiano mai. L'ultimo ritocco sarà reindirizzare tutte le richieste 404 a index.html, necessario per la navigazione in 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";
}
}
}
}Puoi scaricare l'immagine pronta qui: . Occupa 10,5 MB. La versione originale di nginx occupava 19,7 MB. La mia curiosità sportiva è soddisfatta.
Impariamo a far capire ai file statici le variabili d'ambiente
A cosa servono le impostazioni in un SPA? Ad esempio, per specificare quale API RESTful utilizzare. Di solito, le impostazioni per l'ambiente richiesto vengono trasferite nell'SPA durante la fase di build. Se è necessario modificare qualcosa, bisogna ricompilare l'applicazione. Questo non è ciò che voglio. Voglio che l'app venga compilata una sola volta in fase di CI e configurata secondo necessità in fase di CD tramite variabili d'ambiente.
Naturalmente, i file statici di per sé non comprendono alcuna variabile d'ambiente. Pertanto, sarà necessario utilizzare un trucco. Nell'immagine finale eseguirò non nginx, ma uno script shell speciale che leggerà le variabili d'ambiente, le scriverà nei file statici, li comprimerà e solo dopo questo passerà il controllo a nginx.
Per fare questo, nel Dockerfile è previsto il parametro ENTRYPOINT. Passeremo a esso questo script (a titolo di esempio 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-файлах
per f in "$TARGET_DIR"/main*.js; do
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" "$f"
fatto
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttfquot;); do
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
fatto
}
if [ "$1" = 'nginx' ]; then
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
if [ ! -e "$FLAG_FILE" ]; then
echo "Eseguendo lo script di inizializzazione"
echo "Sostituzione delle variabili ambientali"
replace_vars
echo "Compressione dei file"
compress
touch $FLAG_FILE
echo "Fatto"
fi
fi
exec "$@"
Affinché lo script faccia il suo dovere, nelle impostazioni dei file js deve essere scritto in questo modo:${API_URL}.Vale la pena notare che la maggior parte degli SPA moderni aggiunge hash ai propri file durante la build. Questo è necessario affinché il browser possa memorizzare il file nella cache per un lungo periodo. Se il file viene modificato, anche il suo hash cambierà, costringendo il browser a scaricare nuovamente il file.
Sfortunatamente, nel mio metodo, la modifica della configurazione tramite variabili d'ambiente non porta a un cambiamento dell'hash del file, il che significa che è necessario invalidare la cache del browser in un altro modo. Non ho questo problema, perché le diverse configurazioni vengono distribuite in ambienti diversi.
Stiamo costruendo l'immagine finale
Finalmente.
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;"]Ora l'immagine risultante può essere creata e utilizzata ovunque.
Fonte: habr.com
