🌳
pt0/deployF/dockerF/dockerImageF.mts
6import * as fs from 'fs'
7import * as _ from 'lodash-es'
17import { npmName } from './npmNameF.mts'
24import type { DockFile, BakedDf } from './dockFileF.mts'
26const withStandalone = cliFlag('--withStandalone'), withTurbo = cliFlag('--withTurbo'), freshCache = cliFlag('--freshCache')
28const yarnishFileNames = (() => {
29 if ((npmName as string) == 'yarn') return ['yarn.lock']
30 if (npmName == 'pnpm') return [
31 'pnpm-lock.yaml',
32 '.npmrc',
33 ]
34 throw new PtErr('npmName', {npmName})
35})()
37export const globalDependPaths = [
38 '.nvmrc',
39 'package.json',
40 'tsconfig.json',
41 'pnpm-workspace.yaml',
42 ...yarnishFileNames,
45export const dockerBuildAndRet = async ({
46 dfToBuild,
47 reqDockPush = true,
48 ...props
49}: {
50 dfToBuild: DockFile & {imageReused?: boolean},
51 reqDockPush?: boolean,
52 [k: string]: unknown,
53}) => {
54 const {action, git_repo_name, dockreg_host, dockLanHost, dfLabel, cluster_name, gitsshHost, gitsshRepoPath} = _.merge(getKlusterCtx(), props) as Record<string, string>
55 const appCfg = getAppCfg()
56 if (!appCfg.gitsshHost && !appCfg.git_repo_name) appCfgCtx.enterWith(applyDefaultKanikoCfg(appCfg))
57 const {git_sha, kanikoClusterName, name: _name} = _.merge(getAppCfg(), props) as Record<string, string>
59 const {repo_name_tag, push_repo_name_tag, dockerfileContent, tarSpecsA, includeShaInTag} = await dfToBuild.getRnt()
61 const name = _name + '-rnt-' + calcShortHash(repo_name_tag)
63 if (deployActionGensDf({action})) {
64 const buildResult = await dockerBuildPush({
65 name, action, repo_name_tag: push_repo_name_tag, git_sha, dfLabel,
66 dockerfileContent, tarSpecsA, git_repo_name, dockreg_host, dockLanHost,
67 cluster_name: kanikoClusterName || cluster_name, reqDockPush,
68 gitsshHost, gitsshRepoPath, includeShaInTag: includeShaInTag as boolean | undefined,
69 })
70 if (buildResult?.imageReused) dfToBuild.imageReused = true
71 }
73 return dfToBuild
76const nextPath = {pnpm: 'npx next', yarn: '../../node_modules/.bin/next'}[npmName]
77assertDefined(nextPath)
79export const dockerFileNextBuild = async ({aptPkgNames, dockerEnv, afterYarnInstallDockPathsA}: {
80 aptPkgNames?: string[], dockerEnv: Record<string, string>, afterYarnInstallDockPathsA: string[],
81}) => {
82 const {appPath} = nextDeployCtx.getStore() as {appPath: string, prebuildEnvConfH?: Record<string, string>}
84 // includeShaInTag: true ensures NEXT_PUBLIC_GIT_SHA is included in image tag hash
85 // this makes nextbuild rebuild when app code changes (filtered by import tree)
86 const dockFile = await getDockerBaseStr({afterYarnInstallDockPathsA, appPath, aptPkgNames, includeShaInTag: true})
88 const {pushLine} = dockFile
90 const appDir = getFullAppPath({appPath})
91 const nextCacheDir = appDir + '/.next/cache'
92 pushLine(getEnvLines({dockerEnv}))
93 pushLine(`WORKDIR ${appDir}`)
94 pushLine(`ENV ISNEXTBUILD=1`)
95 if (withStandalone) pushLine(`ENV NEXT_STANDALONE=1`)
96 const nextBuildHeapMb = getAppCfg().nextBuildHeapMb || 2048
97 dockFile.pushEnv('NODE_OPTIONS', `--no-warnings=ExperimentalWarning --max-old-space-size=${nextBuildHeapMb}`, {append: true})
98 const {builtWorkerFilename} = getPubEnv() || {}
100 let workerContentHash = ''
101 if (builtWorkerFilename) {
102 const workerAbsPath = `${ptDir}/${appPath}/public/${builtWorkerFilename}`
103 if (fs.existsSync(workerAbsPath)) {
104 workerContentHash = calcShortHash(fs.readFileSync(workerAbsPath, 'utf8'))
105 }
106 }
108 const dockerCacheKey = 'nextbuild' + calcShortHash(
109 JSON.stringify([
110 _.get(nextDeployCtx.getStore(), 'prebuildEnvConfH'),
111 dockBumpNo,
112 workerContentHash,
113 withStandalone ? 'standalone' : '',
114 withTurbo ? 'turbo' : '',
115 ])
116 )
117 // webpack fs cache hard-links into node_modules; overlayfs can't extract cross-dir hard links during pull → ErrImagePull
118 // in-progress marker: a killed build leaves the marker in the PVC-backed cache mount → next build wipes & starts cold.
119 // idempotent self-heal — no manual PVC surgery needed after a timeout/eviction mid-build.
120 const cacheGuard = `mkdir -p ${nextCacheDir} && { ${freshCache ? 'true' : `[ -f ${nextCacheDir}/.ptbuild-inprogress ]`} && find ${nextCacheDir} -mindepth 1 -delete || true; } && touch ${nextCacheDir}/.ptbuild-inprogress`
121 const buildCmdA = [
122 cacheGuard,
123 `echo ${dockerCacheKey}`,
124 `${nextPath} build${withTurbo ? ' --turbo' : ''}`,
125 `find ${nextCacheDir} -type f -links +1 -exec sh -c 'cp "$1" "$1.bak" && mv "$1.bak" "$1"' _ {} \\;`,
126 `rm ${nextCacheDir}/.ptbuild-inprogress`,
127 ]
128 pushLine(`RUN --mount=type=cache,id=${dockerCacheKey},target=${nextCacheDir} ${buildCmdA.join(' && ')}`)
129 pushLine(`ENV ISNEXTBUILD=`)
131 return dockFile
134export const flatDfBuild = false
136export const dockerFileNextServ = async ({appPath, dockerEnv, lastNextBuildDf}: {
137 appPath: string, dockerEnv: Record<string, string>, lastNextBuildDf: BakedDf,
138}) => {
139 const dockFile = initDockFile(undefined, {includeShaInTag: true})
140 const {pushLine} = dockFile
141 const appDir = getFullAppPath({appPath})
143 if (withStandalone) {
144 const port = getNextSvcPortNo()
145 pushLine(`FROM ${lastNextBuildDf.repo_name_tag} AS nextbuildstage`)
146 pushLine(`RUN mkdir -p ${appDir}/public`)
147 pushLine(`FROM ${nodeJsDockerImage()} AS runnerstage`)
148 pushLine(getEnvLines({dockerEnv}))
149 pushLine(`ENV PORT=${port}`)
150 pushLine(`ENV HOSTNAME=0.0.0.0`)
151 pushLine(`WORKDIR ${dockerApproot}`)
152 pushLine(`COPY --from=nextbuildstage ${appDir}/.next/standalone ${dockerApproot}`)
153 pushLine(`COPY --from=nextbuildstage ${appDir}/.next/static ${appDir}/.next/static`)
154 pushLine(`COPY --from=nextbuildstage ${appDir}/public ${appDir}/public`)
155 pushLine(`WORKDIR ${appDir}`)
156 pushLine(`EXPOSE ${port}`)
157 pushLine(`CMD ["node", "server.js"]`)
158 } else if (flatDfBuild) {
159 pushLine(lastNextBuildDf)
160 pushLine(`WORKDIR ${appDir}`)
161 pushLine(`EXPOSE ${getNextSvcPortNo()}`)
162 pushLine(`CMD ["node", "--enable-source-maps", "runnext.mjs"]`)
163 } else {
164 pushLine(`FROM ${lastNextBuildDf.repo_name_tag} AS nextbuildstage`)
165 pushLine(`FROM ${nodeJsDockerImage()} AS runnerstage`)
166 pushLine(getEnvLines({dockerEnv}))
167 pushLine(`COPY --from=nextbuildstage ${dockerApproot} ${dockerApproot}`)
168 pushLine(`WORKDIR ${appDir}`)
169 pushLine(`EXPOSE ${getNextSvcPortNo()}`)
170 pushLine(`CMD ["node", "--enable-source-maps", "runnext.mjs"]`)
171 }
173 return dockFile