Yagona sahifali ilovani tarqatish uchun Docker tasviri

Yagona sahifali dastur (SPA) bu statik JavaScript va HTML fayllari, shuningdek, tasvirlar va boshqa resurslar toΚ»plamidir. Ular dinamik ravishda o'zgarmasligi sababli ularni onlayn nashr qilish juda oson. Buning uchun oddiy GitHub sahifalaridan boshlab (ba'zilar uchun narod.ru bilan) va Amazon S3 kabi CDN bilan tugaydigan ko'plab arzon va hatto bepul xizmatlar mavjud. Biroq, menga boshqa narsa kerak edi.

Menga SPA bilan Docker tasviri kerak edi, shunda uni ishlab chiqarishda ham Kubernetes klasterining bir qismi sifatida, ham SPA nima ekanligini bilmaydigan orqa dasturchining mashinasida osongina ishga tushirish mumkin edi.

Men o'zim uchun quyidagi rasm talablarini aniqladim:

  • foydalanish qulayligi (lekin yig'ish emas);
  • disk va RAM jihatidan ham minimal o'lcham;
  • tasvirni turli muhitlarda ishlatish uchun muhit o'zgaruvchilari orqali konfiguratsiya;
  • fayllarni eng samarali taqsimlash.

Bugun men sizga qanday qilib aytaman:

  • ichak nginx;
  • manbalardan brotli qurish;
  • statik fayllarni muhit o'zgaruvchilarini tushunishga o'rgatish;
  • va, albatta, bularning barchasidan Docker tasvirini qanday yig'ish kerak.

Ushbu maqolaning maqsadi o'z tajribam bilan bo'lishish va tajribali jamoa a'zolarini konstruktiv tanqidga undashdir.

Yig'ish uchun rasm yaratish

Yakuniy Docker tasvirini kichik hajmda qilish uchun siz ikkita qoidaga rioya qilishingiz kerak: minimal qatlamlar va minimalist asosiy tasvir. Eng kichik asosiy tasvirlardan biri Alpine Linux tasviridir, shuning uchun men buni tanlayman. Ba'zilar Alp tog'larini ishlab chiqarish uchun mos emas deb ta'kidlashi mumkin va ular to'g'ri bo'lishi mumkin. Lekin shaxsan men u bilan hech qachon muammoga duch kelmaganman va unga qarshi argumentlar ham yo'q.

Qatlamlar kamroq bo'lishi uchun men tasvirni 2 bosqichda yig'aman. Birinchisi, qoralama, unda barcha yordamchi dasturlar va vaqtinchalik fayllar qoladi. Va oxirgi bosqichda men faqat dasturning yakuniy versiyasini yozaman.

Keling, yordamchi tasvirdan boshlaylik.

SPA ilovasini kompilyatsiya qilish uchun sizga odatda node.js kerak bo'ladi. Men rasmiy tasvirni olaman, u ham npm va ip paketi menejerlari bilan birga keladi. O'z nomimdan ba'zi npm paketlarini yaratish uchun zarur bo'lgan node-gyp va Google'dan Brotli kompressorini qo'shaman, bu biz uchun keyinchalik foydali bo'ladi.

Izohlar bilan 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

Bu erda men minimalizm uchun kurashaman, shuning uchun tasvirni bitta katta jamoa birlashtiradi.

Tayyor rasmni bu yerda topishingiz mumkin: https://hub.docker.com/r/alexxxnf/spa-builder. Garchi men boshqa odamlarning rasmlariga tayanmaslikni va o'zingiznikini to'plashni tavsiya etmayman.

nginx

Statik tarkibni tarqatish uchun istalgan veb-serverdan foydalanishingiz mumkin. Men nginx bilan ishlashga o'rganib qolganman, shuning uchun uni hozir ishlataman.

Nginx rasmiy Docker tasviriga ega, ammo u oddiy statik tarqatish uchun juda ko'p modullarga ega. Qaysi biri etkazib berishga kiritilganligini maxsus guruh yoki rasmiy Dockerfile-da ko'rish mumkin.

$ 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

Men Dockerfile-dan asos sifatida foydalanaman, lekin unda faqat statik tarkibni tarqatish uchun zarur bo'lgan narsalarni qoldiraman. Mening versiyam HTTPS orqali ishlay olmaydi, avtorizatsiyani qo'llab-quvvatlamaydi va boshqalar. Ammo mening versiyam gzip-ga qaraganda bir oz samaraliroq bo'lgan Brotli algoritmi bilan siqilgan fayllarni tarqatishi mumkin. Biz fayllarni bir marta siqib chiqaramiz, buni tezda qilishning hojati yo'q.

Bu men tugatgan Dockerfile. Rus tilidagi sharhlar meniki, ingliz tilida - asl nusxadan.

Docker fayli

# Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ ΠΎΠ±Ρ€Π°Π· снова 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;"]

Men darhol nginx.conf ni tuzataman, shunda gzip va brotli sukut bo'yicha yoqilgan bo'ladi. Shuningdek, men keshlash sarlavhalarini ham qo'shaman, chunki biz hech qachon statikni o'zgartirmaymiz. Va oxirgi teginish barcha 404 so'rovni index.html ga yo'naltirish bo'ladi, bu SPAda navigatsiya qilish uchun zarur.

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

Tayyor rasmni bu yerdan yuklab olishingiz mumkin: https://hub.docker.com/r/alexxxnf/nginx-spa. 10,5 MB joy oladi. Asl nginx 19,7 MB joy egalladi. Mening sportga bo'lgan qiziqishim qondirildi.

Atrof-muhit o'zgaruvchilarini tushunish uchun statikani o'rgatish

Nima uchun SPA-da sozlamalar kerak bo'lishi mumkin? Masalan, qaysi RESTful API ishlatishni belgilash uchun. Odatda, kerakli muhit uchun sozlamalar qurish bosqichida SPA-ga o'tkaziladi. Agar biror narsani o'zgartirish kerak bo'lsa, dasturni qayta tiklashingiz kerak bo'ladi. Men buni xohlamayman. Men dasturni CI bosqichida bir marta qurishni va atrof-muhit o'zgaruvchilari yordamida CD bosqichida kerak bo'lganda sozlashni xohlayman.

Albatta, statik fayllarning o'zi hech qanday muhit o'zgaruvchilarini tushunmaydi. Shuning uchun, siz hiyla ishlatishingiz kerak bo'ladi. Yakuniy rasmda men nginx-ni ishga tushirmayman, balki atrof-muhit o'zgaruvchilarini o'qiydi, ularni statik fayllarga yozadi, siqadi va shundan keyingina boshqaruvni nginx-ga o'tkazadigan maxsus qobiq skriptini ishga tushiraman.

Buning uchun Dockerfile ENTRYPOINT parametrini taqdim etadi. Keling, unga quyidagi skriptni beraylik (misol sifatida Angular-dan foydalanib):

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

Skript o'z vazifasini bajarishi uchun sozlamalar JS fayllarida quyidagi shaklda yozilishi kerak: ${API_URL}.

Shuni ta'kidlash kerakki, zamonaviy SPAlarning ko'pchiligi qurishda o'z fayllariga xeshlarni qo'shadi. Bu brauzer faylni uzoq vaqt xavfsiz tarzda keshlashi uchun zarur. Agar fayl o'zgarmasa, uning xeshi o'zgaradi, bu esa o'z navbatida brauzerni faylni qayta yuklab olishga majbur qiladi.

Afsuski, mening usulimda, muhit o'zgaruvchilari orqali konfiguratsiyani o'zgartirish fayl xeshini o'zgartirishga olib kelmaydi, ya'ni brauzer keshini boshqa yo'l bilan bekor qilish kerak. Menda bu muammo yo'q, chunki turli xil konfiguratsiyalar turli muhitlarda joylashtirilgan.

Yakuniy rasmni birlashtirish

Nihoyat.

Docker fayli

# ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ Π±Π°Π·ΠΎΠ²Ρ‹ΠΉ ΠΎΠ±Ρ€Π°Π· для сборки
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;"]

Endi olingan tasvirni yig'ish va istalgan joyda ishlatish mumkin.

Manba: www.habr.com