Larawan ng Docker para sa pamamahagi ng Single Page Application

Ang Single-page Application (SPA) ay isang set ng mga static na JavaScript at HTML file, pati na rin ang mga larawan at iba pang mapagkukunan. Dahil hindi sila nagbabago nang pabago-bago, ang pag-publish ng mga ito online ay napakadali. Mayroong isang malaking bilang ng mga mura at kahit na mga libreng serbisyo para dito, simula sa isang simpleng GitHub Pages (at para sa ilan kahit na sa narod.ru) at nagtatapos sa isang CDN tulad ng Amazon S3. Gayunpaman, kailangan ko ng ibang bagay.

Kailangan ko ng Docker na imahe na may SPA para madali itong mailunsad kapwa sa produksyon bilang bahagi ng isang Kubernetes cluster, at sa makina ng isang back-end na developer na walang ideya kung ano ang SPA.

Natukoy ko ang sumusunod na mga kinakailangan sa larawan para sa aking sarili:

  • kadalian ng paggamit (ngunit hindi pagpupulong);
  • minimum na laki pareho sa mga tuntunin ng disk at RAM;
  • pagsasaayos sa pamamagitan ng mga variable ng kapaligiran upang ang imahe ay magamit sa iba't ibang mga kapaligiran;
  • ang pinaka mahusay na pamamahagi ng mga file.

Ngayon sasabihin ko sa iyo kung paano:

  • gat nginx;
  • bumuo ng brotli mula sa mga mapagkukunan;
  • turuan ang mga static na file upang maunawaan ang mga variable ng kapaligiran;
  • at siyempre kung paano mag-ipon ng isang imahe ng Docker mula sa lahat ng ito.

Ang layunin ng artikulong ito ay ibahagi ang aking karanasan at pukawin ang mga may karanasang miyembro ng komunidad sa nakabubuo na pagpuna.

Pagbuo ng isang imahe para sa pagpupulong

Upang gawing maliit ang laki ng panghuling larawan ng Docker, kailangan mong sumunod sa dalawang panuntunan: isang minimum na layer at isang minimalistic na base na imahe. Ang isa sa pinakamaliit na base na imahe ay ang Alpine Linux image, kaya iyon ang pipiliin ko. Ang ilan ay maaaring magtaltalan na ang Alpine ay hindi angkop para sa produksyon, at maaaring sila ay tama. Pero sa personal, wala akong naging problema sa kanya at walang mga argumento laban sa kanya.

Upang magkaroon ng mas kaunting mga layer, tipunin ko ang imahe sa 2 yugto. Ang una ay isang draft; lahat ng auxiliary utilities at pansamantalang mga file ay mananatili dito. At sa huling yugto ay isusulat ko lamang ang huling bersyon ng aplikasyon.

Magsimula tayo sa auxiliary na imahe.

Upang mag-compile ng isang SPA application, karaniwan mong kailangan ang node.js. Kukunin ko ang opisyal na imahe, na kasama rin ng mga manager ng npm at yarn package. Sa aking sariling ngalan, magdaragdag ako ng node-gyp, na kinakailangan upang bumuo ng ilang npm package, at ang Brotli compressor mula sa Google, na magiging kapaki-pakinabang sa amin sa ibang pagkakataon.

Dockerfile na may mga komento.

# Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ ΠΎΠ±Ρ€Π°Π·
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

Narito na ako ay nakikipaglaban para sa minimalism, kaya ang imahe ay pinagsama ng isang malaking koponan.

Ang natapos na imahe ay matatagpuan dito: https://hub.docker.com/r/alexxxnf/spa-builder. Bagama't inirerekumenda kong huwag umasa sa mga larawan ng ibang tao at mangolekta ng iyong sarili.

nginx

Maaari mong gamitin ang anumang web server upang ipamahagi ang static na nilalaman. Nakasanayan ko nang magtrabaho sa nginx, kaya gagamitin ko ito ngayon.

