Нэг хуудасны програмыг түгээх зориулалттай докерын зураг

Single-page Application (SPA) нь статик JavaScript болон HTML файлууд, түүнчлэн зураг болон бусад эх сурвалжуудын багц юм. Тэд динамикаар өөрчлөгддөггүй тул онлайнаар нийтлэх нь маш хялбар байдаг. Энгийн GitHub хуудсуудаас (зарим хүмүүсийн хувьд narod.ru) эхлээд Amazon S3 шиг CDN хүртэл олон тооны хямд, бүр үнэгүй үйлчилгээнүүд байдаг. Гэсэн хэдий ч надад өөр зүйл хэрэгтэй байсан.

Надад SPA-тай Docker дүрс хэрэгтэй байсан бөгөөд ингэснээр үүнийг Кубернетес кластерын нэг хэсэг болгон үйлдвэрлэлд болон SPA гэж юу болохыг мэдэхгүй арын хөгжүүлэгчийн машин дээр хялбархан ажиллуулж болно.

Би өөртөө дараах зургийн шаардлагуудыг тодорхойлсон.

  • ашиглахад хялбар (гэхдээ угсрах биш);
  • диск болон RAM-ийн хувьд хамгийн бага хэмжээ;
  • орчны хувьсагчаар дамжуулан дүрсийг өөр өөр орчинд ашиглах боломжтой болгох;
  • файлуудын хамгийн үр дүнтэй хуваарилалт.

Өнөөдөр би танд яаж хэлэх болно:

  • гэдэсний nginx;
  • эх сурвалжаас brotli барих;
  • орчны хувьсагчдыг ойлгохын тулд статик файлуудыг заах;
  • мөн мэдээж энэ бүхнээс Docker дүрсийг хэрхэн угсрах талаар.

Энэ нийтлэлийн зорилго бол өөрийн туршлагаас хуваалцах, олон нийтийн туршлагатай гишүүдийг бүтээлч шүүмжлэлд өдөөх явдал юм.

Угсрах зориулалттай зураг бүтээх

Докерын эцсийн дүрсийг жижиг хэмжээтэй болгохын тулд та хоёр дүрмийг баримтлах хэрэгтэй: хамгийн бага давхарга ба минималист үндсэн зураг. Хамгийн жижиг суурь зургуудын нэг бол Alpine Linux-ийн зураг тул би үүнийг л сонгох болно. Зарим нь Альпийн нурууг үйлдвэрлэхэд тохиромжгүй гэж маргаж магадгүй бөгөөд тэдний зөв байж магадгүй юм. Гэхдээ би хувьдаа түүнтэй хэзээ ч асуудал үүсгэж байгаагүй, түүний эсрэг маргалдсан зүйл байхгүй.

Цөөн давхаргатай байхын тулд би зургийг 2 үе шаттайгаар угсарна. Эхнийх нь ноорог бөгөөд бүх туслах хэрэгслүүд болон түр файлууд үүнд үлдэх болно. Эцсийн шатанд би зөвхөн програмын эцсийн хувилбарыг бичих болно.

Туслах зургаас эхэлцгээе.

SPA програмыг эмхэтгэхийн тулд танд ихэвчлэн node.js хэрэгтэй. Би npm болон утас багцын менежерүүдтэй хамт ирдэг албан ёсны зургийг авах болно. Би өөрийн нэрийн өмнөөс зарим npm багцуудыг бүтээхэд шаардлагатай node-gyp болон Google-ийн Brotli компрессорыг нэмж оруулах болно, энэ нь хожим бидэнд хэрэг болно.

Сэтгэгдэл бүхий Dockerfile.

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

Би энд аль хэдийн минимализмын төлөө тэмцэж байгаа тул нэг том баг имижийг бүрдүүлдэг.

Дууссан зургийг эндээс харж болно: https://hub.docker.com/r/alexxxnf/spa-builder. Хэдийгээр би бусдын зураг дээр найдах хэрэггүй, өөрийнхөө зургийг цуглуулахыг зөвлөж байна.

nginx

Та статик контентыг түгээхийн тулд ямар ч вэб сервер ашиглаж болно. Би nginx-тэй ажиллаж дассан тул одоо ашиглах болно.

Nginx нь албан ёсны Docker дүрстэй боловч энгийн статик түгээлтийн хувьд хэт олон модультай. Хүргэлтэнд аль нь багтсаныг тусгай баг эсвэл албан ёсны 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

