L'Applicazione a Pagina Singola (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. Ci sono molti servizi economici e persino gratuiti, a partire da GitHub Pages (e per qualcuno 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 da poterla avviare facilmente sia in produzione all'interno di un cluster Kubernetes, sia sulla macchina di un sviluppatore back-end che non ha idea di cosa sia una SPA.
Per me, ho definito i seguenti requisiti per l'immagine:
- facilità d'uso (ma non nella costruzione);
- dimensione minima sia in termini di spazio su disco che di RAM;
- configurazione tramite variabili d'ambiente, in modo che l'immagine possa essere utilizzata in ambienti diversi;
- massima efficienza nella distribuzione dei file.
Oggi vi mostrerò come:
- svuotare nginx;
- compilare brotli dai sorgenti;
- insegnare ai file statici a capire le variabili d'ambiente;
- e naturalmente, come assemblare tutto questo in un'immagine Docker.
L'obiettivo di questo articolo è condividere la mia esperienza e stimolare i membri esperti della comunità a fornire critiche costruttive.
Costruzione dell'immagine per il build
Per ottenere un'immagine finale di Docker di dimensioni ridotte, è importante seguire due regole: ridurre al minimo i livelli e utilizzare un'immagine di base minimalista. Una delle immagini di base più piccole è l'immagine di Alpine Linux, quindi opterò per questa. Alcuni potrebbero obiettare che Alpine non è adatta per la produzione, e potrebbero avere ragione. Tuttavia, personalmente non ho mai avuto problemi con essa e non ho argomenti contro di essa.
Per ridurre il numero di livelli, costruirò l'immagine in 2 fasi. La prima è una bozza, in cui rimarranno tutte le utility ausiliarie e i file temporanei. Nella seconda, pulita, registrerò solo la versione finale dell'applicazione.
Iniziamo con l'immagine ausiliaria.
Per compilare un'applicazione SPA, di solito è necessario node.js. Utilizzerò l'immagine ufficiale che include anche i gestori di pacchetti npm e yarn. Aggiungerò node-gyp, necessario per la costruzione di alcuni pacchetti npm, e il compressore Brotli di Google, che ci sarà utile più tardi.
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/srcQui combatto per il minimalismo, quindi l'immagine viene composta da un grande team.
Puoi trovare l'immagine pronta qui: . Anche se consiglio di non affidarti alle immagini altrui e di assemblare la tua.
nginx
Per servire file statici, puoi utilizzare qualsiasi server web. Io sono abituato a lavorare con nginx, quindi lo utilizzerò anche adesso.
nginx ha un'immagine Docker ufficiale, ma per una semplice distribuzione di file statici include troppi moduli. Puoi vedere quali sono inclusi nel pacchetto utilizzando un comando speciale o consultando il 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 SNI TLS 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 ciò che è necessario per la distribuzione della staticità. La mia versione non potrà funzionare tramite HTTPS, non supporterà l'autenticazione e molte altre cose. 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 c'è necessità di farlo al volo.
Ecco come è risultato il mio Dockerfile. 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;"]Modificherò immediatamente nginx.conf, in modo che gzip e Brotli siano attivati di default. Includerò anche le intestazioni di caching, poiché distribuirà una staticità che non cambierà mai. Infine, reindirizzerò tutte le richieste 404 a index.html, il che è necessario per la navigazione in un 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: . Occupando 10,5 MB. La versione originale di nginx occupava 19,7 MB. La mia curiosità sportiva è soddisfatta.
Insegniamo alla statica a comprendere le variabili d'ambiente.
A cosa servono le configurazioni in un SPA? Ad esempio, per specificare quale API RESTful utilizzare. Di solito, le configurazioni per l'ambiente necessario vengono passate nell'SPA durante il processo di build. Se è necessario modificare qualcosa, è necessario ricompilare l'applicazione. Io non voglio questo. Voglio che l'applicazione venga compilata una sola volta nella fase CI e configurata tante volte quanto necessario nella fase CD tramite variabili di ambiente.
Naturalmente, i file statici di per sé non comprendono alcuna variabile di ambiente. Pertanto, dovrò attuare un trucco. Nell'immagine finale, non eseguirò nginx, ma uno script shell speciale che leggerà le variabili di ambiente, le scriverà nei file statici, li comprimerà e solo dopo passerà il controllo a nginx.
Per questo, nel Dockerfile è previsto il parametro ENTRYPOINT. Passeremo questo script (con un esempio in 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"
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 "Running init script"
echo "Replacing env vars"
replace_vars
echo "Compressing files"
compress
touch $FLAG_FILE
echo "Done"
fi
fi
exec "$@"
Affinché lo script faccia il suo dovere, le configurazioni nei file js devono essere scritte in questo modo:${API_URL}.È importante notare che la maggior parte delle moderne SPA 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, cambia anche il suo hash, il che costringe il browser a scaricare nuovamente il file.
Sfortunatamente, nel mio metodo, la modifica della configurazione tramite variabili di ambiente non porta a una modifica dell'hash del file, il che significa che è necessario invalidare la cache del browser in un altro modo. Non ho questo problema, poiché diverse configurazioni vengono distribuite in ambienti diversi.
Creiamo 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
