Docker-kuva yhden sivun sovelluksen jakelua varten

Single-page Application (SPA) on joukko staattisia JavaScript- ja HTML-tiedostoja sekä kuvia ja muita resursseja. Koska ne eivät muutu dynaamisesti, niiden julkaiseminen verkossa on erittäin helppoa. Tätä varten on olemassa suuri määrä halpoja ja jopa ilmaisia ​​palveluita alkaen yksinkertaisista GitHub-sivuista (ja joidenkin jopa narod.ru) ja päättyen CDN:ään, kuten Amazon S3. Tarvitsin kuitenkin jotain muuta.

Tarvitsin Docker-kuvan SPA:lla, jotta se voitaisiin helposti käynnistää sekä tuotannossa osana Kubernetes-klusteria että sellaisen taustakehittäjän koneella, jolla ei ole aavistustakaan SPA:sta.

Olen määrittänyt itselleni seuraavat kuvavaatimukset:

  • helppokäyttöisyys (mutta ei kokoamista);
  • vähimmäiskoko sekä levyn että RAM-muistin suhteen;
  • konfigurointi ympäristömuuttujien kautta niin, että kuvaa voidaan käyttää eri ympäristöissä;
  • tehokkain tiedostojen jakelu.

Tänään kerron sinulle kuinka:

  • suoliston nginx;
  • rakentaa brotli lähteistä;
  • opettaa staattisia tiedostoja ymmärtämään ympäristömuuttujia;
  • ja tietysti kuinka koota Docker-kuva kaikesta tästä.

Tämän artikkelin tarkoituksena on jakaa kokemukseni ja provosoida kokeneita yhteisön jäseniä rakentavaan kritiikkiin.

Kuvan rakentaminen kokoonpanoa varten

Jotta lopullisesta Docker-kuvasta tulee pienikokoinen, sinun on noudatettava kahta sääntöä: kerrosten vähimmäismäärä ja minimalistinen peruskuva. Yksi pienimmistä peruskuvista on Alpine Linux -kuva, joten valitsen sen. Jotkut saattavat väittää, että Alpine ei sovellu tuotantoon, ja he saattavat olla oikeassa. Mutta henkilökohtaisesti minulla ei ole koskaan ollut ongelmia hänen kanssaan, eikä häntä vastaan ​​ole argumentteja.

Jotta kerroksia olisi vähemmän, kokoan kuvan kahdessa vaiheessa. Ensimmäinen on luonnos; kaikki apuohjelmat ja väliaikaiset tiedostot säilyvät siinä. Ja viimeisessä vaiheessa kirjoitan vain sovelluksen lopullisen version.

Aloitetaan apukuvasta.

SPA-sovelluksen kääntämiseen tarvitaan yleensä node.js. Otan virallisen kuvan, joka tulee myös npm- ja lankapakettien hallintaohjelmien mukana. Lisään omasta puolestani node-gypin, jota tarvitaan joidenkin npm-pakettien rakentamiseen, sekä Googlen Brotli-kompressorin, josta on meille hyötyä myöhemmin.

Docker-tiedosto kommenteilla.

# Базовый образ
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

Taistelen jo täällä minimalismin puolesta, joten kuvan kokoaa yksi iso tiimi.

Valmis kuva löytyy täältä: https://hub.docker.com/r/alexxxnf/spa-builder. Suosittelen kuitenkin, että et luota muiden ihmisten kuviin ja kerää omia kuviasi.

Nginx

Voit käyttää mitä tahansa verkkopalvelinta staattisen sisällön jakamiseen. Olen tottunut työskentelemään nginxin kanssa, joten käytän sitä nyt.

Nginxillä on virallinen Docker-kuva, mutta siinä on liian monta moduulia yksinkertaiseen staattiseen jakeluun. Mitkä ovat toimitukseen sisältyvät, sen voi nähdä erikoistiimi tai virallinen Dockerfile.

$ 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

Käytän pohjana Docker-tiedostoa, mutta jätän siihen vain sen, mitä tarvitaan staattisen sisällön jakamiseen. Versioni ei toimi HTTPS:n yli, se ei tue valtuutusta ja paljon muuta. Mutta minun versioni pystyy jakamaan tiedostoja, jotka on pakattu Brotli-algoritmilla, joka on hieman tehokkaampi kuin gzip. Pakkaamme tiedostot kerran; tätä ei tarvitse tehdä lennossa.

Tämä on Docker-tiedosto, johon päädyin. Venäjänkieliset kommentit ovat minun, englanninkieliset - alkuperäisestä.

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

Korjaan välittömästi nginx.conf-tiedoston, jotta gzip ja brotli ovat oletuksena käytössä. Lisään myös välimuistin otsikot, koska staattinen jännite ei muutu koskaan. Ja viimeinen silaus on ohjata kaikki 404 pyyntöä index.html-osoitteeseen, tämä on tarpeen SPA:ssa navigoimiseksi.

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

Voit ladata valmiin kuvan tästä: https://hub.docker.com/r/alexxxnf/nginx-spa. Se vie 10,5 MB. Alkuperäinen nginx vei 19,7 Mt. Urheiluharrastukseni on tyytyväinen.

Statiikan opettaminen ympäristömuuttujien ymmärtämiseksi

Miksi SPA:ssa saatetaan tarvita asetuksia? Esimerkiksi määrittääkseen käytettävän RESTful API:n. Tyypillisesti halutun ympäristön asetukset siirretään SPA:lle rakennusvaiheessa. Jos sinun on muutettava jotain, sinun on rakennettava sovellus uudelleen. En halua sitä. Haluan, että sovellus rakennetaan kerran CI-vaiheessa ja konfiguroidaan niin paljon kuin on tarpeen CD-vaiheessa ympäristömuuttujien avulla.

Tietenkään staattiset tiedostot eivät itse ymmärrä mitään ympäristömuuttujia. Siksi sinun on käytettävä temppua. Lopullisessa kuvassa en käynnistä nginxiä, vaan erityisen shell-skriptin, joka lukee ympäristömuuttujat, kirjoittaa ne staattisiin tiedostoihin, pakkaa ne ja vasta sitten siirtää ohjauksen nginxiin.

Tätä tarkoitusta varten Dockerfile tarjoaa ENTRYPOINT-parametrin. Annetaan hänelle seuraava käsikirjoitus (käyttäen Angular esimerkkinä):

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 "$@"

Jotta komentosarja voisi tehdä tehtävänsä, asetukset on kirjoitettava js-tiedostoihin tässä muodossa: ${API_URL}.

On syytä huomata, että useimmat nykyaikaiset SPA:t lisäävät tiedostoihinsa tiivisteitä rakentaessaan. Tämä on välttämätöntä, jotta selain voi tallentaa tiedoston turvallisesti välimuistiin pitkään. Jos tiedosto muuttuu, sen hash muuttuu, mikä puolestaan ​​pakottaa selaimen lataamaan tiedoston uudelleen.

Valitettavasti menetelmässäni konfiguroinnin muuttaminen ympäristömuuttujien kautta ei johda tiedoston tiivisteen muutokseen, mikä tarkoittaa, että selaimen välimuisti on mitätöitävä jollain muulla tavalla. Minulla ei ole tätä ongelmaa, koska eri kokoonpanot ovat käytössä eri ympäristöissä.

Lopullisen kuvan yhdistäminen

Lopulta.

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

Nyt tuloksena oleva kuva voidaan koota ja käyttää missä tahansa.

Lähde: will.com