Wen Chen
Published on

在 Nx-NextJS 專案中透過 output:standalone 降低 docker image size

Authors
  • avatar
    Name
    Wen Chen

前言

在寫這篇文章時,光標題就想了很久,主要是想做個紀錄,可能有機會剛好幫到使用相同技術組合的人。

這陣子專案開始在做打包上線,工作就漸漸從開發轉變至成品的優化。

先提供一下目前專案使用的技術組合:

  1. NextJs : 12.3.0
  2. @nrwl/next : 14.5.4
  3. nx : 14.5.4

因為這個專案最終上線是由我們完成 dockerFile 的撰寫與打包,最終再將 Image 交給架構去 deploy,但當我們參考一些範例完成初版本的 image 後, 便發現檔案大小似乎意外肥大,在本機打包出來動輒 1GB,因為 monorepo 的關係,過程中也不斷去確認是否有複製到其他專案的資料,反覆修正後似乎就是這麼大。

經過一番搜尋,我將目光放到了:Output File Tracing

關於 Output File Tracing,除了官方文件以外,網上也有不少中文資源可以翻閱,這裡就不加贅述。

總之,看起來可以大幅減少 image 的體積,那就來試試吧!

補充

啟用 output file tracing 後 build 完會在匯出的資料夾內找到一個 .next/standalone 資料夾,透過運行裡面的 server.js 即可取代 next start

在進行 image 打包時,只需要打包 standalone 資料夾即可,不需要和過去一樣打包所有 build 完的檔案與 node_module

public.next/static 資料夾預設不會被包進 standalone 內,如果沒有額外透過 CDN 處理的話,可以手動複製進去資料夾內。


實作

首先先照著官方範例在專案的 next.config.js 啟用該功能:

// next.config.js
module.exports = {
  output: 'standalone'
}

在正常有支援 output 的版本下,加入上面那行應該就會運作了,不過在 monorepo 架構下,根據官方提供的文件, 我們需要再加入一行設定:

// next.config.js
module.exports = {
  experimental: {
    // this includes files from the monorepo base two directories up
    outputFileTracingRoot: path.join(__dirname, '../../'),
  }
}

如此一來才有辦法將專案資料夾上層目錄納入處理。加入上述設定後可以再測試 buildstart 是否可以成功運行,如果可以,請直接跳到最後結語。

給執行到上一步還無法成功建置的人:

沒錯,我就是照著官方作法做完還沒辦法成功建置的人,經過一番搜尋,找到了一則 Github issue,照做之後居然成功執行了。

  • 完成前兩項操作後,到專案 pages 底下的任何一隻檔案( 我加在index.tsx )
// pages/index.tsx
import path from 'path';
path.resolve('./next.config.js');

從他本身的評論看起來,作者好像也不知道為何可以運作...翻閱其他官方文件似乎也沒有找到相關的說明,不過底下其他評論看起來,似乎有許多人靠上面的操作使 standalone 成功運作起來。

因為當初確實是專案跑不起來才去找討論區的解法來做嘗試,但在寫這篇文章的當下我有刻意回去把上面那兩行扣刪掉,但卻無法在復現該錯誤,這部分再麻煩有其他知道原理的大神替我解惑了QQ

該則 Issue 底下最新的留言有提到 nx : 16.3.0 已對該錯誤做修正,如果是使用較新版本 NX 的朋友可能就不會遇到此問題了。

結語

最終設定完打包出來的 Docker Image 從原先的 1GB 減少到 161 MB 左右。

如果近期也有使用 NextJS 且需要打包成 docker image 的人不妨可以試試看,瘦身效果非常明顯!

底下附上更新過後的 dockerFile 內容:

FROM node:16.20.0-alpine AS builder

# alpine 輕量化 Image 不包含必要的 python 資源,需另外加載才可正確執行 npm 安裝指令
RUN apk add --no-cache libc6-compat python3 make g++

# build step 就是執行 docker build,此處略過,網路上很多範例,沒有什麼特別的操作。

FROM node:16.20.0-alpine AS serve-step

# 變數待執行時才帶入
ARG project_name
ARG port
WORKDIR /app

# 指定 group 和使用者
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 將檔案綁定至 group 與使用者
COPY --from=builder --chown=nextjs:nodejs /build/dist/apps/${project_name}/.next/standalone ./
# 複製靜態資料
COPY --from=builder --chown=nextjs:nodejs /build/dist/apps/${project_name}/.next/static ./dist/apps/${project_name}/.next/static
COPY --from=builder --chown=nextjs:nodejs /build/dist/apps/${project_name}/public ./apps/${project_name}/public

USER nextjs

# server.js 運行路徑
ENV APP_PATH=apps/${project_name}/server.js
EXPOSE ${port}
ENV PORT ${port}

CMD node $APP_PATH