Просмотр исходного кода

add official-docker-image spec

Yuki Takei 1 месяц назад
Родитель
Сommit
4dc0e31e5d

+ 506 - 0
.kiro/specs/official-docker-image/design.md

@@ -0,0 +1,506 @@
+# Design Document: official-docker-image
+
+## Overview
+
+**Purpose**: GROWI 公式 Docker イメージの Dockerfile と entrypoint を 2025-2026 年のベストプラクティスに基づきモダナイズし、セキュリティ強化・メモリ管理最適化・ビルド効率向上を実現する。
+
+**Users**: インフラ管理者(ビルド・デプロイ)、GROWI 運用者(メモリチューニング)、Docker image エンドユーザー(docker-compose での利用)が対象。
+
+**Impact**: 既存の 3 ステージ Dockerfile を 5 ステージ構成に再設計。ベースイメージを Docker Hardened Images (DHI) に移行。entrypoint を shell script から TypeScript に変更し(Node.js 24 のネイティブ TypeScript 実行)、シェル不要の完全ハードニング構成を実現。
+
+### Goals
+
+- DHI ベースイメージ採用による CVE 最大 95% 削減
+- **シェル完全不要の TypeScript entrypoint** — Node.js 24 のネイティブ TypeScript 実行(type stripping)、DHI runtime の攻撃面最小化をそのまま維持
+- `GROWI_HEAP_SIZE` / cgroup 自動算出 / V8 デフォルトの 3 段フォールバックによるメモリ管理
+- `turbo prune --docker` パターンによるビルドキャッシュ効率向上
+- gosu → `process.setuid/setgid`(Node.js ネイティブ)による権限ドロップ
+
+### Non-Goals
+
+- Kubernetes マニフェスト / Helm chart の変更(GROWI.cloud 側の `GROWI_HEAP_SIZE` 設定は対象外)
+- アプリケーションコードの変更(gc() 追加、.pipe() 移行等は別 spec)
+- docker-compose.yml の更新(ドキュメント更新のみ)
+- Node.js 24 未満のバージョンサポート
+- HEALTHCHECK 命令の追加(k8s は独自 probe を使用、Docker Compose ユーザーは自前で設定可能)
+
+## Architecture
+
+### Existing Architecture Analysis
+
+**現行 Dockerfile の 3 ステージ構成:**
+
+| Stage | Base Image | 役割 |
+|-------|-----------|------|
+| `base` | `node:20-slim` | pnpm + turbo のインストール |
+| `builder` | `base` | `COPY . .` → install → build → artifacts |
+| release (unnamed) | `node:20-slim` | gosu install → artifacts 展開 → 実行 |
+
+**主な課題:**
+- `COPY . .` でモノレポ全体がビルドレイヤーに含まれる
+- pnpm バージョンがハードコード (`PNPM_VERSION="10.4.1"`)
+- `---frozen-lockfile` の typo
+- ベースイメージが node:20-slim(CVE が蓄積しやすい)
+- メモリ管理フラグなし
+- OCI ラベルなし
+- gosu のインストールに apt-get が必要(runtime に apt 依存)
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph 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:**
+- Selected pattern: Multi-stage build with dependency caching separation
+- Domain boundaries: Build concerns (stages 1-4) vs Runtime concerns (stage 5 + entrypoint)
+- Existing patterns preserved: pnpm deploy による本番依存抽出、tar.gz アーティファクト転送
+- New components: pruner ステージ(turbo prune)、TypeScript entrypoint
+- **Key change**: gosu + shell script → TypeScript entrypoint(`process.setuid/setgid` + `fs` module + `child_process.execFileSync/spawn`)。busybox/bash のコピーが不要になり、DHI runtime の攻撃面最小化をそのまま維持。Node.js 24 の type stripping で `.ts` を直接実行
+- Steering compliance: Debian ベース維持(glibc パフォーマンス)、モノレポビルドパターン維持
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Base Image (build) | `dhi.io/node:24-debian13-dev` | ビルドステージのベース | apt/bash/git/util-linux 利用可能 |
+| Base Image (runtime) | `dhi.io/node:24-debian13` | リリースステージのベース | 極小構成、CVE 95% 削減、**シェルなし** |
+| Entrypoint | Node.js (TypeScript) | 初期化・ヒープ算出・権限ドロップ・プロセス起動 | Node.js 24 native type stripping、busybox/bash 不要 |
+| Privilege Drop | `process.setuid/setgid` (Node.js) | root → node ユーザー切替 | 外部バイナリ不要 |
+| Build Tool | `turbo prune --docker` | モノレポ最小化 | Turborepo 公式推奨 |
+| Package Manager | pnpm (wget standalone) | 依存管理 | corepack 不採用(Node.js 25+ で廃止予定) |
+
+> TypeScript entrypoint 採用の経緯、busybox-static/setpriv との比較は `research.md` を参照。
+
+## System Flows
+
+### Entrypoint 実行フロー
+
+```mermaid
+flowchart TD
+    Start[Container Start<br>as root via node entrypoint.ts] --> Setup[Directory Setup<br>fs.mkdirSync + symlinkSync + chownSync]
+    Setup --> HeapCalc{GROWI_HEAP_SIZE<br>is set?}
+    HeapCalc -->|Yes| UseEnv[Use GROWI_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 GROWI_OPTIMIZE_MEMORY<br>and GROWI_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:**
+- cgroup v2 (`/sys/fs/cgroup/memory.max`) を優先、v1 にフォールバック
+- cgroup v1 の unlimited 値(巨大な数値)はフラグなしとして扱う(閾値: 64GB)
+- `--max-heap-size` は entrypoint プロセスではなく、spawn される子プロセス(アプリ本体)に渡される
+- migration は `child_process.execFileSync` で直接 node を呼び出す(`npm run` 不使用、シェル不要)
+- アプリ起動は `child_process.spawn` + シグナルフォワーディングで PID 1 の責務を果たす
+
+### Docker Build フロー
+
+```mermaid
+flowchart 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
+```
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces | Flows |
+|-------------|---------|------------|------------|-------|
+| 1.1 | DHI ベースイメージ | base, release ステージ | — | Build フロー |
+| 1.2 | syntax ディレクティブ更新 | Dockerfile ヘッダ | — | — |
+| 1.3 | pnpm wget インストール維持 | base ステージ | — | Build フロー |
+| 1.4 | frozen-lockfile typo 修正 | deps ステージ | — | — |
+| 1.5 | pnpm バージョン非ハードコード | base ステージ | — | — |
+| 2.1 | GROWI_HEAP_SIZE | docker-entrypoint.ts | 環境変数 I/F | Entrypoint フロー |
+| 2.2 | cgroup 自動算出 | docker-entrypoint.ts | cgroup fs I/F | Entrypoint フロー |
+| 2.3 | フラグなしフォールバック | docker-entrypoint.ts | — | Entrypoint フロー |
+| 2.4 | GROWI_OPTIMIZE_MEMORY | docker-entrypoint.ts | 環境変数 I/F | Entrypoint フロー |
+| 2.5 | GROWI_LITE_MODE | docker-entrypoint.ts | 環境変数 I/F | Entrypoint フロー |
+| 2.6 | --max-heap-size 使用 | docker-entrypoint.ts | spawn args | Entrypoint フロー |
+| 2.7 | NODE_OPTIONS 不使用 | docker-entrypoint.ts | — | Entrypoint フロー |
+| 3.1 | COPY . . 廃止 | pruner + deps ステージ | — | Build フロー |
+| 3.2 | pnpm cache mount 維持 | deps, builder ステージ | — | Build フロー |
+| 3.3 | apt cache mount 維持 | base ステージ | — | Build フロー |
+| 3.4 | .next/cache 除外 | builder ステージ | — | — |
+| 3.5 | bind from=builder パターン | release ステージ | — | Build フロー |
+| 4.1 | 非 root 実行 | docker-entrypoint.ts | process.setuid/setgid | Entrypoint フロー |
+| 4.2 | 不要パッケージ排除 | release ステージ | — | — |
+| 4.3 | .dockerignore 強化 | Dockerfile.dockerignore | — | — |
+| 4.4 | --no-install-recommends | base ステージ | — | — |
+| 4.5 | ビルドツール排除 | release ステージ | — | — |
+| 5.1 | OCI ラベル | release ステージ | — | — |
+| 5.2 | EXPOSE 維持 | release ステージ | — | — |
+| 5.3 | VOLUME 維持 | release ステージ | — | — |
+| 6.1 | ヒープサイズ算出ロジック | docker-entrypoint.ts | — | Entrypoint フロー |
+| 6.2 | 権限ドロップ exec | docker-entrypoint.ts | process.setuid/setgid | Entrypoint フロー |
+| 6.3 | /data/uploads 維持 | docker-entrypoint.ts | fs module | Entrypoint フロー |
+| 6.4 | /tmp/page-bulk-export 維持 | docker-entrypoint.ts | fs module | Entrypoint フロー |
+| 6.5 | CMD migrate 維持 | docker-entrypoint.ts | execFileSync | Entrypoint フロー |
+| 6.6 | --expose_gc 維持 | docker-entrypoint.ts | spawn args | Entrypoint フロー |
+| 6.7 | フラグログ出力 | docker-entrypoint.ts | console.log | Entrypoint フロー |
+| 6.8 | TypeScript で記述 | docker-entrypoint.ts | Node.js type stripping | — |
+| 7.1-7.5 | 後方互換性 | 全コンポーネント | — | — |
+
+## Components and Interfaces
+
+| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|-------------|--------|-------------|-----------------|-----------|
+| Dockerfile | Infrastructure | Docker イメージビルド定義 | 1.1-1.5, 3.1-3.5, 4.1-4.5, 5.1-5.3, 6.5 | DHI images (P0), turbo (P0), pnpm (P0) | — |
+| docker-entrypoint.ts | Infrastructure | コンテナ起動時の初期化(TypeScript) | 2.1-2.7, 6.1-6.4, 6.6-6.8 | Node.js fs/child_process (P0), cgroup fs (P1) | Batch |
+| Dockerfile.dockerignore | Infrastructure | ビルドコンテキストフィルタ | 4.3 | — | — |
+
+### Infrastructure Layer
+
+#### Dockerfile
+
+| Field | Detail |
+|-------|--------|
+| Intent | 5 ステージの Docker イメージビルド定義 |
+| Requirements | 1.1-1.5, 3.1-3.5, 4.1-4.5, 5.1-5.3, 6.5, 7.1-7.5 |
+
+**Responsibilities & Constraints**
+- 5 ステージ構成: `base` → `pruner` → `deps` → `builder` → `release`
+- DHI ベースイメージの使用(`dhi.io/node:24-debian13-dev` / `dhi.io/node:24-debian13`)
+- **runtime にシェル・追加バイナリのコピーなし**(Node.js entrypoint で全て完結)
+- OCI ラベルの付与
+
+**Dependencies**
+- External: `dhi.io/node:24-debian13-dev` — ビルドベースイメージ (P0)
+- External: `dhi.io/node:24-debian13` — ランタイムベースイメージ (P0)
+- Outbound: pnpm — 依存管理 (P0)
+- Outbound: turbo — ビルドオーケストレーション (P0)
+
+**Contracts**: Batch [x]
+
+##### Stage Definitions
+
+**Stage 1: `base`**
+```
+FROM dhi.io/node:24-debian13-dev AS base
+```
+- apt-get で `ca-certificates`, `wget` をインストール(ビルド専用)
+- wget スタンドアロンスクリプトで pnpm をインストール(バージョンはスクリプトのデフォルト)
+- pnpm add turbo --global
+
+**Stage 2: `pruner`**
+```
+FROM base AS pruner
+```
+- `COPY . .` でモノレポ全体をコピー
+- `turbo prune @growi/app --docker` で Docker 最適化ファイルを生成
+- 出力: `out/json/`(package.json 群)、`out/pnpm-lock.yaml`、`out/full/`(ソース)
+
+**Stage 3: `deps`**
+```
+FROM base AS deps
+```
+- `COPY --from=pruner` で json/ と lockfile のみコピー(キャッシュ効率化)
+- `pnpm install --frozen-lockfile` で依存インストール
+- `pnpm add node-gyp --global`(native modules 用)
+
+**Stage 4: `builder`**
+```
+FROM deps AS builder
+```
+- `COPY --from=pruner` で full/ ソースをコピー
+- `turbo run build --filter @growi/app`
+- `pnpm deploy out --prod --filter @growi/app`
+- artifacts を tar.gz にパッケージング(現行の内容を維持、`apps/app/tmp` 含む)
+
+**Stage 5: `release`**
+```
+FROM dhi.io/node:24-debian13 AS release
+```
+- **追加バイナリのコピーなし**(シェル・gosu・setpriv・busybox 一切不要)
+- artifacts を `--mount=type=bind,from=builder` で展開
+- `docker-entrypoint.ts` を COPY
+- OCI ラベル、EXPOSE、VOLUME を設定
+- `ENTRYPOINT ["node", "/docker-entrypoint.ts"]`
+
+**Implementation Notes**
+- `turbo prune --docker` が pnpm workspace と互換でない場合のフォールバック: 最適化 COPY パターン(lockfile + package.json 群を先にコピー → install → ソースコピー → build)
+- DHI イメージの pull には `docker login dhi.io` が必要(CI/CD での認証設定が必要)
+- release ステージに apt-get は一切不要(現行の gosu install が完全に排除される)
+
+#### docker-entrypoint.ts
+
+| Field | Detail |
+|-------|--------|
+| Intent | コンテナ起動時の初期化処理(ディレクトリ設定、ヒープサイズ算出、権限ドロップ、migration 実行、アプリ起動)。TypeScript で記述、Node.js 24 のネイティブ type stripping で直接実行 |
+| Requirements | 2.1-2.7, 6.1-6.8 |
+
+**Responsibilities & Constraints**
+- **TypeScript で記述**: Node.js 24 のネイティブ type stripping で直接実行(`node docker-entrypoint.ts`)。enum は使用不可(erasable syntax のみ使用)
+- root 権限での初期化処理(`fs.mkdirSync`、`fs.symlinkSync`、`fs.chownSync` で実装)
+- 3 段フォールバックによるヒープサイズ決定(`fs.readFileSync` で cgroup 読み取り)
+- Node.js ネイティブの `process.setgid()` + `process.setuid()` で権限ドロップ
+- `child_process.execFileSync` で migration を直接実行(npm run 不使用、シェル不要)
+- `child_process.spawn` でアプリプロセスを起動し、SIGTERM/SIGINT をフォワード
+- **外部バイナリ依存なし**(Node.js の標準ライブラリのみ使用)
+
+**Dependencies**
+- External: Node.js `fs` module — ファイルシステム操作 (P0)
+- External: Node.js `child_process` module — プロセス起動 (P0)
+- External: cgroup filesystem — メモリリミット取得 (P1)
+- Inbound: Environment variables — GROWI_HEAP_SIZE, GROWI_OPTIMIZE_MEMORY, GROWI_LITE_MODE
+
+**Contracts**: Batch [x]
+
+##### Batch / Job Contract
+
+- **Trigger**: コンテナ起動時(`ENTRYPOINT ["node", "/docker-entrypoint.ts"]` として実行)
+- **Input / validation**:
+  - `GROWI_HEAP_SIZE`: 正の整数(MB 単位)。空文字列は未設定として扱う
+  - `GROWI_OPTIMIZE_MEMORY`: `"true"` のみ有効。それ以外は無視
+  - `GROWI_LITE_MODE`: `"true"` のみ有効。それ以外は無視
+  - cgroup v2: `/sys/fs/cgroup/memory.max` — 数値または `"max"`(unlimited)
+  - cgroup v1: `/sys/fs/cgroup/memory/memory.limit_in_bytes` — 数値(unlimited 時は巨大値)
+- **Output / destination**: `child_process.spawn` の引数として node フラグを直接渡す
+- **Idempotency & recovery**: コンテナ再起動時に毎回実行。冪等(`fs.mkdirSync` の `recursive: true` で安全)
+
+##### Environment Variable Interface
+
+| Variable | Type | Default | Description |
+|----------|------|---------|-------------|
+| `GROWI_HEAP_SIZE` | int (MB) | (未設定) | Node.js の --max-heap-size 値を明示指定 |
+| `GROWI_OPTIMIZE_MEMORY` | `"true"` / (未設定) | (未設定) | --optimize-for-size フラグを有効化 |
+| `GROWI_LITE_MODE` | `"true"` / (未設定) | (未設定) | --lite-mode フラグを有効化 |
+
+##### Heap Size Calculation Logic
+
+```typescript
+// Priority 1: GROWI_HEAP_SIZE env
+// Priority 2: cgroup v2 (/sys/fs/cgroup/memory.max) — 60%
+// Priority 3: cgroup v1 (/sys/fs/cgroup/memory/memory.limit_in_bytes) — 60%, < 64GB
+// Priority 4: undefined (V8 default)
+
+function detectHeapSize(): number | undefined {
+  const envValue: string | undefined = process.env.GROWI_HEAP_SIZE;
+  if (envValue != null && envValue !== '') {
+    const parsed: number = parseInt(envValue, 10);
+    return Number.isNaN(parsed) ? undefined : parsed;
+  }
+
+  // cgroup v2
+  const cgroupV2: number | undefined = readCgroupLimit('/sys/fs/cgroup/memory.max');
+  if (cgroupV2 != null) {
+    return Math.floor(cgroupV2 / 1024 / 1024 * 0.6);
+  }
+
+  // cgroup v1
+  const cgroupV1: number | undefined = readCgroupLimit('/sys/fs/cgroup/memory/memory.limit_in_bytes');
+  if (cgroupV1 != null && cgroupV1 < 64 * 1024 * 1024 * 1024) {
+    return Math.floor(cgroupV1 / 1024 / 1024 * 0.6);
+  }
+
+  return undefined;
+}
+```
+
+##### Node Flags Assembly
+
+```typescript
+const nodeFlags: string[] = ['--expose_gc'];
+
+const heapSize: number | undefined = detectHeapSize();
+if (heapSize != null) {
+  nodeFlags.push(`--max-heap-size=${heapSize}`);
+}
+
+if (process.env.GROWI_OPTIMIZE_MEMORY === 'true') {
+  nodeFlags.push('--optimize-for-size');
+}
+
+if (process.env.GROWI_LITE_MODE === 'true') {
+  nodeFlags.push('--lite-mode');
+}
+```
+
+##### Directory Setup (as root)
+
+```typescript
+import fs from 'node:fs';
+
+// /data/uploads for FILE_UPLOAD=local
+fs.mkdirSync('/data/uploads', { recursive: true });
+if (!fs.existsSync('./public/uploads')) {
+  fs.symlinkSync('/data/uploads', './public/uploads');
+}
+chownRecursive('/data/uploads', 1000, 1000);
+fs.lchownSync('./public/uploads', 1000, 1000);
+
+// /tmp/page-bulk-export
+fs.mkdirSync('/tmp/page-bulk-export', { recursive: true });
+chownRecursive('/tmp/page-bulk-export', 1000, 1000);
+fs.chmodSync('/tmp/page-bulk-export', 0o700);
+```
+
+`chownRecursive` は `fs.readdirSync` + `fs.chownSync` で再帰的に所有者を変更するヘルパー関数。
+
+##### Privilege Drop
+
+```typescript
+process.initgroups('node', 1000);
+process.setgid(1000);
+process.setuid(1000);
+```
+
+`setgid` → `setuid` の順序は必須(setuid 後は setgid できない)。`initgroups` で supplementary groups も初期化。
+
+##### Migration Execution
+
+```typescript
+import { execFileSync } from 'node:child_process';
+
+execFileSync(process.execPath, [
+  '-r', 'dotenv-flow/config',
+  'node_modules/migrate-mongo/bin/migrate-mongo', 'up',
+  '-f', 'config/migrate-mongo-config.js',
+], { stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production' } });
+```
+
+`execFileSync` はシェルを介さず直接 node バイナリを実行。`npm run migrate` と同等の動作をシェル不要で実現。
+
+##### App Process Spawn
+
+```typescript
+import { spawn } from 'node:child_process';
+import type { ChildProcess } from 'node:child_process';
+
+const child: ChildProcess = spawn(process.execPath, [
+  ...nodeFlags,
+  '-r', 'dotenv-flow/config',
+  'dist/server/app.js',
+], { stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production' } });
+
+// PID 1 signal forwarding
+const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];
+for (const sig of signals) {
+  process.on(sig, () => child.kill(sig));
+}
+child.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
+  process.exit(code ?? (signal === 'SIGTERM' ? 0 : 1));
+});
+```
+
+**Implementation Notes**
+- TypeScript で記述し、Node.js 24 のネイティブ type stripping で直接実行。`ENTRYPOINT ["node", "/docker-entrypoint.ts"]`
+- enum は使用不可(非 erasable syntax)。interface/type/type annotation のみ使用
+- entrypoint は `process.execPath`(= `/usr/local/bin/node`)を使って migration と app を実行するため、シェルが一切不要
+- `--max-heap-size` は spawn の引数として直接渡されるため、NODE_OPTIONS の制約を回避
+- migration コマンドは `apps/app/package.json` の `migrate` スクリプトの中身を直接記述。package.json の変更時は entrypoint の更新も必要
+- PID 1 の責務: シグナルフォワーディング、子プロセスの reap、正常終了コードの伝播
+
+#### Dockerfile.dockerignore
+
+| Field | Detail |
+|-------|--------|
+| Intent | ビルドコンテキストから不要ファイルを除外 |
+| Requirements | 4.3 |
+
+**Implementation Notes**
+- 現行に追加すべきエントリ: `.git`, `.env*`(production 以外), `*.md`, `test/`, `**/*.spec.*`, `**/*.test.*`, `.vscode/`, `.idea/`
+- 現行維持: `**/node_modules`, `**/coverage`, `**/Dockerfile`, `**/*.dockerignore`, `**/.pnpm-store`, `**/.next`, `**/.turbo`, `out`, `apps/slackbot-proxy`
+
+## Error Handling
+
+### Error Strategy
+
+entrypoint は try-catch で各フェーズのエラーを捕捉。致命的エラーは `process.exit(1)` でコンテナの起動失敗として Docker/k8s に通知。
+
+### Error Categories and Responses
+
+| Error | Category | Response |
+|-------|----------|----------|
+| cgroup ファイル読み取り失敗 | System | `console.warn` で警告し、フラグなし(V8 デフォルト)で続行 |
+| GROWI_HEAP_SIZE が不正値(NaN 等) | User | `console.error` で警告し、フラグなしで続行(コンテナは起動する) |
+| ディレクトリ作成/権限設定失敗 | System | `process.exit(1)` でコンテナ起動失敗。ボリュームマウント設定を確認 |
+| Migration 失敗 | Business Logic | `execFileSync` が例外を throw → `process.exit(1)`。Docker/k8s が再起動 |
+| アプリプロセス異常終了 | System | 子プロセスの exit code を伝播して `process.exit(code)` |
+
+## Testing Strategy
+
+### Unit Tests
+- docker-entrypoint.ts のヒープサイズ算出ロジック: cgroup v2/v1/なし の 3 パターン(TypeScript で型安全にテスト)
+- docker-entrypoint.ts の環境変数組み合わせ: GROWI_HEAP_SIZE + GROWI_OPTIMIZE_MEMORY + GROWI_LITE_MODE
+- docker-entrypoint.ts の chownRecursive ヘルパー: ネストされたディレクトリ構造で正しく再帰 chown されること
+- Node.js 24 の type stripping で docker-entrypoint.ts が直接実行可能なこと
+
+### Integration Tests
+- Docker build が成功し、全 5 ステージが完了すること
+- `GROWI_HEAP_SIZE=250` を設定してコンテナ起動し、node プロセスの `--max-heap-size=250` を確認
+- cgroup memory limit 付きでコンテナ起動し、自動算出の `--max-heap-size` が正しいことを確認
+- migration が正常に実行されること(`execFileSync` 経由)
+
+### E2E Tests
+- `docker compose up` で GROWI + MongoDB が起動し、ブラウザアクセスが可能なこと
+- `FILE_UPLOAD=local` でファイルアップロードが動作すること(/data/uploads の symlink 確認)
+- SIGTERM 送信でコンテナが graceful に停止すること
+
+## Security Considerations
+
+- **DHI ベースイメージ**: CVE 最大 95% 削減、SLSA Build Level 3 の provenance
+- **シェル不要**: runtime に bash/sh/busybox なし。コマンドインジェクションの攻撃ベクターを排除
+- **gosu/setpriv 不要**: Node.js ネイティブの `process.setuid/setgid` で権限ドロップ。追加バイナリの攻撃面なし
+- **非 root 実行**: アプリケーションは node (UID 1000) で実行。root は entrypoint の初期化(mkdir/chown)のみ
+- **DHI レジストリ認証**: CI/CD で `docker login dhi.io` が必要。Docker Hub 認証情報を使用
+
+## Performance & Scalability
+
+- **ビルドキャッシュ**: `turbo prune --docker` により dependency install レイヤーをキャッシュ。ソースコード変更時の再ビルドで依存インストールをスキップ
+- **イメージサイズ**: DHI runtime に追加バイナリなし。node:24-slim 比でベースレイヤーが縮小
+- **メモリ効率**: `--max-heap-size` による total heap 制御で、v24 の trusted_space overhead 問題を回避。マルチテナントでのメモリ圧迫を防止

+ 121 - 0
.kiro/specs/official-docker-image/requirements.md

@@ -0,0 +1,121 @@
+# Requirements Document
+
+## Introduction
+
+GROWI 公式 Docker イメージの Dockerfile (`apps/app/docker/Dockerfile`) および `docker-entrypoint.sh` を、2025-2026 年のベストプラクティスに基づきモダナイズ・最適化する。Node.js 24 をターゲットとし、メモリレポート (`apps/app/tmp/memory-results/REPORT.md`) の知見を反映してメモリ管理を改善する。
+
+### 現状分析の要約
+
+**現行 Dockerfile の構成:**
+- 3 ステージ構成: `base` → `builder` → `release`(node:20-slim ベース)
+- pnpm + turbo によるモノレポビルド、`pnpm deploy` による本番依存抽出
+- gosu を使った root → node ユーザーへの権限ドロップ(entrypoint でディレクトリ作成後)
+- `COPY . .` でコンテキスト全体をビルダーにコピー
+- CMD 内で `npm run migrate` 実行後にアプリ起動
+
+**GROWI 固有の設計意図(維持すべき事項):**
+- 権限ドロップパターン: entrypoint が root 権限で `/data/uploads` や `/tmp/page-bulk-export` を作成・権限設定した後、node ユーザーに降格して実行する必要がある
+- `pnpm deploy --prod`: pnpm モノレポから本番依存のみを抽出するための公式手法
+- tar.gz によるステージ間アーティファクト受け渡し: ビルド成果物を cleanly に release ステージに転送
+- `apps/app/tmp` ディレクトリ: 運用中にファイルが配置されるため本番イメージに必要
+- `--expose_gc` フラグ: バッチ処理(ES rebuild、import 等)で明示的に `gc()` を呼び出すために必要
+- CMD 内の `npm run migrate`: Docker image ユーザーの利便性のため、起動時にマイグレーションを自動実行
+
+**参考資料:**
+- [Future Architect: 2024年版 Dockerfile ベストプラクティス](https://future-architect.github.io/articles/20240726a/)
+- [Snyk: 10 best practices to containerize Node.js](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/)
+- [ByteScrum: Dockerfile Best Practices 2025](https://blog.bytescrum.com/dockerfile-best-practices-2025-secure-fast-and-modern)
+- [OneUptime: Docker Health Check Best Practices 2026](https://oneuptime.com/blog/post/2026-01-30-docker-health-check-best-practices/view)
+- [Docker: Introduction to heredocs in Dockerfiles](https://www.docker.com/blog/introduction-to-heredocs-in-dockerfiles/)
+- [Docker Hardened Images: Node.js 移行ガイド](https://docs.docker.com/dhi/migration/examples/node/)
+- [Docker Hardened Images カタログ: Node.js](https://hub.docker.com/hardened-images/catalog/dhi/node)
+- GROWI メモリ使用量調査レポート (`apps/app/tmp/memory-results/REPORT.md`)
+
+## Requirements
+
+### Requirement 1: ベースイメージとビルド環境のモダナイズ
+
+**Objective:** As an インフラ管理者, I want Dockerfile のベースイメージと構文が最新のベストプラクティスに準拠していること, so that セキュリティパッチの適用・パフォーマンス向上・メンテナンス性の改善が得られる
+
+#### Acceptance Criteria
+
+1. The Dockerfile shall ベースイメージとして Docker Hardened Images(DHI)を使用する。ビルドステージには `dhi.io/node:24-debian13-dev`、リリースステージには `dhi.io/node:24-debian13` を使用する(glibc ベースでパフォーマンス維持、CVE 最大 95% 削減)
+2. The Dockerfile shall syntax ディレクティブを `# syntax=docker/dockerfile:1`(最新安定版を自動追従)に更新する
+3. The Dockerfile shall pnpm のインストールに wget スタンドアロンスクリプト方式を維持する(corepack は Node.js 25 以降で同梱廃止のため不採用)
+4. The Dockerfile shall `pnpm install ---frozen-lockfile`(ダッシュ3つ)の typo を `--frozen-lockfile`(ダッシュ2つ)に修正する
+5. The Dockerfile shall pnpm バージョンのハードコードを避け、`package.json` の `packageManager` フィールドまたはインストールスクリプトの最新版取得を活用する
+
+### Requirement 2: メモリ管理の最適化
+
+**Objective:** As a GROWI 運用者, I want コンテナのメモリ制約に応じて Node.js のヒープサイズが適切に制御されること, so that OOMKilled のリスクが低減し、マルチテナント環境でのメモリ効率が向上する
+
+#### Acceptance Criteria
+
+1. The docker-entrypoint.ts shall `GROWI_HEAP_SIZE` 環境変数が設定されている場合、その値を `--max-heap-size` フラグとして node プロセスに渡す
+2. While `GROWI_HEAP_SIZE` 環境変数が未設定の場合, the docker-entrypoint.ts shall cgroup メモリリミット(v2: `/sys/fs/cgroup/memory.max`、v1: `/sys/fs/cgroup/memory/memory.limit_in_bytes`)を読み取り、その 60% を `--max-heap-size` として自動算出する
+3. While cgroup メモリリミットが検出できない(ベアメタル等)かつ `GROWI_HEAP_SIZE` が未設定の場合, the docker-entrypoint.ts shall `--max-heap-size` フラグを付与せず、V8 のデフォルト動作に委ねる
+4. When `GROWI_OPTIMIZE_MEMORY` 環境変数が `true` に設定された場合, the docker-entrypoint.ts shall `--optimize-for-size` フラグを node プロセスに追加する
+5. When `GROWI_LITE_MODE` 環境変数が `true` に設定された場合, the docker-entrypoint.ts shall `--lite-mode` フラグを node プロセスに追加する(TurboFan 無効化により RSS を v20 同等まで削減。OOMKilled 頻発時の最終手段として使用)
+6. The docker-entrypoint.ts shall `--max-heap-size` を使用し、`--max_old_space_size` は使用しない(Node.js 24 の trusted_space overhead 問題を回避するため)
+7. The docker-entrypoint.ts shall `--max-heap-size` を `NODE_OPTIONS` ではなく node コマンドの直接引数として渡す(Node.js の制約)
+
+### Requirement 3: ビルド効率とキャッシュの最適化
+
+**Objective:** As a 開発者, I want Docker ビルドが高速かつ効率的であること, so that CI/CD パイプラインのビルド時間が短縮され、イメージサイズが最小化される
+
+#### Acceptance Criteria
+
+1. The Dockerfile shall builder ステージで `COPY . .` の代わりに `--mount=type=bind` を使用し、ソースコードをレイヤーに含めない
+2. The Dockerfile shall pnpm store のキャッシュマウント (`--mount=type=cache,target=...`) を維持する
+3. The Dockerfile shall ビルドステージで apt-get のキャッシュマウントを維持する
+4. The Dockerfile shall release ステージで `.next/cache` が含まれないことを保証する
+5. The Dockerfile shall ビルドステージからリリースステージへのアーティファクト転送に `--mount=type=bind,from=builder` パターンを使用する
+
+### Requirement 4: セキュリティ強化
+
+**Objective:** As a セキュリティ担当者, I want Docker イメージがセキュリティベストプラクティスに準拠していること, so that 攻撃面が最小化され、本番環境の安全性が向上する
+
+#### Acceptance Criteria
+
+1. The Dockerfile shall 非 root ユーザー(node)でアプリケーションを実行する(Node.js entrypoint で `process.setuid/setgid` を使用)
+2. The Dockerfile shall release ステージに不要なパッケージ(wget、curl 等のビルドツール)をインストールしない
+3. The Dockerfile shall `.dockerignore` により、`.git`、`node_modules`、テストファイル、シークレットファイル等がビルドコンテキストに含まれないことを保証する
+4. The Dockerfile shall `apt-get install` で `--no-install-recommends` を使用して不要な推奨パッケージのインストールを防ぐ
+5. The Dockerfile shall release ステージのイメージに、ビルド時にのみ必要なツール(turbo、node-gyp、pnpm 等)を含めない
+
+### Requirement 5: 運用性・可観測性の向上
+
+**Objective:** As a 運用担当者, I want Docker イメージに適切なメタデータが設定されていること, so that コンテナオーケストレーターによる管理が容易になる
+
+#### Acceptance Criteria
+
+1. The Dockerfile shall OCI 標準の LABEL アノテーション(`org.opencontainers.image.source`、`org.opencontainers.image.title`、`org.opencontainers.image.description`、`org.opencontainers.image.vendor`)を含める
+2. The Dockerfile shall `EXPOSE 3000` を維持してポートをドキュメント化する
+3. The Dockerfile shall `VOLUME /data` を維持してデータ永続化ポイントをドキュメント化する
+
+### Requirement 6: entrypoint と CMD のリファクタリング
+
+**Objective:** As a 開発者, I want entrypoint スクリプトと CMD が明確で保守しやすい構造であること, so that メモリフラグの動的組み立てや将来の拡張が容易になる
+
+#### Acceptance Criteria
+
+1. The docker-entrypoint.ts shall ヒープサイズ算出ロジック(Requirement 2 の 3 段フォールバック)を含める
+2. The docker-entrypoint.ts shall 算出されたフラグを node コマンドの引数として組み立て、`process.setgid` + `process.setuid` で権限ドロップ後に `child_process.spawn` で実行する
+3. The docker-entrypoint.ts shall `/data/uploads` のディレクトリ作成・シンボリックリンク・権限設定(FILE_UPLOAD=local サポート)を維持する
+4. The docker-entrypoint.ts shall `/tmp/page-bulk-export` のディレクトリ作成・権限設定を維持する
+5. The docker-entrypoint.ts shall マイグレーション実行後にアプリケーションを起動する現行動作を維持する
+6. The docker-entrypoint.ts shall `--expose_gc` フラグを維持する(バッチ処理での明示的 GC 呼び出しに必要)
+7. When `GROWI_HEAP_SIZE`、cgroup 算出値、または各種最適化フラグが設定された場合, the docker-entrypoint.ts shall 適用されたフラグの内容を標準出力にログ出力する
+8. The docker-entrypoint.ts shall TypeScript で記述し、Node.js 24 のネイティブ TypeScript 実行機能(type stripping)で直接実行する
+
+### Requirement 7: 後方互換性
+
+**Objective:** As a 既存の Docker image ユーザー, I want 新しい Dockerfile に移行しても既存の運用が壊れないこと, so that アップグレード時のリスクが最小化される
+
+#### Acceptance Criteria
+
+1. The Docker イメージ shall 環境変数によるアプリケーション設定(`MONGO_URI`、`FILE_UPLOAD` 等)を従来通りサポートする
+2. The Docker イメージ shall `VOLUME /data` を維持し、既存のデータボリュームマウントとの互換性を保つ
+3. The Docker イメージ shall ポート 3000 でリッスンする現行動作を維持する
+4. While メモリ管理の環境変数(`GROWI_HEAP_SIZE`、`GROWI_OPTIMIZE_MEMORY`、`GROWI_LITE_MODE`)が未設定の場合, the Docker イメージ shall 既存の動作(Node.js 24 のデフォルト)と実質的に同等に動作する
+5. The Docker イメージ shall `docker-compose.yml` / `compose.yaml` からの利用パターンを維持する

+ 205 - 0
.kiro/specs/official-docker-image/research.md

@@ -0,0 +1,205 @@
+# Research & Design Decisions
+
+---
+**Purpose**: Discovery findings and design decision rationale for the official Docker image modernization.
+---
+
+## Summary
+- **Feature**: `official-docker-image`
+- **Discovery Scope**: Extension(既存 Dockerfile の大幅な改善)
+- **Key Findings**:
+  - DHI runtime image (`dhi.io/node:24-debian13`) はシェル・パッケージマネージャ・coreutils を含まない極小構成。Node.js entrypoint(TypeScript)を採用し、シェル・追加バイナリ一切不要の構成を実現
+  - `--mount=type=bind` はモノレポのマルチステップビルドでは非実用的。`turbo prune --docker` が Turborepo 公式推奨のDocker最適化手法
+  - gosu は Node.js ネイティブの `process.setuid/setgid` で置き換え。外部バイナリ(gosu/setpriv/busybox)が完全に不要
+  - HEALTHCHECK は不採用(k8s は独自 probe を使用。Docker Compose ユーザーは自前で設定可能)
+  - Node.js 24 は TypeScript ネイティブ実行(type stripping)をサポート。entrypoint を TypeScript で記述可能
+
+## Research Log
+
+### DHI Runtime Image の構成
+
+- **Context**: `dhi.io/node:24-debian13` をリリースステージのベースイメージとして採用する際の制約調査
+- **Sources Consulted**:
+  - [DHI Catalog GitHub](https://github.com/docker-hardened-images/catalog) — `image/node/debian-13/` ディレクトリ
+  - [DHI Documentation](https://docs.docker.com/dhi/)
+  - [DHI Use an Image](https://docs.docker.com/dhi/how-to/use/)
+- **Findings**:
+  - Runtime image のプリインストールパッケージ: `base-files`, `ca-certificates`, `libc6`, `libgomp1`, `libstdc++6`, `netbase`, `tzdata` のみ
+  - **シェルなし**、**apt なし**、**coreutils なし**、**curl/wget なし**
+  - デフォルトユーザー: `node` (UID 1000, GID 1000)
+  - Dev image (`-dev`): `apt`, `bash`, `git`, `util-linux`, `coreutils` 等がプリインストール
+  - 利用可能タグ: `dhi.io/node:24-debian13`, `dhi.io/node:24-debian13-dev`
+  - プラットフォーム: `linux/amd64`, `linux/arm64`
+- **Implications**:
+  - entrypoint を Node.js(TypeScript)で記述することで、シェルも追加バイナリも完全に不要
+  - gosu/setpriv は Node.js ネイティブの `process.setuid/setgid` で代替。外部バイナリのコピーが不要
+  - HEALTHCHECK は不採用(k8s は独自 probe を使用)。curl/Node.js http モジュールによるヘルスチェックは不要
+
+### `--mount=type=bind` のモノレポビルドでの適用性
+
+- **Context**: Requirement 3.1「builder ステージで `COPY . .` の代わりに `--mount=type=bind` を使用」の実現可能性調査
+- **Sources Consulted**:
+  - [Docker Build Cache Optimization](https://docs.docker.com/build/cache/optimize/)
+  - [Dockerfile Reference - RUN --mount](https://docs.docker.com/reference/dockerfile/)
+  - [pnpm Docker Documentation](https://pnpm.io/docker)
+  - [Turborepo Docker Guide](https://turbo.build/repo/docs/handbook/deploying-with-docker)
+- **Findings**:
+  - `--mount=type=bind` は **RUN 命令の実行中のみ有効** で、次の RUN 命令には引き継がれない
+  - モノレポビルドの multi-step プロセス(install → build → deploy)では、各ステップが前のステップの成果物に依存するため、bind mount だけでは実現困難
+  - 全ステップを単一 RUN にまとめることは可能だが、レイヤーキャッシュの利点が失われる
+  - **Turborepo 公式推奨**: `turbo prune --docker` で Docker 用にモノレポを最小化
+    - `out/json/` — dependency install に必要な package.json のみ
+    - `out/pnpm-lock.yaml` — lockfile
+    - `out/full/` — ビルドに必要なソースコード
+  - この方式により `COPY . .` を回避しつつ、レイヤーキャッシュを活用可能
+- **Implications**:
+  - Requirement 3.1 は `--mount=type=bind` ではなく `turbo prune --docker` パターンで実現すべき
+  - 目標(ソースコードのレイヤー最小化・キャッシュ効率向上)は同等に達成可能
+  - **ただし** `turbo prune --docker` の pnpm workspace との互換性は実装時に検証が必要
+
+### gosu の代替手段
+
+- **Context**: DHI runtime image で gosu が利用できないため、代替手段を調査
+- **Sources Consulted**:
+  - [gosu GitHub](https://github.com/tianon/gosu) — 代替ツール一覧
+  - [Debian Packages - gosu in trixie](https://packages.debian.org/trixie/admin/gosu)
+  - [PhotoPrism: Switch from gosu to setpriv](https://github.com/photoprism/photoprism/pull/2730)
+  - [MongoDB Docker: Replace gosu by setpriv](https://github.com/docker-library/mongo/pull/714)
+  - Node.js `process.setuid/setgid` documentation
+- **Findings**:
+  - `setpriv` は `util-linux` の一部で、DHI dev image にプリインストール済み
+  - `gosu node command` → `setpriv --reuid=node --regid=node --init-groups -- command` に置換可能
+  - PhotoPrism、MongoDB 公式 Docker image が gosu → setpriv に移行済み
+  - **Node.js ネイティブ**: `process.setgid(1000)` + `process.setuid(1000)` + `process.initgroups('node', 1000)` で完全に代替可能
+  - Node.js entrypoint を採用する場合、外部バイナリ(gosu/setpriv/busybox)が一切不要
+- **Implications**:
+  - **最終決定**: Node.js ネイティブの `process.setuid/setgid` を採用(setpriv も不要)
+  - gosu/setpriv バイナリのコピーが不要になり、release ステージに追加バイナリなし
+  - DHI runtime の攻撃面最小化をそのまま維持
+
+### HEALTHCHECK の実装方式(不採用)
+
+- **Context**: DHI runtime image に curl がないため、HEALTHCHECK の実装方式を調査
+- **Sources Consulted**:
+  - [Docker Healthchecks in Distroless Node.js](https://www.mattknight.io/blog/docker-healthchecks-in-distroless-node-js)
+  - [Docker Healthchecks: Why Not to Use curl](https://blog.sixeyed.com/docker-healthchecks-why-not-to-use-curl-or-iwr/)
+  - GROWI healthcheck endpoint: `apps/app/src/server/routes/apiv3/healthcheck.ts`
+- **Findings**:
+  - Node.js の `http` モジュールで十分(curl は不要)
+  - GROWI の `/_api/v3/healthcheck` エンドポイントはパラメータなしで `{ status: 'OK' }` を返す
+  - Docker HEALTHCHECK は Docker Compose の `depends_on: service_healthy` 依存順序制御に有用
+  - k8s 環境では独自 probe(liveness/readiness)を使用するため Dockerfile の HEALTHCHECK は不要
+- **Implications**:
+  - **最終決定: 不採用**。k8s は独自 probe を使用し、Docker Compose ユーザーは compose.yaml で自前設定可能
+  - Dockerfile に HEALTHCHECK を含めないことで、シンプルさを維持
+
+### npm run migrate のシェル依存性
+
+- **Context**: CMD 内の `npm run migrate` が shell を必要とするかの調査
+- **Sources Consulted**:
+  - GROWI `apps/app/package.json` の `migrate` スクリプト
+- **Findings**:
+  - `migrate` スクリプトの実態: `node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js`
+  - `npm run` は内部で `sh -c` を使用するため、shell が必要
+  - 代替: スクリプトの中身を直接 node で実行すれば npm/sh は不要
+  - ただし、npm run を使用する方が保守性が高い(package.json の変更に追従可能)
+- **Implications**:
+  - **最終決定**: Node.js entrypoint で `child_process.execFileSync` を使用し、migration コマンドを直接実行(npm run 不使用、シェル不要)
+  - package.json の `migrate` スクリプトの中身を entrypoint 内で直接記述する方式を採用
+  - package.json の変更時は entrypoint の更新も必要だが、DHI runtime の完全シェルレスを優先
+
+### Node.js 24 TypeScript ネイティブ実行
+
+- **Context**: entrypoint を TypeScript で記述する場合、Node.js 24 のネイティブ TypeScript 実行機能を利用可能か調査
+- **Sources Consulted**:
+  - [Node.js 23 Release Notes](https://nodejs.org/en/blog/release/v23.0.0) — `--experimental-strip-types` が unflag
+  - [Node.js Type Stripping Documentation](https://nodejs.org/docs/latest/api/typescript.html)
+- **Findings**:
+  - Node.js 23 から type stripping がデフォルト有効(`--experimental-strip-types` フラグ不要)
+  - Node.js 24 では安定機能として利用可能
+  - **制約**: enum、namespace 等の「非 erasable syntax」は使用不可。`--experimental-transform-types` が必要
+  - interface、type alias、type annotation(`: string`、`: number` 等)は問題なく使用可能
+  - `ENTRYPOINT ["node", "docker-entrypoint.ts"]` で直接実行可能
+- **Implications**:
+  - entrypoint を TypeScript で記述し、型安全な実装が可能
+  - enum は使用せず、union type (`type Foo = 'a' | 'b'`) で代替
+  - tsconfig.json は不要(type stripping は独立動作)
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| DHI runtime + busybox-static | busybox-static をコピーして sh/coreutils を提供 | 最小限の追加(~1MB)で全機能動作 | DHI 採用の本来の意図(攻撃面最小化)と矛盾。追加バイナリは攻撃ベクター | 却下 |
+| DHI runtime + bash/coreutils コピー | dev stage から bash と各種バイナリを個別コピー | bash の全機能が使える | 共有ライブラリ依存が複雑、コピー対象が多い | 却下 |
+| DHI dev image を runtime に使用 | dev image をそのまま本番利用 | 設定変更最小 | apt/git 等が含まれ攻撃面が増大、DHI の意味が薄れる | 却下 |
+| Node.js entrypoint(TypeScript、シェルレス) | entrypoint を TypeScript で記述。Node.js 24 のネイティブ TypeScript 実行で動作 | 完全にシェル不要、DHI runtime の攻撃面をそのまま維持、型安全 | migration コマンドを直接記述(npm run 不使用)、package.json 変更時に更新必要 | **採用** |
+
+## Design Decisions
+
+### Decision: Node.js TypeScript entrypoint(シェル完全不要)
+
+- **Context**: DHI runtime image にはシェルも coreutils も含まれない。busybox-static のコピーは DHI 採用の意図(攻撃面最小化)と矛盾する
+- **Alternatives Considered**:
+  1. busybox-static をコピーして shell + coreutils を提供 — DHI の攻撃面最小化と矛盾
+  2. bash + coreutils を個別コピー — 依存関係が複雑
+  3. Node.js TypeScript entrypoint — `fs`、`child_process`、`process.setuid/setgid` で全て完結
+- **Selected Approach**: entrypoint を TypeScript で記述(`docker-entrypoint.ts`)。Node.js 24 のネイティブ TypeScript 実行(type stripping)で直接実行
+- **Rationale**: DHI runtime に追加バイナリ一切不要。fs module でディレクトリ操作、process.setuid/setgid で権限ドロップ、execFileSync で migration、spawn でアプリ起動。型安全による保守性向上
+- **Trade-offs**: migration コマンドを直接記述(npm run 不使用)。package.json の migrate スクリプト変更時に entrypoint の更新も必要
+- **Follow-up**: Node.js 24 の type stripping が entrypoint の import 文なしの単一ファイルで正常動作することを検証
+
+### Decision: Node.js ネイティブの process.setuid/setgid による権限ドロップ
+
+- **Context**: gosu は DHI runtime にインストールできない。busybox-static/setpriv も不採用(追加バイナリ排除方針)
+- **Alternatives Considered**:
+  1. gosu バイナリをコピー — 動作するが、業界トレンドに逆行
+  2. setpriv バイナリをコピー — 動作するが、追加バイナリ排除方針に反する
+  3. Node.js `process.setuid/setgid` — Node.js の標準 API
+  4. Docker `--user` フラグ — entrypoint の動的処理に対応できない
+- **Selected Approach**: `process.initgroups('node', 1000)` + `process.setgid(1000)` + `process.setuid(1000)` で権限ドロップ
+- **Rationale**: 外部バイナリ完全不要。Node.js entrypoint 内で直接呼び出し可能。setgid → setuid の順序で安全に権限ドロップ
+- **Trade-offs**: entrypoint が Node.js プロセスとして root で起動し、アプリもその子プロセスとなる(gosu のような exec ではない)。ただし spawn でアプリプロセスを分離し、シグナルフォワーディングで PID 1 の責務を果たす
+- **Follow-up**: なし
+
+### Decision: turbo prune --docker パターン
+
+- **Context**: Requirement 3.1 で `COPY . .` の廃止が求められているが、`--mount=type=bind` はモノレポビルドで非実用的
+- **Alternatives Considered**:
+  1. `--mount=type=bind` — RUN 間で永続化しないため multi-step ビルドに不向き
+  2. 単一 RUN に全ステップをまとめる — キャッシュ効率が悪い
+  3. `turbo prune --docker` — Turborepo 公式推奨
+- **Selected Approach**: `turbo prune --docker` で Docker 用にモノレポを最小化し、最適化された COPY パターンを使用
+- **Rationale**: Turborepo 公式推奨。dependency install と source copy を分離してレイヤーキャッシュを最大活用。`COPY . .` を排除しつつ実用的
+- **Trade-offs**: ビルドステージが 1 つ増える(pruner ステージ)が、キャッシュ効率の改善で相殺
+- **Follow-up**: `turbo prune --docker` の pnpm workspace 互換性を実装時に検証
+
+### Decision: spawn 引数によるフラグ注入
+
+- **Context**: `--max-heap-size` は `NODE_OPTIONS` では使用不可。node コマンドの直接引数として渡す必要がある
+- **Alternatives Considered**:
+  1. 環境変数 `GROWI_NODE_FLAGS` を export し、CMD 内の shell 変数展開で注入 — shell が必要
+  2. entrypoint 内で CMD 文字列を sed で書き換え — fragile
+  3. Node.js entrypoint で `child_process.spawn` の引数として直接渡す — シェル不要
+- **Selected Approach**: entrypoint 内でフラグ配列を組み立て、`spawn(process.execPath, [...nodeFlags, ...appArgs])` で直接渡す
+- **Rationale**: シェル変数展開不要。配列として直接渡すためシェルインジェクションのリスクゼロ。Node.js entrypoint との自然な統合
+- **Trade-offs**: CMD が不要になる(entrypoint が全ての起動処理を行う)。docker run でのコマンド上書きが entrypoint 内のロジックには影響しない
+- **Follow-up**: なし
+
+## Risks & Mitigations
+
+- **Node.js 24 TypeScript ネイティブ実行の安定性**: type stripping は Node.js 23 で unflag 済み。Node.js 24 では安定機能。ただし enum 等の非 erasable syntax は使用不可 → interface/type のみ使用
+- **migration コマンドの直接記述**: package.json の `migrate` スクリプトを entrypoint 内に直接記述するため、変更時に同期が必要 → 実装時にコメントで明記
+- **turbo prune の pnpm workspace 互換性**: 実装時に検証。非互換の場合は最適化された COPY パターンにフォールバック
+- **process.setuid/setgid の制限**: supplementary groups の初期化に `process.initgroups` が必要。setgid → setuid の順序厳守
+- **DHI イメージの docker login 要件**: CI/CD で `docker login dhi.io` が必要。認証情報管理のセキュリティ考慮が必要
+
+## References
+
+- [Docker Hardened Images Documentation](https://docs.docker.com/dhi/) — DHI の全体像と利用方法
+- [DHI Catalog GitHub](https://github.com/docker-hardened-images/catalog) — イメージ定義とタグ一覧
+- [Turborepo Docker Guide](https://turbo.build/repo/docs/handbook/deploying-with-docker) — turbo prune --docker パターン
+- [pnpm Docker Documentation](https://pnpm.io/docker) — pnpm のDockerビルド推奨
+- [Future Architect: 2024年版 Dockerfile ベストプラクティス](https://future-architect.github.io/articles/20240726a/) — モダンな Dockerfile 構文
+- [MongoDB Docker: gosu → setpriv](https://github.com/docker-library/mongo/pull/714) — setpriv 移行の先行事例
+- [Docker Healthchecks in Distroless](https://www.mattknight.io/blog/docker-healthchecks-in-distroless-node-js) — curl なしのヘルスチェック
+- GROWI メモリ使用量調査レポート (`apps/app/tmp/memory-results/REPORT.md`) — ヒープサイズ制御の根拠

+ 22 - 0
.kiro/specs/official-docker-image/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "official-docker-image",
+  "created_at": "2026-02-20T00:00:00.000Z",
+  "updated_at": "2026-02-20T00:00:00.000Z",
+  "language": "ja",
+  "phase": "tasks-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

+ 143 - 0
.kiro/specs/official-docker-image/tasks.md

@@ -0,0 +1,143 @@
+# Implementation Plan
+
+> **タスク順序の設計方針**:
+> - **Phase 1(本フェーズ)**: DHI ベースイメージ + TypeScript entrypoint で、現行と同一仕様のイメージを再現する。ビルドパイプライン(`COPY . .` による 3 ステージ構成)は現行を維持し、**runtime の安全な移行を優先**する。
+> - **Phase 2(次フェーズ)**: `turbo prune --docker` パターンの導入によるビルド最適化。Phase 1 で runtime が安定してから実施する。pruner/deps ステージの追加で 5 ステージ化。
+>
+> **実装ディレクトリ**: `apps/app/docker-new/` に新規作成する。現行の `apps/app/docker/` は一切変更しない。並行して比較・検証可能な状態を維持する。
+>
+> ディレクトリ権限周りは最優先で実装・テストし、デグレを早期に検出する。entrypoint(TypeScript)と Dockerfile は独立したファイルのため、一部タスクは並行実行可能。
+
+## Phase 1: DHI + TypeScript entrypoint(現行ビルドパターン維持)
+
+- [ ] 1. (P) ビルドコンテキストフィルタの強化
+  - 現行の除外ルールに `.git`、`.env*`(production 以外)、テストファイル、IDE 設定ファイル等を追加する
+  - セキュリティ上の機密ファイル(シークレット、認証情報)がコンテキストに含まれないことを確認する
+  - 現行の除外ルール(`node_modules`、`.next`、`.turbo`、`apps/slackbot-proxy` 等)は維持する
+  - _Requirements: 4.3_
+
+- [ ] 2. TypeScript entrypoint のディレクトリ初期化と権限管理
+- [ ] 2.1 (P) entrypoint スケルトンと再帰 chown ヘルパーの作成
+  - Node.js 24 の type stripping で直接実行可能な TypeScript ファイルを新規作成する(enum 不使用、erasable syntax のみ)
+  - メインの実行フローを `main()` 関数として構造化し、エラーハンドリングのトップレベル try-catch を設ける
+  - ディレクトリ内のファイル・サブディレクトリを再帰的に所有者変更するヘルパー関数を実装する
+  - ヘルパー関数のユニットテストを作成する(ネストされたディレクトリ構造での再帰動作を検証)
+  - _Requirements: 6.8_
+
+- [ ] 2.2 ディレクトリ初期化処理の実装
+  - `/data/uploads` の作成、`./public/uploads` へのシンボリックリンク作成、再帰的な所有者変更を実装する
+  - `/tmp/page-bulk-export` の作成、再帰的な所有者変更、パーミッション 700 の設定を実装する
+  - 冪等性を確保する(`recursive: true` による mkdir、既存シンボリックリンクの重複作成防止)
+  - **現行 `docker-entrypoint.sh` と同一の振る舞い**を保証するユニットテストを作成する(fs モック使用、ディレクトリ・シンボリックリンク・所有者・パーミッションの各状態を検証)
+  - 失敗時(ボリュームマウント未設定等)にプロセス終了(exit code 1)することを検証する
+  - _Requirements: 6.3, 6.4_
+
+- [ ] 2.3 権限ドロップの実装
+  - root から node ユーザー(UID 1000, GID 1000)への降格処理を実装する
+  - supplementary groups の初期化を行い、setgid → setuid の順序を厳守する(逆順だと setgid が失敗する)
+  - 権限ドロップ失敗時にエラーメッセージを出力してプロセスを終了する
+  - _Requirements: 4.1, 6.2_
+
+- [ ] 3. ヒープサイズ算出とノードフラグ組み立て
+- [ ] 3.1 (P) cgroup メモリリミット検出の実装
+  - cgroup v2 ファイルの読み取りと数値パースを実装する(`"max"` 文字列は unlimited として扱う)
+  - cgroup v1 ファイルへのフォールバックを実装する(64GB 超は unlimited として扱う)
+  - メモリリミットの 60% をヒープサイズ(MB 単位)として算出する
+  - ファイル読み取り失敗時は警告ログを出力し、フラグなし(V8 デフォルト)で続行する
+  - 各パターン(v2 正常検出、v2 unlimited、v1 フォールバック、v1 unlimited、検出不可)のユニットテストを作成する
+  - _Requirements: 2.2, 2.3_
+
+- [ ] 3.2 (P) 環境変数によるヒープサイズ指定の実装
+  - `GROWI_HEAP_SIZE` 環境変数のパースとバリデーションを実装する(正の整数、MB 単位)
+  - 不正値(NaN、負数、空文字列)の場合は警告ログを出力してフラグなしにフォールバックする
+  - 環境変数指定が cgroup 自動算出より優先されることをテストで確認する
+  - _Requirements: 2.1_
+
+- [ ] 3.3 ノードフラグの組み立てとログ出力の実装
+  - 3 段フォールバック(環境変数 → cgroup 算出 → V8 デフォルト)の統合ロジックを実装する
+  - `--expose_gc` フラグを常時付与する
+  - `GROWI_OPTIMIZE_MEMORY=true` で `--optimize-for-size`、`GROWI_LITE_MODE=true` で `--lite-mode` を追加する
+  - `--max-heap-size` を spawn 引数として直接渡す構造にする(`--max_old_space_size` は不使用、`NODE_OPTIONS` には含めない)
+  - 適用されたフラグの内容を標準出力にログ出力する(どの段で決定されたかを含む)
+  - 環境変数の各組み合わせパターン(全未設定、HEAP_SIZE のみ、全有効等)のユニットテストを作成する
+  - _Requirements: 2.4, 2.5, 2.6, 2.7, 6.1, 6.6, 6.7_
+
+- [ ] 4. マイグレーション実行とアプリプロセス管理
+- [ ] 4.1 マイグレーションの直接実行
+  - node バイナリを直接呼び出して migrate-mongo を実行する(npm run を使用しない、シェルを介さない)
+  - 標準入出力を inherit して migration のログを表示する
+  - migration 失敗時は例外をキャッチしてプロセスを終了し、コンテナオーケストレーターによる再起動を促す
+  - _Requirements: 6.5_
+
+- [ ] 4.2 アプリプロセスの起動とシグナル管理
+  - 算出済みノードフラグを引数に含めた子プロセスとしてアプリケーションを起動する
+  - SIGTERM、SIGINT、SIGHUP を子プロセスにフォワードする
+  - 子プロセスの終了コード(またはシグナル)を entrypoint の終了コードとして伝播する
+  - PID 1 としての責務(シグナルフォワーディング、子プロセス reap、graceful shutdown)を検証するテストを作成する
+  - _Requirements: 6.2, 6.5_
+
+- [ ] 5. Dockerfile の再構築(現行 3 ステージパターン + DHI)
+- [ ] 5.1 (P) base ステージの構築
+  - DHI dev イメージをベースに設定し、syntax ディレクティブを最新安定版自動追従に更新する
+  - wget スタンドアロンスクリプトで pnpm をインストールする(バージョンのハードコードを排除する)
+  - turbo をグローバルにインストールする
+  - ビルドに必要なパッケージを `--no-install-recommends` 付きでインストールし、apt キャッシュマウントを適用する
+  - _Requirements: 1.1, 1.2, 1.3, 1.5, 3.3, 4.4_
+
+- [ ] 5.2 builder ステージの構築
+  - 現行の `COPY . .` パターンを維持してモノレポ全体をコピーし、依存インストール・ビルド・本番依存抽出を行う
+  - `--frozen-lockfile` の typo(ダッシュ3つ → 2つ)を修正する
+  - pnpm store のキャッシュマウントを設定してリビルド時間を短縮する
+  - 本番依存のみを抽出し、tar.gz にパッケージングする(`apps/app/tmp` ディレクトリを含む)
+  - `.next/cache` がアーティファクトに含まれないことを保証する
+  - _Requirements: 1.4, 3.2, 3.4_
+
+- [ ] 5.3 release ステージの構築
+  - DHI ランタイムイメージをベースに設定し、追加バイナリのコピーを一切行わない
+  - ビルドステージのアーティファクトをバインドマウント経由で展開する
+  - TypeScript entrypoint ファイルを COPY し、ENTRYPOINT に node 経由の直接実行を設定する
+  - リリースステージにビルドツール(turbo、pnpm、node-gyp 等)やビルド用パッケージ(wget、curl 等)が含まれないことを確認する
+  - _Requirements: 1.1, 3.5, 4.2, 4.5_
+
+- [ ] 5.4 (P) OCI ラベルとポート・ボリューム宣言の設定
+  - OCI 標準ラベル(source、title、description、vendor)を設定する
+  - `EXPOSE 3000` と `VOLUME /data` を維持する
+  - _Requirements: 5.1, 5.2, 5.3_
+
+- [ ] 6. 統合検証と後方互換性の確認
+- [ ] 6.1 Docker ビルドの E2E 検証
+  - 3 ステージ全てが正常完了する Docker ビルドを実行し、ビルドエラーがないことを確認する
+  - リリースイメージにシェル、apt、ビルドツールが含まれていないことを確認する
+  - _Requirements: 1.1, 4.2, 4.5_
+
+- [ ] 6.2 ランタイム動作と後方互換性の検証
+  - 環境変数(`MONGO_URI`、`FILE_UPLOAD` 等)が従来通りアプリケーションに透過されることを確認する
+  - `/data` ボリュームマウントとの互換性およびファイルアップロード動作を確認する
+  - ポート 3000 でのリッスン動作を確認する
+  - メモリ管理環境変数が未設定の場合に V8 デフォルト動作となることを確認する
+  - `docker compose up` での起動と SIGTERM による graceful shutdown を確認する
+  - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_
+
+## Phase 2: turbo prune --docker ビルド最適化(次フェーズ)
+
+> Phase 1 で runtime が安定した後に実施する。現行の `COPY . .` + 3 ステージ構成を `turbo prune --docker` + 5 ステージ構成に移行し、ビルドキャッシュ効率を向上させる。
+
+- [ ] 7. turbo prune --docker パターンの導入
+- [ ] 7.1 pruner ステージの新設
+  - base ステージの直後に pruner ステージを追加し、`turbo prune @growi/app --docker` でモノレポを Docker 用に最小化する
+  - pnpm workspace との互換性を検証する(非互換の場合は Phase 1 の `COPY . .` パターンを維持)
+  - 出力(json ディレクトリ、lockfile、full ディレクトリ)が正しく生成されることを確認する
+  - _Requirements: 3.1_
+
+- [ ] 7.2 deps ステージの分離と builder の再構成
+  - builder ステージから依存インストールを分離し、deps ステージとして独立させる
+  - pruner の出力から package.json 群と lockfile のみをコピーして依存をインストールする(レイヤーキャッシュ効率化)
+  - builder ステージは deps をベースにソースコードをコピーしてビルドのみを行う構成に変更する
+  - 依存変更なし・ソースコードのみ変更の場合に、依存インストールレイヤーがキャッシュされることを検証する
+  - _Requirements: 3.1, 3.2_
+
+- [ ] 7.3 5 ステージ構成の統合検証
+  - base → pruner → deps → builder → release の 5 ステージ全てが正常完了することを確認する
+  - Phase 1 の 3 ステージ構成と同等の runtime 動作を維持していることを確認する
+  - ビルドキャッシュの効率改善(ソースコード変更時に依存インストールがスキップされること)を検証する
+  - _Requirements: 3.1, 3.2, 3.4_