تصویر داکر برای توزیع برنامه تک صفحه

برنامه تک صفحه ای (SPA) مجموعه ای از فایل های جاوا اسکریپت و HTML ایستا و همچنین تصاویر و منابع دیگر است. از آنجا که آنها به صورت پویا تغییر نمی کنند، انتشار آنلاین آنها بسیار آسان است. تعداد زیادی خدمات ارزان و حتی رایگان برای این کار وجود دارد، که با صفحات ساده GitHub (و برای برخی حتی با narod.ru) شروع می شود و با CDN مانند Amazon S3 ختم می شود. با این حال، من به چیز دیگری نیاز داشتم.

من به یک تصویر Docker با SPA نیاز داشتم تا بتوان آن را به راحتی هم در مرحله تولید به عنوان بخشی از یک خوشه Kubernetes و هم در دستگاه یک توسعه‌دهنده بک‌اند که اصلاً نمی‌داند SPA چیست راه‌اندازی کرد.

من شرایط تصویر زیر را برای خودم تعیین کرده ام:

  • سهولت استفاده (اما نه مونتاژ)؛
  • حداقل اندازه از نظر دیسک و رم.
  • پیکربندی از طریق متغیرهای محیطی به طوری که تصویر می تواند در محیط های مختلف استفاده شود.
  • کارآمدترین توزیع فایل ها

امروز به شما خواهم گفت که چگونه:

  • nginx روده;
  • ساخت بروتلی از منابع؛
  • آموزش فایل های استاتیک برای درک متغیرهای محیط.
  • و البته نحوه جمع آوری یک تصویر داکر از همه اینها.

هدف این مقاله به اشتراک گذاشتن تجربیاتم و تحریک اعضای مجرب جامعه به انتقاد سازنده است.

ساخت تصویر برای مونتاژ

برای کوچک کردن تصویر نهایی Docker، باید دو قانون را رعایت کنید: حداقل لایه ها و یک تصویر پایه حداقلی. یکی از کوچکترین تصاویر پایه، تصویر آلپاین لینوکس است، بنابراین این چیزی است که من انتخاب خواهم کرد. برخی ممکن است استدلال کنند که Alpine برای تولید مناسب نیست و ممکن است حق با آنها باشد. اما شخصاً هیچ وقت با او مشکلی نداشته ام و هیچ استدلالی علیه او وجود ندارد.

برای داشتن لایه های کمتر، تصویر را در 2 مرحله مونتاژ می کنم. اولی یک پیش نویس است؛ تمام ابزارهای کمکی و فایل های موقت در آن باقی خواهند ماند. و در مرحله آخر فقط نسخه نهایی برنامه را یادداشت می کنم.

بیایید با تصویر کمکی شروع کنیم.

برای کامپایل یک برنامه SPA معمولاً به node.js نیاز دارید. من تصویر رسمی را می‌گیرم، که با مدیریت بسته‌های npm و نخ نیز همراه است. از طرف خودم، node-gyp را که برای ساختن برخی از بسته های npm لازم است، و کمپرسور 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 دارای یک تصویر رسمی 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 است که من با آن به پایان رسیدم. نظرات به زبان روسی مال من است، به زبان انگلیسی - از اصل.

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 هدایت کنید، این برای پیمایش در 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";
            }
        }
    }
}

می توانید تصویر تمام شده را از اینجا دانلود کنید: https://hub.docker.com/r/alexxxnf/nginx-spa. 10,5 مگابایت طول می کشد. nginx اصلی 19,7 مگابایت حجم داشت. علاقه ورزشی من ارضا شده است.

آموزش استاتیک برای درک متغیرهای محیطی

چرا ممکن است تنظیمات در SPA مورد نیاز باشد؟ به عنوان مثال، برای اینکه مشخص کنید از کدام API RESTful استفاده کنید. به طور معمول، تنظیمات محیط مورد نظر در مرحله ساخت به 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}.

شایان ذکر است که اکثر SPA های مدرن هنگام ساختن، هش را به فایل های خود اضافه می کنند. این لازم است تا مرورگر بتواند با خیال راحت فایل را برای مدت طولانی ذخیره کند. اگر فایل تغییر کند، هش آن تغییر می کند، که به نوبه خود مرورگر را مجبور می کند تا فایل را دوباره دانلود کند.

متأسفانه در روش من، تغییر پیکربندی از طریق متغیرهای محیطی منجر به تغییر در هش فایل نمی شود، به این معنی که کش مرورگر باید به روش دیگری باطل شود. من این مشکل را ندارم زیرا تنظیمات مختلف در محیط های مختلف مستقر هستند.

کنار هم قرار دادن تصویر نهایی

سرانجام.

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

اکنون تصویر به دست آمده را می توان مونتاژ کرد و در هر مکانی استفاده کرد.

منبع: www.habr.com