Docker slika za distribuciju aplikacije za jednu stranicu

Aplikacija za jednu stranicu (SPA) je skup statičkih JavaScript i HTML datoteka, kao i slika i drugih resursa. Budući da se ne mijenjaju dinamički, objavljivanje na internetu je vrlo jednostavno. Postoji veliki broj jeftinih, pa čak i besplatnih servisa za ovo, počevši od jednostavnih GitHub stranica (a za neke čak i narod.ru) do CDN-a kao što je Amazon S3. Međutim, trebalo mi je nešto drugo.

Trebao mi je Docker imidž sa SPA kako bi se lako mogao pokrenuti iu proizvodnji kao dio Kubernetes klastera, i na mašini back-end programera koji nema pojma šta je SPA.

Za sebe sam odredio sljedeće zahtjeve za sliku:

  • jednostavnost upotrebe (ali ne montaže);
  • minimalna veličina iu smislu diska i RAM-a;
  • konfiguraciju kroz varijable okruženja tako da se slika može koristiti u različitim okruženjima;
  • najefikasnija distribucija fajlova.

Danas ću vam reći kako:

  • gut nginx;
  • izgraditi brotli iz izvora;
  • naučiti statičke datoteke da razumiju varijable okruženja;
  • i naravno kako sastaviti Docker sliku od svega ovoga.

Svrha ovog članka je podijeliti svoje iskustvo i isprovocirati iskusne članove zajednice na konstruktivnu kritiku.

Izgradnja slike za sklapanje

Da bi konačna Docker slika bila mala, morate se pridržavati dva pravila: minimum slojeva i minimalističku osnovnu sliku. Jedna od najmanjih osnovnih slika je slika Alpine Linuxa, tako da ću to izabrati. Neki bi mogli tvrditi da Alpine nije pogodan za proizvodnju, i možda su u pravu. Ali lično, nikada nisam imao problema sa njim i nema nikakvih argumenata protiv njega.

Kako bih imao manje slojeva, sastavit ću sliku u 2 faze. Prvi je nacrt, u njemu će ostati svi pomoćni programi i privremeni fajlovi. I u završnoj fazi ću samo napisati konačnu verziju aplikacije.

Počnimo s pomoćnom slikom.

Da biste kompajlirali SPA aplikaciju, obično vam je potreban node.js. Uzet ću službenu sliku, koja također dolazi s npm i yarn paket menadžerima. U svoje ime dodat ću node-gyp, koji je potreban za pravljenje nekih npm paketa, i Brotli kompresor iz Google-a, koji će nam kasnije biti od koristi.

Dockerfile sa komentarima.

# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <[email protected]>"
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/src

Već ovdje se borim za minimalizam, tako da imidž sastavlja jedan veliki tim.

Gotovu sliku možete pronaći ovdje: https://hub.docker.com/r/alexxxnf/spa-builder. Iako preporučujem da se ne oslanjate na tuđe slike i da sakupljate svoje.

nginx

Možete koristiti bilo koji web server za distribuciju statičkog sadržaja. Navikao sam da radim sa nginxom, pa ću ga sada koristiti.

Nginx ima službenu Docker sliku, ali ima previše modula za jednostavnu statičku distribuciju. Koji su uključeni u isporuku može se vidjeti u posebnom timu ili u službenom Dockerfileu.

$ docker run --rm nginx:1-alpine nginx -V

nginx version: nginx/1.17.9
built by gcc 8.3.0 (Alpine 8.3.0) 
built with OpenSSL 1.1.1d  10 Sep 2019
TLS SNI support enabled
configure arguments: --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-needed

Koristiću Dockerfile kao osnovu, ali ću u njemu ostaviti samo ono što je potrebno za distribuciju statičkog sadržaja. Moja verzija neće moći raditi preko HTTPS-a, neće podržavati autorizaciju i još mnogo toga. Ali moja verzija će moći distribuirati datoteke komprimirane pomoću Brotli algoritma, koji je malo efikasniji od gzip-a. Fajlove ćemo komprimirati jednom; nema potrebe da to radimo u hodu.

Ovo je Dockerfile sa kojim sam završio. Komentari na ruskom su moji, na engleskom - iz originala.

dockerfile

# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <[email protected]>"
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;"]

Odmah ću popraviti nginx.conf tako da su gzip i brotli uključeni po defaultu. Uključiću i zaglavlja za keširanje, jer nikada nećemo mijenjati statičke. I posljednji dodir će biti preusmjeravanje svih 404 zahtjeva na index.html, ovo je neophodno za navigaciju u 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";
            }
        }
    }
}

Gotovu sliku možete preuzeti ovdje: https://hub.docker.com/r/alexxxnf/nginx-spa. Zauzima 10,5 MB. Originalni nginx zauzimao je 19,7 MB. Moj sportski interes je zadovoljan.

Podučavanje statike za razumijevanje varijabli okruženja

Zašto mogu biti potrebna podešavanja u SPA? Na primjer, da biste odredili koji RESTful API koristiti. Obično se postavke za željeno okruženje prenose u SPA u fazi izgradnje. Ako trebate nešto promijeniti, morat ćete ponovo izgraditi aplikaciju. Ne želim to. Želim da se aplikacija jednom napravi u CI fazi i konfiguriše koliko god je potrebno u CD fazi koristeći varijable okruženja.

Naravno, sami statički fajlovi ne razumeju nikakve varijable okruženja. Stoga ćete morati upotrijebiti trik. U konačnoj slici neću pokrenuti nginx, već posebnu shell skriptu koja će čitati varijable okruženja, pisati ih u statičke datoteke, komprimirati ih i tek onda prenijeti kontrolu na nginx.

U tu svrhu, Dockerfile pruža parametar ENTRYPOINT. Dajmo mu sljedeću skriptu (koristeći Angular kao primjer):

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")" > "$f"
done
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttf


quot;); do
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
done
}
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 "$@"

Da bi skripta radila svoj posao, postavke moraju biti zapisane u js fajlovima u ovom obliku: ${API_URL}.

Vrijedi napomenuti da većina modernih SPA-ova dodaje hashove u svoje datoteke prilikom izgradnje. Ovo je neophodno kako bi pretraživač mogao sigurno keširati datoteku dugo vremena. Ako se datoteka promijeni, tada će se promijeniti njen hash, što će zauzvrat natjerati pretraživač da ponovo preuzme datoteku.

Nažalost, u mojoj metodi, promjena konfiguracije kroz varijable okruženja ne dovodi do promjene u hash datoteke, što znači da keš pretraživača mora biti poništen na neki drugi način. Nemam ovaj problem jer su različite konfiguracije raspoređene u različitim okruženjima.

Sastavljanje konačne slike

Konačno.

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;"]

Sada se rezultirajuća slika može sastaviti i koristiti bilo gdje.

izvor: www.habr.com