Ang Nginx ay may opisyal na imahe ng Docker, ngunit mayroon itong napakaraming mga module para sa simpleng static na pamamahagi. Alin ang mga kasama sa paghahatid ay makikita ng isang espesyal na koponan o sa opisyal na 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

Gagamitin ko ang Dockerfile bilang batayan, ngunit iiwan ko lamang dito ang kailangan upang maipamahagi ang static na nilalaman. Hindi gagana ang aking bersyon sa HTTPS, hindi susuportahan ang pahintulot, at marami pang iba. Ngunit ang aking bersyon ay makakapagbahagi ng mga file na naka-compress gamit ang Brotli algorithm, na bahagyang mas mahusay kaysa sa gzip. I-compress namin ang mga file nang isang beses; hindi na kailangang gawin ito sa mabilisang.

Ito ang Dockerfile na natapos ko. Ang mga komento sa Russian ay akin, sa Ingles - mula sa orihinal.

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

Aayusin ko kaagad ang nginx.conf para ma-enable ang gzip at brotli bilang default. Isasama ko rin ang mga header ng pag-cache, dahil hinding-hindi tayo magbabago ng static. At ang huling ugnayan ay ang pag-redirect ng lahat ng 404 na kahilingan sa index.html, ito ay kinakailangan para sa pag-navigate sa 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";
            }
        }
    }
}

Maaari mong i-download ang natapos na larawan dito: https://hub.docker.com/r/alexxxnf/nginx-spa. Tumatagal ito ng 10,5 MB. Ang orihinal na nginx ay umabot ng 19,7 MB. Ang aking interes sa palakasan ay nasiyahan.

Pagtuturo ng statics upang maunawaan ang mga variable ng kapaligiran

Bakit maaaring kailanganin ang mga setting sa SPA? Halimbawa, para matukoy kung aling RESTful API ang gagamitin. Karaniwan, ang mga setting para sa gustong kapaligiran ay inililipat sa SPA sa yugto ng pagbuo. Kung kailangan mong baguhin ang isang bagay, kakailanganin mong muling buuin ang application. Di ko ito gusto. Gusto kong mabuo ang application nang isang beses sa yugto ng CI, at i-configure hangga't kinakailangan sa yugto ng CD gamit ang mga variable ng kapaligiran.

Siyempre, ang mga static na file mismo ay hindi nauunawaan ang anumang mga variable ng kapaligiran. Samakatuwid, kailangan mong gumamit ng isang trick. Sa huling imahe, hindi ako maglulunsad ng nginx, ngunit isang espesyal na script ng shell na magbabasa ng mga variable ng kapaligiran, isulat ang mga ito sa mga static na file, i-compress ang mga ito, at pagkatapos ay ilipat ang kontrol sa nginx.

Para sa layuning ito, ang Dockerfile ay nagbibigay ng ENTRYPOINT parameter. Bigyan natin siya ng sumusunod na script (gamit ang Angular bilang isang halimbawa):

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

Upang magawa ng script ang trabaho nito, ang mga setting ay dapat na nakasulat sa mga js file sa form na ito: ${API_URL}.

Kapansin-pansin na karamihan sa mga modernong SPA ay nagdaragdag ng mga hash sa kanilang mga file kapag bumubuo. Ito ay kinakailangan upang ligtas na mai-cache ng browser ang file sa loob ng mahabang panahon. Kung magbabago ang file, magbabago ang hash nito, na pipilitin naman ang browser na i-download muli ang file.

Sa kasamaang palad, sa aking pamamaraan, ang pagbabago ng pagsasaayos sa pamamagitan ng mga variable ng kapaligiran ay hindi humahantong sa isang pagbabago sa hash ng file, na nangangahulugan na ang cache ng browser ay dapat na walang bisa sa ibang paraan. Wala akong problemang ito dahil iba't ibang configuration ang naka-deploy sa iba't ibang environment.

Pagtitipon ng pangwakas na imahe

Sa wakas.

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

Ngayon ang nagresultang imahe ay maaaring tipunin at magamit kahit saan.

Pinagmulan: www.habr.com