Би Dockerfile-ийг үндэс болгон ашиглах болно, гэхдээ би зөвхөн статик контентыг түгээхэд шаардлагатай зүйлийг л үлдээх болно. Миний хувилбар HTTPS дээр ажиллах боломжгүй, зөвшөөрлийг дэмжихгүй гэх мэт. Гэхдээ миний хувилбар нь Brotli алгоритмаар шахагдсан файлуудыг түгээх боломжтой бөгөөд энэ нь gzip-ээс арай илүү үр дүнтэй байдаг. Бид файлуудыг нэг удаа шахах бөгөөд үүнийг шууд хийх шаардлагагүй.

Энэ бол миний дуусгасан 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;"]

Би nginx.conf-г нэн даруй засч, gzip болон brotli-г анхдагчаар идэвхжүүлнэ. Би бас кэшийн толгойг оруулах болно, учир нь бид статикийг хэзээ ч өөрчлөхгүй. Эцсийн мэдрэгч нь бүх 404 хүсэлтийг index.html руу дахин чиглүүлэх болно, энэ нь ДЦГ-т навигац хийхэд шаардлагатай.

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

Та дууссан зургийг эндээс татаж авах боломжтой: https://hub.docker.com/r/alexxxnf/nginx-spa. Энэ нь 10,5 MB зай эзэлнэ. Анхны nginx нь 19,7 MB багтаамжтай. Миний спортын сонирхол сэтгэл хангалуун байна.

Орчны хувьсагчдыг ойлгохын тулд статикийг заах

SPA-д яагаад тохиргоо хэрэгтэй байж болох вэ? Жишээлбэл, аль RESTful API ашиглахыг зааж өгөхийн тулд. Ихэвчлэн бүтээх шатанд хүссэн орчны тохиргоог SPA руу шилжүүлдэг. Хэрэв та ямар нэг зүйлийг өөрчлөх шаардлагатай бол програмыг дахин бүтээх хэрэгтэй болно. Би хүсэхгүй байна. Би програмыг CI шатанд нэг удаа бүтээж, CD-ийн үе шатанд орчны хувьсагчдыг ашиглан шаардлагатай хэмжээгээр тохируулахыг хүсч байна.

Мэдээжийн хэрэг, статик файлууд өөрсдөө ямар ч орчны хувьсагчийг ойлгодоггүй. Тиймээс та заль мэх ашиглах хэрэгтэй болно. Төгсгөлийн зураг дээр би nginx-г ажиллуулахгүй, харин орчны хувьсагчдыг уншиж, статик файлд бичиж, шахаж, дараа нь nginx руу удирдлагыг шилжүүлэх тусгай бүрхүүлийн скриптийг ажиллуулах болно.

Энэ зорилгоор Dockerfile нь ENTRYPOINT параметрийг өгдөг. Түүнд дараах скриптийг өгье (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 "$@"

Скрипт нь үүргээ гүйцэтгэхийн тулд тохиргоог js файлд дараах хэлбэрээр бичсэн байх ёстой. ${API_URL}.

Ихэнх орчин үеийн ДЦГ-ууд барилга барихдаа файлдаа хэш нэмдэг гэдгийг тэмдэглэх нь зүйтэй. Хөтөч нь файлыг удаан хугацаанд найдвартай кэшлэхийн тулд энэ нь зайлшгүй шаардлагатай. Хэрэв файл өөрчлөгдвөл түүний хэш өөрчлөгдөх бөгөөд энэ нь хөтчийг файлыг дахин татаж авахад хүргэдэг.

Харамсалтай нь, миний аргаар тохиргоог орчны хувьсагчаар өөрчлөх нь файлын хэшийг өөрчлөхөд хүргэдэггүй бөгөөд энэ нь хөтчийн кэшийг өөр аргаар хүчингүй болгох ёстой гэсэн үг юм. Янз бүрийн тохиргоог өөр өөр орчинд байрлуулсан тул надад ийм асуудал байхгүй.

Эцсийн зургийг нэгтгэж байна

Эцэст нь.

Докер файл

# Первый базовый образ для сборки
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;"]

Одоо үүссэн зургийг хаана ч угсарч ашиглаж болно.

Эх сурвалж: www.habr.com