使用 Docker 部署
建置 Docker(在新分頁中開啟) 映像檔是部署各種應用程式的常見方式。不過,從單一儲存庫執行此操作會遇到幾個挑戰。
問題
重點摘要:在單一儲存庫中,不相關的變更會讓 Docker 在部署應用程式時執行不必要的作業。
想像一下,您有一個如下所示的單一儲存庫
├── apps
│ ├── docs
│ │ ├── server.js
│ │ └── package.json
│ └── web
│ └── package.json
├── package.json
└── package-lock.json
您想使用 Docker 部署 apps/docs
,因此您建立一個 Dockerfile
FROM node:16
WORKDIR /usr/src/app
# Copy root package.json and lockfile
COPY package.json ./
COPY package-lock.json ./
# Copy the docs package.json
COPY apps/docs/package.json ./apps/docs/package.json
RUN npm install
# Copy app source
COPY . .
EXPOSE 8080
CMD [ "node", "apps/docs/server.js" ]
這會將根目錄 package.json
和根目錄鎖定檔複製到 Docker 映像檔。然後,它會安裝相依性、複製應用程式原始碼並啟動應用程式。
您也應該建立一個 .dockerignore
檔案,以防止 node_modules 與應用程式原始碼一起複製。
node_modules
npm-debug.log
鎖定檔變更太頻繁
Docker 在部署應用程式的方式上相當聰明。就像 Turbo 一樣,它會盡量 減少作業(在新分頁中開啟)。
就我們的 Dockerfile 而言,它只會在映像檔中的檔案與前一次執行不同時執行 npm install
。如果不是,它會還原之前已有的 node_modules
目錄。
這表示每當 package.json
、apps/docs/package.json
或 package-lock.json
變更時,Docker 映像檔都會執行 npm install
。
這聽起來很棒,直到我們意識到某件事。package-lock.json
對單一儲存庫而言是全域性的。這表示如果我們在 apps/web
中安裝新的套件,我們將導致 apps/docs
重新部署。
在大型單一儲存庫中,這可能會造成大量時間浪費,因為對單一儲存庫鎖定檔案的任何變更都會導致數十或數百次的部署。
解決方案
解決方案是將 Dockerfile 的輸入精簡到僅有絕對必要的內容。Turborepo 提供了一個簡單的解決方案 - turbo prune
。
turbo prune docs --docker
執行此指令會在 ./out
目錄中建立單一儲存庫的精簡版本。它只包含 docs
所依賴的工作區。
至關重要的是,它也精簡鎖定檔案,以便只會下載相關的 node_modules
。
--docker
旗標
預設情況下,turbo prune
會將所有相關檔案放入 ./out
中。但為了最佳化 Docker 快取,我們理想上希望分兩個階段複製檔案。
首先,我們只想複製安裝套件所需的內容。執行 --docker
時,您會在 ./out/json
中找到此內容。
out
├── json
│ ├── apps
│ │ └── docs
│ │ └── package.json
│ └── package.json
├── full
│ ├── apps
│ │ └── docs
│ │ ├── server.js
│ │ └── package.json
│ ├── package.json
│ └── turbo.json
└── package-lock.json
之後,您可以複製 ./out/full
中的檔案來新增原始檔。
以這種方式將相依性和原始檔分開,讓我們只有在相依性變更時才執行 npm install
,這讓我們大幅提升速度。
在沒有 --docker
的情況下,所有已裁剪的檔案都會放置在 ./out
中。
範例
我們詳細的 with-docker
範例(在新分頁中開啟) 深入探討如何充分利用 prune
。以下是 Dockerfile,複製過來以供參考。
這個 Dockerfile 是為使用 standalone
輸出模式(在新分頁開啟) 的 Next.js(在新分頁開啟) 應用程式所撰寫。
FROM node:18-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune web --docker
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
RUN yarn turbo run build --filter=web...
FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --from=installer /app/apps/web/next.config.js .
COPY --from=installer /app/apps/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.dev.org.tw/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
CMD node apps/web/server.js
遠端快取
要在 Docker 建置期間利用遠端快取,您需要確保您的建置容器有憑證存取您的遠端快取。
有許多方法可以在 Docker 映像中處理機密。我們將在此使用一個簡單的策略,使用多階段建置,將機密作為建置參數,而這些參數會對最終映像隱藏起來。
假設您使用類似於上面那個 Dockerfile,我們將在 turbo build
之前,從建置參數中引入一些環境變數。
ARG TURBO_TEAM
ENV TURBO_TEAM=$TURBO_TEAM
ARG TURBO_TOKEN
ENV TURBO_TOKEN=$TURBO_TOKEN
RUN yarn turbo run build --filter=web...
turbo
現在可以存取您的遠端快取。若要查看 TurboRepo 快取命中非快取 Docker 建置映像,請從您的專案根目錄執行類似下列指令的指令
docker build -f apps/web/Dockerfile . --build-arg TURBO_TEAM=“your-team-name” --build-arg TURBO_TOKEN=“your-token“ --no-cache