Turborepo logo

Docker

建置 Docker 鏡像是部署各種應用程式的常見方式。然而,從 Monorepo 執行此操作會帶來一些挑戰。

须知:

本指南假設您正在使用 create-turbo 或具有相似結構的儲存庫。

問題

在 Monorepo 中,不相關的變更可能會在部署應用程式時,導致 Docker 執行不必要的工作。

假設您有一個如下所示的 Monorepo

server.js
package.json
package.json
package.json
package-lock.json

您想要使用 Docker 部署 apps/api,因此您建立一個 Dockerfile

./apps/api/Dockerfile
FROM node:16
 
WORKDIR /usr/src/app
 
# Copy root package.json and lockfile
COPY package.json ./
COPY package-lock.json ./
 
# Copy the api package.json
COPY apps/api/package.json ./apps/api/package.json
 
RUN npm install
 
# Copy app source
COPY . .
 
EXPOSE 8080
 
CMD [ "node", "apps/api/server.js" ]

這會將根目錄的 package.json 和根鎖定檔複製到 Docker 映像檔。然後,它會安裝依賴項,複製應用程式原始碼並啟動應用程式。

您也應該建立一個 .dockerignore 檔案,以防止 node_modules 與應用程式原始碼一起複製進來。

.dockerignore
node_modules
npm-debug.log

鎖定檔變更頻繁

Docker 在部署您的應用程式方面非常聰明。就像 Turborepo 一樣,它會盡可能減少工作量。

在我們的 Dockerfile 案例中,只有當其映像檔中的檔案與上次執行不同時,它才會執行 npm install。否則,它會還原之前擁有的 node_modules 目錄。

這表示每當 package.jsonapps/api/package.jsonpackage-lock.json 變更時,Docker 映像檔都會執行 npm install

這聽起來很棒 - 直到我們意識到一件事。`package-lock.json` 對於 monorepo 而言是全域的。這表示如果我們在 apps/web 內部安裝一個新套件,我們將導致 apps/api 重新部署。

在大型 Monorepo 中,這可能會導致大量時間損失,因為對 Monorepo 鎖定檔的任何變更都會連鎖反應到數十甚至數百次的部署。

解決方案

解決方案是修剪 Dockerfile 的輸入,使其僅包含絕對必要的內容。Turborepo 提供了一個簡單的解決方案 - turbo prune

終端機
turbo prune api --docker

執行此命令會在 `./out` 目錄中建立 Monorepo 的修剪版本。它僅包含 api 依賴的工作區。它還會修剪鎖定檔,以便僅下載相關的 node_modules

--docker 旗標

預設情況下,`turbo prune` 會將所有相關檔案放入 `./out` 中。但為了最佳化 Docker 的快取,我們理想情況下希望分兩個階段複製檔案。

首先,我們只想複製安裝套件所需的內容。執行 `--docker` 時,您會在 `./out/json` 內部找到它。

package.json
package.json
server.js
package.json
package.json
turbo.json
package-lock.json

之後,您可以複製 `./out/full` 中的檔案以新增原始碼檔案。

以這種方式分隔依賴項和原始碼檔案,讓我們僅在依賴項變更時才執行 npm install - 從而大幅提升速度。

如果沒有 `--docker`,所有修剪後的檔案都會放置在 `./out` 內部。

範例

我們詳細的 with-docker 範例深入探討了如何充分利用 prune。這是 Dockerfile,為了方便起見複製過來。

從 Monorepo 的根目錄建置 Dockerfile

終端機
docker build -f apps/web/Dockerfile .

此 Dockerfile 是為使用 standalone 輸出模式的 Next.js 應用程式編寫的。

./apps/api/Dockerfile
FROM node:18-alpine AS base
 
FROM base AS builder
RUN apk update
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
# Replace <your-major-version> with the major version installed in your repository. For example:
# RUN yarn global add turbo@^2
RUN yarn global add turbo@^<your-major-version>
COPY . .
 
# Generate a partial monorepo with a pruned lockfile for a target workspace.
# Assuming "web" is the name entered in the project's package.json: { name: "web" }
RUN turbo prune web --docker
 
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk update
RUN apk add --no-cache libc6-compat
WORKDIR /app
 
# First install the dependencies (as they change less often)
COPY --from=builder /app/out/json/ .
RUN yarn install --frozen-lockfile
 
# Build the project
COPY --from=builder /app/out/full/ .
RUN yarn turbo run build
 
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
 
# 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 之前立即從建置引數中引入一些環境變數

./apps/api/Dockerfile
ARG TURBO_TEAM
ENV TURBO_TEAM=$TURBO_TEAM
 
ARG TURBO_TOKEN
ENV TURBO_TOKEN=$TURBO_TOKEN
 
RUN yarn turbo run build

turbo 現在將能夠命中您的遠端快取。若要查看非快取 Docker 建置映像檔的 Turborepo 快取命中,請從您的專案根目錄執行類似於以下的命令

終端機
docker build -f apps/web/Dockerfile . --build-arg TURBO_TEAM=“your-team-name” --build-arg TURBO_TOKEN=“your-token“ --no-cache

小時

總計節省的運算時間
開始使用
遠端快取 →

本頁內容