Turborepo

Docker

建置 Docker 映像檔是部署各種應用程式的常見方式。但是,從單一程式碼庫執行此操作會遇到一些挑戰。

注意事項

本指南假設您正在使用 create-turbo 或具有類似結構的程式碼庫。

問題

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

讓我們想像一下,您有一個類似這樣的單一程式碼庫

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 對於單一程式碼庫來說是全域的。這表示如果我們在 apps/web 內安裝一個新的套件,就會導致 apps/api 重新部署

在大型單一程式碼庫中,這可能會導致大量時間損失,因為對單一程式碼庫鎖定檔的任何變更都會串聯到數十或數百個部署。

解決方案

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

終端機
turbo prune api --docker

執行此命令會在 ./out 目錄內建立一個經過修剪的單一程式碼庫版本。它只包含 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,為方便起見複製過來。

從單一程式碼庫的根目錄建置 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
 
# 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

小時

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

本頁內容