official-docker-imagedhi.io/node:24-debian13) is a minimal configuration that does not include a shell, package manager, or coreutils. By adopting a Node.js entrypoint (TypeScript), a configuration requiring no shell or additional binaries is achieved--mount=type=bind is impractical for monorepo multi-step builds. turbo prune --docker is the officially recommended Docker optimization approach by Turborepoprocess.setuid/setgid. External binaries (gosu/setpriv/busybox) are completely unnecessarydhi.io/node:24-debian13 as the base image for the release stageimage/node/debian-13/ directorybase-files, ca-certificates, libc6, libgomp1, libstdc++6, netbase, tzdatanode (UID 1000, GID 1000)-dev): apt, bash, git, util-linux, coreutils, etc. are pre-installeddhi.io/node:24-debian13, dhi.io/node:24-debian13-devlinux/amd64, linux/arm64process.setuid/setgid. No need to copy external binaries--mount=type=bind in Monorepo Builds--mount=type=bind instead of COPY . . in the builder stage"--mount=type=bind is only valid during the execution of a RUN instruction and is not carried over to the next RUN instructionturbo prune --docker to minimize the monorepo for Dockerout/json/ — only package.json files needed for dependency installout/pnpm-lock.yaml — lockfileout/full/ — source code needed for the buildCOPY . . while leveraging layer cachingturbo prune --docker pattern instead of --mount=type=bindturbo prune --docker with pnpm workspaces needs to be verified during implementationprocess.setuid/setgid documentationsetpriv is part of util-linux and is pre-installed in the DHI dev imagegosu node command can be replaced with setpriv --reuid=node --regid=node --init-groups -- commandprocess.setgid(1000) + process.setuid(1000) + process.initgroups('node', 1000)process.setuid/setgid (setpriv is also unnecessary)apps/app/src/server/routes/apiv3/healthcheck.tshttp module is sufficient (curl is unnecessary)/_api/v3/healthcheck endpoint returns { status: 'OK' } without any parametersdepends_on: service_healthy dependency order controlnpm run migrate within CMD requires a shellapps/app/package.json's migrate scriptmigrate script content: node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.jsnpm run internally uses sh -c, so a shell is requiredchild_process.execFileSync in the Node.js entrypoint to directly execute the migration command (not using npm run, no shell needed)migrate script contents within the entrypoint--experimental-strip-types unflagged--experimental-strip-types flag needed)--experimental-transform-types is required for those: string, : number, etc.) can be used without issuesENTRYPOINT ["node", "docker-entrypoint.ts"]type Foo = 'a' | 'b') as alternatives| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| DHI runtime + busybox-static | Copy busybox-static to provide sh/coreutils | Minimal addition (~1MB) enables full functionality | Contradicts the original intent of DHI adoption (minimizing attack surface). Additional binaries are attack vectors | Rejected |
| DHI runtime + bash/coreutils copy | Copy bash and various binaries individually from the dev stage | Full bash functionality available | Shared library dependencies are complex, many files need to be copied | Rejected |
| DHI dev image as runtime | Use the dev image as-is for production | Minimal configuration changes | Increased attack surface due to apt/git etc., diminishes the meaning of DHI | Rejected |
| Node.js entrypoint (TypeScript, shell-less) | Write the entrypoint in TypeScript. Runs with Node.js 24's native TypeScript execution | Completely shell-free, maintains DHI runtime's attack surface as-is, type-safe | Migration command written directly (not using npm run), updates needed when package.json changes | Adopted |
fs, child_process, and process.setuid/setgiddocker-entrypoint.ts). Execute directly using Node.js 24's native TypeScript execution (type stripping)process.setuid/setgid — Standard Node.js API--user flag — Cannot handle dynamic processing in the entrypointprocess.initgroups('node', 1000) + process.setgid(1000) + process.setuid(1000)COPY . ., but --mount=type=bind is impractical for monorepo builds--mount=type=bind — Does not persist across RUN instructions, unsuitable for multi-step buildsturbo prune --docker — Officially recommended by Turborepoturbo prune --docker to minimize the monorepo for Docker, using optimized COPY patternsCOPY . . while remaining practicalturbo prune --docker compatibility with pnpm workspaces during implementation--max-heap-size cannot be used in NODE_OPTIONS. It needs to be passed as a direct argument to the node commandGROWI_NODE_FLAGS and inject via shell variable expansion in CMD — Requires a shellchild_process.spawn in the Node.js entrypoint — No shell neededspawn(process.execPath, [...nodeFlags, ...appArgs])apps/app/docker/codebuild/buildspec.yml — Current CodeBuild build definitionapps/app/docker/codebuild/secretsmanager.tf — AWS Secrets Manager configurationdocker login dhi.io --username <dockerhub-user> --password-stdinDOCKER_REGISTRY_PASSWORD secretdhi.io as well (no additional secrets required)reusable-app-build-image.yml -> CodeBuild Project -> buildspec.yml does not need to changedocker login dhi.io to the pre_build in buildspec.ymlsecretsmanager.tf are neededapps/app/docker-new/ with apps/app/docker/apps/app/docker keywordbuildspec.yml: -f ./apps/app/docker/Dockerfile — Same path after replacement (no change needed)codebuild.tf: buildspec = "apps/app/docker/codebuild/buildspec.yml" — Same (no change needed).github/workflows/release.yml: readme-filepath: ./apps/app/docker/README.md — Same (no change needed).github/workflows/ci-app.yml / ci-app-prod.yml: !apps/app/docker/** exclusion pattern — Same (no change needed)apps/app/bin/github-actions/update-readme.sh: cd docker + sed — Same (no change needed)apps/app/docker-new/docker-entrypoint.ts — Needs updating (self-referencing path)package.json and vitest.config for docker-related references — Nonelefthook.yml for docker-related hooks — Noneapps/app/docker/ path and require no changescodebuild/ directory and README.md are maintained as-is within docker/GROWI_HEAP_SIZE, GROWI_OPTIMIZE_MEMORY, and GROWI_LITE_MODE as environment variable names. These names obscure the relationship between the env var and the underlying V8 flag it controlsGROWI_HEAP_SIZE | V8_MAX_HEAP_SIZE | --max-heap-size |
| GROWI_OPTIMIZE_MEMORY | V8_OPTIMIZE_FOR_SIZE | --optimize-for-size |
| GROWI_LITE_MODE | V8_LITE_MODE | --lite-mode |V8_ prefix + option name in UPPER_SNAKE_CASEdocker-entrypoint.ts: Code changes (env var reads, comments, log messages)docker-entrypoint.spec.ts: Test updates (env var references in test cases)README.md: Add documentation for the new environment variablesdesign.md, requirements.md, tasks.md: Spec document updatesGROWI_HEAP_SIZE, GROWI_OPTIMIZE_MEMORY, or GROWI_LITE_MODE in their docker-compose.yml or deployment configs will need to update to the new names. This is acceptable as these variables were introduced in the same release (v7.5.x) and have not been published yetmigrate script from package.json is written directly in the entrypoint, so synchronization is needed when changes occur -> Clearly noted in comments during implementationprocess.initgroups is required for supplementary group initialization. The order setgid -> setuid must be strictly followeddocker login dhi.io is required in CI/CD. Security considerations for credential management are neededdhi.io/node:24-debian13-dev) did not include the which commandSHELL="$(which sh)" to SHELL=/bin/shdhi.io/node:24-debian13) did not have /bin/sh. The design planned --mount=type=bind,from=builder + RUN tar -zxf, but RUN instructions require /bin/shtar -zcf to cp -a into a staging directory /tmp/release/RUN --mount=type=bind... tar -zxf to COPY --from=builder --chown=node:node--mount=type=bind,from=builder pattern) was replaced with COPY --from=builder. The security goal of not requiring a shell at runtime was achieved even more robustlyCOPY, WORKDIR, ENV, LABEL, ENTRYPOINT are processed by the Docker daemon and do not require a shellprocess.initgroups('node', 1000) was called for in the design, but implementation was deferred because the type definition does not exist in @types/nodeprocess.initgroups does exist at runtime in Node.js 24@types/node fix, or use (process as any).initgroups('node', 1000)apps/app/tmp/memory-results/REPORT.md) — Basis for heap size control