Purpose: Modernize the Dockerfile and entrypoint for the GROWI official Docker image based on 2025-2026 best practices, achieving enhanced security, optimized memory management, and improved build efficiency.
Users: Infrastructure administrators (build/deploy), GROWI operators (memory tuning), and Docker image end users (usage via docker-compose).
Impact: Redesign the existing 3-stage Dockerfile into a 5-stage configuration. Migrate the base image to Docker Hardened Images (DHI). Change the entrypoint from a shell script to TypeScript (using Node.js 24 native TypeScript execution), achieving a fully hardened configuration that requires no shell.
V8_MAX_HEAP_SIZE / cgroup auto-calculation / V8 defaultV8_MAX_HEAP_SIZE, V8_OPTIMIZE_FOR_SIZE, V8_LITE_MODE)turbo prune --docker patternprocess.setuid/setgid (Node.js native)V8_MAX_HEAP_SIZE configuration is out of scope)Current Dockerfile 3-stage configuration:
| Stage | Base Image | Role |
|---|---|---|
base |
node:20-slim |
Install pnpm + turbo |
builder |
base |
COPY . . → install → build → artifacts |
| release (unnamed) | node:20-slim |
gosu install → artifact extraction → execution |
Main issues:
COPY . . includes the entire monorepo in the build layerPNPM_VERSION="10.32.1")---frozen-lockfilegraph TB
subgraph BuildPhase
base[base stage<br>DHI dev + pnpm + turbo]
pruner[pruner stage<br>turbo prune --docker]
deps[deps stage<br>dependency install]
builder[builder stage<br>build + artifacts]
end
subgraph ReleasePhase
release[release stage<br>DHI runtime - no shell]
end
base --> pruner
pruner --> deps
deps --> builder
builder -->|artifacts| release
subgraph RuntimeFiles
entrypoint[docker-entrypoint.ts<br>TypeScript entrypoint]
end
entrypoint --> release
Architecture Integration:
process.setuid/setgid + fs module + child_process.execFileSync/spawn). Eliminates the need for copying busybox/bash, maintaining the minimized attack surface of the DHI runtime as-is. Executes .ts directly via Node.js 24 type stripping| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Base Image (build) | dhi.io/node:24-debian13-dev |
Base for build stages | apt/bash/git/util-linux available |
| Base Image (runtime) | dhi.io/node:24-debian13 |
Base for release stage | Minimal configuration, 95% CVE reduction, no shell |
| Entrypoint | Node.js (TypeScript) | Initialization, heap calculation, privilege drop, process startup | Node.js 24 native type stripping, no busybox/bash needed |
| Privilege Drop | process.setuid/setgid (Node.js) |
root → node user switch | No external binaries needed |
| Build Tool | turbo prune --docker |
Monorepo minimization | Official Turborepo recommendation |
| Package Manager | pnpm (wget standalone) | Dependency management | corepack not adopted (scheduled for removal in Node.js 25+) |
For the rationale behind adopting the TypeScript entrypoint and comparison with busybox-static/setpriv, see
research.md.
flowchart TD
Start[Container Start<br>as root via node entrypoint.ts] --> Setup[Directory Setup<br>fs.mkdirSync + symlinkSync + chownSync]
Setup --> HeapCalc{V8_MAX_HEAP_SIZE<br>is set?}
HeapCalc -->|Yes| UseEnv[Use V8_MAX_HEAP_SIZE]
HeapCalc -->|No| CgroupCheck{cgroup limit<br>detectable?}
CgroupCheck -->|Yes| AutoCalc[Auto-calculate<br>60% of cgroup limit]
CgroupCheck -->|No| NoFlag[No heap flag<br>V8 default]
UseEnv --> OptFlags[Check V8_OPTIMIZE_FOR_SIZE<br>and V8_LITE_MODE]
AutoCalc --> OptFlags
NoFlag --> OptFlags
OptFlags --> LogFlags[console.log applied flags]
LogFlags --> DropPriv[Drop privileges<br>process.setgid + setuid]
DropPriv --> Migration[Run migration<br>execFileSync node migrate-mongo]
Migration --> SpawnApp[Spawn app process<br>node --max-heap-size=X ... app.js]
SpawnApp --> SignalFwd[Forward SIGTERM/SIGINT<br>to child process]
Key Decisions:
/sys/fs/cgroup/memory.max), fall back to v1--max-heap-size is passed to the spawned child process (the application itself), not the entrypoint processchild_process.execFileSync calling node (no npm run, no shell needed)child_process.spawn + signal forwarding to fulfill PID 1 responsibilitiesflowchart LR
subgraph Stage1[base]
S1[DHI dev image<br>+ pnpm + turbo]
end
subgraph Stage2[pruner]
S2A[COPY monorepo]
S2B[turbo prune --docker]
end
subgraph Stage3[deps]
S3A[COPY json + lockfile]
S3B[pnpm install --frozen-lockfile]
end
subgraph Stage4[builder]
S4A[COPY full source]
S4B[turbo run build]
S4C[pnpm deploy + tar.gz]
end
subgraph Stage5[release]
S5A[DHI runtime<br>no additional binaries]
S5B[Extract artifacts]
S5C[COPY entrypoint.js]
end
Stage1 --> Stage2 --> Stage3 --> Stage4
Stage4 -->|tar.gz| Stage5
| Component | Domain/Layer | Intent | Key Dependencies |
|---|---|---|---|
| Dockerfile | Infrastructure | 5-stage Docker image build definition | DHI images, turbo, pnpm |
| docker-entrypoint.ts | Infrastructure | Container startup initialization (TypeScript) | Node.js fs/child_process, cgroup fs |
| docker-entrypoint.spec.ts | Infrastructure | Unit tests for entrypoint | vitest |
| Dockerfile.dockerignore | Infrastructure | Build context filter | — |
| README.md | Documentation | Docker Hub image documentation | — |
| buildspec.yml | CI/CD | CodeBuild build definition | AWS Secrets Manager, dhi.io |
Responsibilities & Constraints
base → pruner → deps → builder → releasedhi.io/node:24-debian13-dev / dhi.io/node:24-debian13)Stage Definitions:
ca-certificates, wget)COPY . . + turbo prune @growi/app --dockerpnpm install --frozen-lockfile + node-gypturbo run build + pnpm deploy + artifact packagingCOPY --from=builder artifacts + entrypoint + OCI labels + EXPOSE/VOLUMEResponsibilities & Constraints
/data/uploads + symlink, /tmp/page-bulk-export)process.setgid() + process.setuid()child_process.execFileSync (direct node invocation, no shell)child_process.spawn with signal forwarding (PID 1 responsibilities)Environment Variable Interface
| Variable | Type | Default | Description |
|---|---|---|---|
V8_MAX_HEAP_SIZE |
int (MB) | (unset) | Explicitly specify the --max-heap-size value for Node.js |
V8_OPTIMIZE_FOR_SIZE |
"true" / (unset) |
(unset) | Enable the --optimize-for-size flag |
V8_LITE_MODE |
"true" / (unset) |
(unset) | Enable the --lite-mode flag |
Naming Convention: Environment variable names are aligned with their corresponding V8 option names (
--max-heap-size,--optimize-for-size,--lite-mode) prefixed withV8_. This improves discoverability and self-documentation compared to the previousGROWI_-prefixed names.
Batch Contract
ENTRYPOINT ["node", "/docker-entrypoint.ts"])"true" is valid), cgroup v2 (memory.max: numeric or "max"), cgroup v1 (memory.limit_in_bytes: numeric, large value = unlimited)child_process.spawnfs.mkdirSync({ recursive: true })Responsibilities & Constraints
V8_MAX_HEAP_SIZE, V8_OPTIMIZE_FOR_SIZE, V8_LITE_MODE| Error | Category | Response |
|---|---|---|
| cgroup file read failure | System | Warn and continue with no flag (V8 default) |
| V8_MAX_HEAP_SIZE is invalid | User | Warn and continue with no flag (container still starts) |
| Directory creation/permission failure | System | process.exit(1) — check volume mount configuration |
| Migration failure | Business Logic | execFileSync throws → process.exit(1) — Docker/k8s restarts |
| App process abnormal exit | System | Propagate child process exit code |
turbo prune --docker caches the dependency install layer. Skips dependency installation during rebuilds when only source code changes--max-heap-size avoids the v24 trusted_space overhead issue. Prevents memory pressure in multi-tenant environments