Dockeri pilt ühe lehe rakenduse levitamiseks

Üheleheline rakendus (SPA) on staatiliste JavaScript- ja HTML-failide ning piltide ja muude ressursside komplekt. Kuna need ei muutu dünaamiliselt, on nende veebis avaldamine väga lihtne. Selle jaoks on palju odavaid ja isegi tasuta teenuseid, alustades lihtsast GitHubi lehtedest (ja mõne jaoks isegi narod.ru-st) ja lõpetades CDN-iga nagu Amazon S3. Siiski vajasin midagi muud.

Mul oli vaja SPA-ga Dockeri pilti, et seda saaks hõlpsasti käivitada nii tootmises Kubernetese klastri osana kui ka taustaarendaja masinas, kellel pole õrna aimugi, mis on SPA.

Olen määranud enda jaoks järgmised pildinõuded:

  • kasutusmugavus (kuid mitte kokkupanemine);
  • minimaalne suurus nii ketta kui ka RAM-i osas;
  • konfigureerimine läbi keskkonnamuutujate, et pilti saaks kasutada erinevates keskkondades;
  • failide kõige tõhusam levitamine.

Täna ütlen teile, kuidas:

  • soolestiku nginx;
  • ehitada brotli allikatest;
  • õpetada staatilisi faile mõistma keskkonnamuutujaid;
  • ja muidugi kuidas sellest kõigest Dockeri pilt kokku panna.

Selle artikli eesmärk on jagada oma kogemusi ja provotseerida kogenud kogukonnaliikmeid konstruktiivsele kriitikale.

Pildi loomine kokkupanekuks

Dockeri lõpliku pildi väikeseks muutmiseks peate järgima kahte reeglit: minimaalselt kihte ja minimalistlikku aluspilti. Üks väiksemaid baaspilte on Alpine Linuxi pilt, nii et ma valin selle. Mõned võivad väita, et Alpine ei sobi tootmiseks, ja neil võib õigus olla. Aga isiklikult pole mul temaga kunagi probleeme olnud ja vastuargumente pole.

Et kihte oleks vähem, panen pildi kokku kahes etapis. Esimene on mustand; kõik abiutiliidid ja ajutised failid jäävad sellesse. Ja viimases etapis panen kirja ainult rakenduse lõpliku versiooni.

Alustame abipildiga.

SPA-rakenduse koostamiseks on tavaliselt vaja faili node.js. Võtan ametliku pildi, mis on ka npm ja lõngapaketi halduritega kaasas. Enda nimel lisan node-gyp, mida on vaja mõne npm paketi ehitamiseks ja Google'i Brotli kompressori, mis on meile hiljem kasulik.

Dockeri fail kommentaaridega.

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

Juba siin võitlen minimalismi eest, nii et pildi paneb kokku üks suur meeskond.

Valmis pildi leiab siit: https://hub.docker.com/r/alexxxnf/spa-builder. Kuigi ma soovitan mitte loota teiste inimeste piltidele ja koguda enda oma.

nginx

Staatilise sisu levitamiseks saate kasutada mis tahes veebiserverit. Olen harjunud nginxiga töötama, seega kasutan seda nüüd.

Nginxil on ametlik Dockeri pilt, kuid sellel on liiga palju mooduleid lihtsaks staatiliseks levitamiseks. Millised on tarne, saab näha spetsiaalse meeskonna poolt või ametlikus Dockerfile'is.

$ 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

Ma võtan aluseks Dockerfile'i, kuid jätan sellesse ainult selle, mis on vajalik staatilise sisu levitamiseks. Minu versioon ei tööta HTTPS-i kaudu, ei toeta autoriseerimist ja palju muud. Kuid minu versioon suudab levitada faile, mis on tihendatud Brotli algoritmiga, mis on pisut tõhusam kui gzip. Tihendame failid üks kord, seda pole vaja käigu pealt teha.

See on Dockerfile, mille juurde ma sattusin. Venekeelsed kommentaarid on minu, inglisekeelsed - originaalist.

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

Parandan kohe faili nginx.conf, et gzip ja brotli oleksid vaikimisi lubatud. Lisan ka vahemällu salvestamise päised, sest me ei muutu kunagi staatiliselt. Ja viimane lihv on suunata kõik 404 taotlust saidile index.html, see on vajalik SPA-s navigeerimiseks.

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

Valmis pildi saad alla laadida siit: https://hub.docker.com/r/alexxxnf/nginx-spa. See võtab 10,5 MB. Algne nginx võttis 19,7 MB. Minu sportlik huvi on rahuldatud.

Staatika õpetamine keskkonnamuutujate mõistmiseks

Miks võib SPA-s seadistusi vaja minna? Näiteks selleks, et määrata, millist RESTful API-d kasutada. Tavaliselt kantakse soovitud keskkonna sätted SPA-sse ehitusetapis. Kui teil on vaja midagi muuta, peate rakenduse uuesti üles ehitama. ma ei taha seda. Soovin, et rakendus ehitataks üks kord CI etapis ja konfigureeritaks nii palju kui vaja CD etapis, kasutades keskkonnamuutujaid.

Loomulikult ei mõista staatilised failid ise ühtegi keskkonnamuutujat. Seetõttu peate kasutama trikki. Lõplikul pildil ei käivita ma nginxi, vaid spetsiaalset shelliskripti, mis loeb keskkonnamuutujaid, kirjutab need staatilistele failidele, tihendab need ja alles siis annab juhtimise üle nginxile.

Selleks pakub Dockerfile parameetri ENTRYPOINT. Anname talle järgmise skripti (kasutades näiteks 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")" > "$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 "$@"

Selleks, et skript saaks oma tööd teha, tuleb sätted js-failidesse kirjutada järgmisel kujul: ${API_URL}.

Tasub teada, et enamus kaasaegseid SPA-sid lisavad ehitamisel oma failidesse räsi. See on vajalik selleks, et brauser saaks faili pikka aega turvaliselt vahemällu salvestada. Kui fail muutub, muutub selle räsi, mis omakorda sunnib brauserit faili uuesti alla laadima.

Kahjuks ei too minu meetodil keskkonnamuutujate kaudu konfiguratsiooni muutmine kaasa faili räsi muutust, mis tähendab, et brauseri vahemälu tuleb muul viisil kehtetuks muuta. Mul seda probleemi ei ole, kuna erinevates keskkondades kasutatakse erinevaid konfiguratsioone.

Lõpliku pildi kokkupanek

Lõpuks.

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

Nüüd saab saadud pilti kokku panna ja kasutada kõikjal.

Allikas: www.habr.com