用於分發單頁應用程式的 Docker 映像

單頁應用程式 (SPA) 是一組靜態 JavaScript 和 HTML 文件,以及圖像和其他資源。 因為它們不會動態變化,所以在線發布它們非常容易。 為此有大量廉價甚至免費的服務,從簡單的 GitHub Pages(有些甚至是 narod.ru)到像 Amazon S3 這樣的 CDN。 然而,我還需要別的東西。

我需要一個帶有 SPA 的 Docker 映像,以便它可以作為 Kubernetes 叢集的一部分在生產中輕鬆啟動,也可以在不知道 SPA 是什麼的後端開發人員的機器上啟動。

我為自己確定了以下形像要求:

  • 易於使用(但不易於組裝);
  • 磁碟和 RAM 的最小大小;
  • 透過環境變數進行配置,使得鏡像可以在不同的環境下使用;
  • 最有效的文件分發。

今天我會告訴你如何:

  • 腸道 nginx;
  • 從原始碼建構 brotli;
  • 教導靜態文件理解環境變數;
  • 當然,還有如何從這一切組裝一個 Docker 映像。

本文的目的是分享我的經驗並激發經驗豐富的社群成員提出建設性的批評。

建構裝配影像

為了讓最終的 Docker 映像體積更小,您需要遵守兩個規則:最少的層數和簡約的基礎映像。 最小的基礎鏡像之一是 Alpine Linux 鏡像,所以我會選擇它。 有些人可能會認為 Alpine 不適合生產,他們可能是對的。 但就我個人而言,我和他之間從來沒有任何問題,也沒有人反對他。

為了減少層數,我將分兩個階段組裝影像。 第一個是草稿;所有輔助實用程式和臨時文件都將保留在其中。 在最後階段我只會寫下應用程式的最終版本。

讓我們從輔助圖像開始。

為了編譯 SPA 應用程序,您通常需要 node.js。 我將使用官方映像,該映像還附帶 npm 和 Yarn 套件管理器。 我將代表我自己添加建造一些 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的

您可以使用任何 Web 伺服器來分發靜態內容。 我習慣使用 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 MB。 最初的 nginx 佔用了 19,7 MB。 我的運動興趣得到了滿足。

教授靜態知識以了解環境變數

為什麼 SPA 中需要設定? 例如,為了指定使用哪個 RESTful API。 通常,所需環境的設定會在建置階段傳輸到 SPA。 如果您需要更改某些內容,則必須重建應用程式。 我不想要這個。 我希望應用程式在 CI 階段建置一次,並在 CD 階段使用環境變數進行盡可能多的配置。

當然,靜態文件本身不理解任何環境變數。 因此,你必須使用一個技巧。 在最終映像中,我不會啟動 nginx,而是啟動一個特殊的 shell 腳本,該腳本將讀取環境變量,將它們寫入靜態文件,壓縮它們,然後將控制權轉移給 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