第30章:最終成果:安全デフォルト・テンプレ完成🎉📦 + 自己点検チェック✅
この章は 「次の新規PJで毎回コピペして使える、安全寄りテンプレ」を完成させる回だよ〜!😄✨ ゴールは2つ👇
- ① 安全デフォルトのテンプレ一式(Compose / Dockerfile / 運用ルール)を完成🎁
- ② 5分セルフ監査で、やらかし(秘密漏れ・踏み台・誤爆)を毎回潰す🕔🔍✅
30.1 完成イメージ(“被害半径”が小さい構造)🗺️🔒
ざっくり、こういう形に固定します👇
- 公開するのは 入口(app)だけ🚪
- DB/Redisは 内部ネットワークに閉じ込め🍱
- 秘密は env直書き禁止 → secretsでファイル注入🔑
- ビルド時の秘密も BuildKit secretsで一時注入🏗️🤫
- AI拡張は 「見せていい範囲」をテンプレ側で作る🤖🧱
(Docker Composeのsecretsは /run/secrets/<secret_name> にファイルとしてマウントされ、サービス単位で明示的に渡したものだけが見える設計だよ📄🔐)(Docker Documentation)
30.2 テンプレのフォルダ構成📁✨
こんな構成で固定しちゃうのがラクです👇
compose.yaml(本命テンプレ)Dockerfile(本命テンプレ).dockerignore(秘密やゴミをビルドに入れない).env.example(秘密じゃない設定だけ)secrets/(ローカル専用。必ずgitignore対象)🔒docs/SECURITY_CHECK.md(5分監査チェック表📝)
30.3 安全デフォルトの compose.yaml(コピペして育てる🌱📦)
ポイントは 「公開・共有・権限・秘密・AI」 が最小になってること!✂️🔐🤖
services:
app:
build:
context: .
secrets:
- npm_token # ビルド時だけ使う(例:private npm)
ports:
# 入口だけ公開。しかも localhost に縛る(LANへ撒かない)🧯
- "127.0.0.1:3000:3000"
environment:
NODE_ENV: production
DB_HOST: db
DB_USER: app
DB_NAME: appdb
# 秘密は「値」じゃなく「ファイルの場所」を渡す📄🔐
DB_PASSWORD_FILE: /run/secrets/db_password
SESSION_KEY_FILE: /run/secrets/session_key
secrets:
- db_password
- session_key
depends_on:
db:
condition: service_healthy
networks:
- front
- back
# ===== 権限を削る(安全寄りの標準装備)🛡️ =====
user: "node" # rootで動かさない🙂
read_only: true # ルートFSは基本読取専用📖
tmpfs:
- /tmp # 書ける場所はここに寄せる🧊
security_opt:
- no-new-privileges:true # 途中で権限が増えない🧱
cap_drop:
- ALL # 権限は全部落としてスタート✂️
init: true # ゾンビプロセス対策(地味に大事)🧹
# 書き込みが必要な場所だけ「専用」にする✅
volumes:
- app_uploads:/app/uploads
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_DB: appdb
# postgres公式イメージは *_FILE で secret ファイルを読める慣習があるよ🔐
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks:
- back
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7-alpine
networks:
- back
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
# ===== AIや自動化で「コマンド実行させたい」時の避難所(任意)🤖🧯 =====
tools:
profiles: ["tools"]
image: node:22-alpine
working_dir: /work
# リポジトリは「読取専用」で渡す(壊せない)📎🔒
volumes:
- ./:/work:ro
- tools_tmp:/tmp
read_only: true
tmpfs:
- /tmp
network_mode: "none" # 外に出られない(流出しにくい)🚫🌐
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: "node"
command: ["node", "--version"]
secrets:
db_password:
file: ./secrets/db_password.txt
session_key:
file: ./secrets/session_key.txt
# ビルド時だけ使うsecret:環境変数から注入(例)🏗️🤫
npm_token:
environment: NPM_TOKEN
networks:
front: {}
back:
internal: true # 外部と遮断したネットワークにできる🔒
volumes:
pgdata: {}
redisdata: {}
app_uploads: {}
tools_tmp: {}
- secretsが
/run/secrets/...にファイルで入ること、env直入れより漏れにくい理由(ログに出たりしやすい等)は公式が明言してるよ📚(Docker Documentation) internal: trueは 外部接続を遮断したネットワークを作れる(=閉じ込めに使える)よ🔒(Docker Documentation)build: secrets:で ビルド時だけ secret を使う書き方も公式の例があるよ🏗️(Docker Documentation)
30.4 Dockerfile(BuildKit secretsで“ビルド中だけ見える”🏗️🤫)
BuildKitのsecret mountは「ビルド命令の間だけ一時的に見える」設計。private依存を取る時に便利!(Docker Documentation)
## syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
## 依存関係だけ先にコピー(キャッシュ効かせる)⚡
COPY package*.json ./
## 例:private npm を使う場合だけ token を build secret で渡す
## secret は /run/secrets/<id> にデフォルトでマウントされる📄
## (ビルド命令の間だけ一時的に利用できる):contentReference[oaicite:5]{index=5}
RUN --mount=type=secret,id=npm_token \
sh -lc 'if [ -f /run/secrets/npm_token ]; then \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc; \
fi; \
npm ci; \
rm -f .npmrc'
COPY . .
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
## 実行に必要なものだけ持ってくる(小さく・安全に)📦
COPY --from=deps /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
30.5 TypeScript側:secretを読む“最小パターン”🔑📄
「DBパスワード文字列」じゃなくて、“ファイルパス”を環境変数で受けて読むのがコツ!🙂
import { readFile } from "node:fs/promises";
async function readSecret(pathEnv: string): Promise<string> {
const p = process.env[pathEnv];
if (!p) throw new Error(`Missing env: ${pathEnv}`);
return (await readFile(p, "utf8")).trim();
}
export const secrets = {
dbPassword: await readSecret("DB_PASSWORD_FILE"),
sessionKey: await readSecret("SESSION_KEY_FILE"),
};
30.6 VS Codeでの作業手順(最短)🧑💻✨
secrets/を作って、以下2ファイルを作成✍️
secrets/db_password.txtsecrets/session_key.txt
.dockerignoreと.gitignoreを用意🧯
secrets/
.env
secrets/
.env
node_modules
dist
.git
- 起動(ターミナルで)🚀
docker compose up -d --build
- 動作確認👀
docker compose ps
docker compose logs -f app
30.7 5分セルフ監査(毎回これだけ✅🕔)
チェックは Yesが並んだら勝ち🏆✨
A. 権限(Privilege)🧤✂️
-
privileged: trueが無い -
docker.sockをマウントしてない -
user: rootになってない(USER/user:がある) -
cap_drop: [ALL]+no-new-privileges:trueが入ってる -
read_only: trueになってる(書く場所は volume/tmpfs に分離)
B. 共有(Share / Mount)📎🗂️
- bind mount(
.:/work等)を“必要最小限”にした - できる場所は
:ro(読取専用)にした - 書き込みは「専用volume」だけ(
/app/uploads等)
C. 公開(Expose)🚪🌐
-
ports:は 入口の1つだけ -
127.0.0.1:でローカルに縛ってる - DB/Redisに
ports:が無い - 内部ネットワーク
internal: trueを使って閉じ込めてる(Docker Documentation)
D. 秘密(Secrets)🔑🫣
- パスワード/APIキーを
environment:に直書きしてない -
/run/secrets/...で受けてる(サービス単位で付与)(Docker Documentation) - ビルド時の秘密も
build: secrets:/ BuildKit secret mount に寄せてる(Docker Documentation) -
secrets/はgitに入ってない(絶対)🚫
E. AI(Prompt Injection / AI拡張)🤖⚠️
-
AIに見せる範囲から
secrets/を外してる(“見える場所”を減らす) -
外部のIssue/PR/README等の文章は “信用しない”(混入指示があり得る)
-
AIがコマンド実行する系は、できれば
toolsサービス(ネット無し・読取専用)でやる- 間接プロンプト注入で トークン露出・機密ファイル露出・任意コード実行に繋がり得る、という指摘があるよ🧨(The GitHub Blog)
- “サンドボックス(例:Dockerコンテナ等)”を防御層にする話も出てる🧱(The GitHub Blog)
30.8 演習(テンプレを“自分のもの”にする💪😆)
演習1:DBが外から見えないのを確認🔍
- いまの構成だとDBは
ports:なし=外部から直アクセスしづらい👍 - わざと
dbにports: "5432:5432"を追加して、何が起きるかを体験→すぐ戻す🧯
演習2:secretsが「必要なサービスだけ見える」を体験🔐
redisにdb_passwordを渡してない状態でOK- 逆に渡しちゃうと「被害半径が増える」感覚を掴む🗺️
演習3:AI用避難所(tools)を使ってみる🤖🧯
toolsはネット無し&読取専用なので、AIが提案したコマンドを“安全寄り”に試せる
docker compose --profile tools run --rm tools sh
30.9 よくある詰まりポイント(ここで沼る😵💫)
- read_onlyにしたら落ちる → アプリが書く場所(uploads, tmp, cache)を洗い出して、volume/tmpfsに逃がす✅
- DB接続でパスワードが空っぽ
→
DB_PASSWORD_FILEのパスが合ってるか、secretがappサービスに付与されてるか確認👀 - private npmが取れない
→
NPM_TOKENが環境変数としてセットされてるか、build: secrets:が効いてるか確認🔍(Docker Documentation)
ここまでできたら、もう 新規PJはこのテンプレからしか始めないでOK!🎉📦 次の章(もし作るなら)は、このテンプレを **「Next.js版」「API+Worker版」「DBなし版」**みたいに“派生テンプレ集”にしていくと最強だよ😆✨