Docker ドキュメントにあるDocker Compose をはじめよう(Flask フレームワーク + Redis でアクセスカウンターを作るサンプル)は、学習用としてとても分かりやすい構成です。
しかしそのまま本番環境で使うと、セキュリティや安定性に課題があります。
build時のWARNINGメッセージ
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
そこで今回は、このサンプルを元に「ハードニング(セキュリティ強化)版」を作る方法を解説します。
初心者でもステップごとに理解できるように説明します。
Docker Compose をはじめようの元構成と改善ポイント
ハードニング(Hardening) とは
Dockerにおける ハードニング(Hardening) とは、Dockerコンテナやホスト環境をより安全にし、外部からの攻撃に強くするための一連のセキュリティ対策を指します。
目的は、システムに潜む脆弱性をできるだけ減らし、攻撃者が侵入や悪用できる余地(攻撃対象領域 / Attack Surface)を最小限に抑えることです。
元の構成
- Flask(Python 製の軽量フレームワーク)
- Redis(インメモリ KVS、ここではカウンター管理に使用)
- Docker Compose(複数コンテナをまとめて管理)
オリジナルのサンプルはシンプルですが、以下のような弱点があります。
- Flask の開発用サーバーをそのまま使っている(本番非推奨)
- Redis に認証がない(誰でもアクセスできる)
- root 権限で動作している
- 不要なキャッシュやファイルを含んだまま
改善(ハードニング)のポイント
今回のハードニング版では次の点を改善します。
- Flask 開発サーバーを Gunicorn (WSGI) に変更
→ 本番環境でも安心して使える。 - 非 root ユーザーで動作
→ 万が一突破されても被害を最小化。 - Redis にパスワード認証を追加
→ 不正アクセスを防止。 - read-only ファイルシステムとリソース制限
→ 攻撃・暴走対策。 - 不要ファイルを削除(pycache / .git など)
→ イメージを軽量化 & セキュリティ向上。 .envファイルで秘密情報を外出し
→ パスワードをコードに直接書かない。
元の構成 vs ハードニング構成

ハードニング作業
ディレクトリ構成
flask-redis-secure/
├─ app/
│ ├─ app.py
│ ├─ gunicorn.conf.py
│ ├─ requirements.txt
│ └─ __init__.py
├─ Dockerfile
├─ docker-compose.yml
├─ .dockerignore
├─ .env.example
└─ README.md
アプリコード(Flask + Redis + /health)
#app/app.py
from flask import Flask
import os
import redis
app = Flask(__name__)
redis_host = os.getenv("REDIS_HOST", "redis")
redis_port = int(os.getenv("REDIS_PORT", "6379"))
redis_password = os.getenv("REDIS_PASSWORD", None)
r = redis.Redis(host=redis_host, port=redis_port, password=redis_password, db=0, decode_responses=True)
@app.route("/")
def index():
try:
count = r.incr("hits")
except Exception:
count = "unavailable"
return f"Hello from Flask! This page has been viewed {count} times.\n"
@app.route("/health")
def health():
try:
r.ping()
return "OK", 200
except Exception:
return "NG", 503
Gunicorn(WSGIサーバー)設定ファイル
公式チュートリアル(Flask+Redis)は開発向けに Flask の開発サーバー (flask run) を直接使っていますが、セキュリティ・パフォーマンスを考慮してGunicorn (WSGI サーバー)を経由する。
gunicorn.conf.py は その Gunicorn を制御するための外部設定ファイル。
#app/gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 2
threads = 2
timeout = 30
説明
bind = "0.0.0.0:8000" # どのアドレス/ポートで待ち受けるか
workers = 2 # プロセス数 (CPU コア数 x 2+1 が目安)
threads = 2 # 各ワーカー内のスレッド数
timeout = 30 # リクエスト処理のタイムアウト秒数
requirements.txt(Pythonプロジェクトの依存関係を固定)
#app/requirements.txt
Flask==3.0.3
redis==5.0.8
gunicorn==22.0.0
async-timeout>=4.0.3
Flask==3.0.3 → Web フレームワーク本体
redis==5.0.8 → Redis クライアントライブラリ
gunicorn==22.0.0 → WSGI サーバー
async-timeout>=4.0.3 → Redis asyncio 対応に必要な依存(非同期処理のタイムアウト管理)
Dockerfile
#Dockerfile
# build ステージ(Debian/glibc, Python 3.11 に合わせる)
FROM python:3.11-slim AS build
WORKDIR /app
COPY app/requirements.txt .
RUN python -m pip install --upgrade pip \
&& pip install --no-cache-dir --prefix=/opt/python -r requirements.txt
COPY app/ /app/
RUN find /app -type d -name "__pycache__" -prune -exec rm -rf {} +
# run ステージ(Distroless / Debian, Python 3.11)
FROM gcr.io/distroless/python3-debian12
USER 10001:10001
WORKDIR /srv/app
COPY --chown=10001:10001 --from=build /opt/python /opt/python
COPY --chown=10001:10001 --from=build /app /srv/app
# ランタイムの Python 3.11 に合わせる
ENV PYTHONPATH="/opt/python/lib/python3.11/site-packages"
EXPOSE 8000
ENTRYPOINT ["/usr/bin/python3", "-m", "gunicorn", "-c", "gunicorn.conf.py", "app:app"]
#pip を最新化してから依存をインストール。
RUN python -m pip install --upgrade pip \
&& pip install --no-cache-dir --prefix=/opt/python -r requirements.txt
・--prefix=/opt/python
仮想環境(venv)ではなく、site-packages を /opt/python 以下にインストールする。
#コンテナイメージのベースとなるイメージを指定
FROM gcr.io/distroless/python3-debian12
gcr.io:Google Container Registry。Googleが提供するコンテナイメージのリポジトリ。
distroless:Googleが開発・管理している「Distroless」シリーズのイメージ。必要最小限のファイルだけを含む。
#マルチステージビルド
--from=build
マルチステージビルドとは、1つのDockerfile内で複数のFROM命令を使って、コンテナイメージを段階的に作成する手法です。
通常、最初のステージでアプリケーションをビルドし、次のステージでそのビルド成果物だけを最終的なイメージにコピーします。
このプロセスで、--fromオプションは以下の役割を果たします。
ステージ名の設定: 最初のFROM命令にAS buildのように名前を付けます。
成果物のコピー: COPY --from=build ...のように記述することで、ビルドステージで生成された実行ファイルや成果物だけを、最終ステージに持ってきます。
#コンテナの起動
ENTRYPOINT :コンテナ起動時に実行するコマンド。
/usr/bin/python3 -m gunicorn ... :Python モジュールとして Gunicorn を起動。
-c gunicorn.conf.py :Gunicorn 設定ファイルを読み込む。
"app:app":app.py モジュール内の Flask アプリオブジェクト app を起動対象に指定。
docker-compose.yml
#docker-compose.yml
services:
web:
build: .
ports:
- "8000:8000"
environment:
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_PASSWORD: ${REDIS_PASSWORD:?set_in_.env}
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8000/health"]
interval: 30s
retries: 3
read_only: true
tmpfs:
- /tmp:size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
redis:
image: redis:7.4-alpine
command: >
redis-server --requirepass ${REDIS_PASSWORD:?set_in_.env}
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "PING"]
interval: 30s
retries: 5
read_only: true
tmpfs:
- /data:size=64m
・redis が ヘルスチェック成功してから web を起動
・healthcheck:
web コンテナ自身のヘルスチェック設定。
30秒ごとに http://127.0.0.1:8000/health を叩き、3回失敗で unhealthy 扱い。
・read_only: true
コンテナのルートファイルシステムを読み取り専用。
・ tmpfs:
- /tmp:size=16m
一時ファイルは /tmp(tmpfs)にのみ保存可(16MB 制限)。
・no-new-privileges:true:
コンテナ内のプロセスが setuid などで権限昇格できなくなる。
・cap_drop: ALL :
Linux のケーパビリティを全て剥奪(最小権限で実行)
・ redis-server --requirepass ${REDIS_PASSWORD:?set_in_.env}
Redis を パスワード認証必須モードで起動。
.env に設定された REDIS_PASSWORD が無いと起動失敗する。
・expose:
外部公開は web のみ(Redis は expose で内部ネットワーク限定)
ports: を使っていないので、ホストには公開されない(内部限定)
・両コンテナとも read_only ルートFS、必要最小の tmpfs を付与。
・cap-drop=ALL、no-new-privileges、seccomp=runtime/default(デフォルトプロファイル)を明示。
・ヘルスチェックで依存順序・自己診断。
・リソース制限:OOM や暴走を防止。
環境変数
#.env.example
REDIS_PASSWORD=change_me_strong_random
使用時
(1) サンプルから.envを作る
cp .env.example .env
(2) 必要に応じて.envを編集(パスワードやシークレットを設定).env が存在しない場合?set_in_.env} の必須指定 (:?…) を使っていれば エラーで止まる
単なる ${REDIS_PASSWORD} なら 空の値として動作する。
ビルド時にコピーされないファイル・ディレクトリを指定
.dockerignoreで、Docker ビルド時にコピーされないようにするファイル・ディレクトリを指定する。
#.dockerignore
.git
__pycache__/
*.pyc
*.pyo
*.log
.env
■不要なファイルをイメージに含めない
.git/ や tests/、ドキュメントなど、本番で不要なものがコンテナに入らなくなる。
→ イメージのサイズ削減。
■ビルド効率の向上
Docker は COPY や ADD でコピーする対象を一旦 コンテキストにまとめて送ります。
.dockerignore で余計なファイルを除外すると、この転送データが減ってビルドが速くなる。
■セキュリティ向上
誤って .env などの秘密情報ファイルをイメージに含めないようにできる。
これを怠ると、ビルドしたイメージを配布するときに 認証情報が漏洩するリスクがある。
実行手順
サンプルから.envを作成し編集
#サンプルから.envを作成
cp .env.example .env
#.env内のREDIS_PASSWORDを設定
1.パスワード生成
32文字のランダムパスワードを生成
$ openssl rand -base64 32
2.出力を.env の REDIS_PASSWORDに設定する
ビルド
docker compose build --no-cache
起動
docker compose up -d
動作確認
#カウンター更新
http://localhost:8000/
#ヘルスチェック
http://localhost:8000/health
ハードニング状況
Docker Compose をはじめようの元構成からのハードニング状況は、下記の通りです。
- WSGI 本番サーバー:Gunicorn で稼働
- 非 root 実行(UID=10001)
- distroless ランタイム(最小攻撃面)
- read-only ルートFS + tmpfs(Compose 設定済み)
- cap-drop=ALL / no-new-privileges(昇格防止)
- /health 実装 + ヘルスチェック
- Redis を内部ネットワーク限定(
exposeのみ)
まとめ
- 公式チュートリアルのままでは本番利用に不安あり
- Gunicorn・非root・認証・read-only 化などでセキュリティを大幅に向上
.envを使って秘密情報を外出しし、安全に管理できる
これで Flask + Redis サンプルを、「学習用」から「実運用に近い安全な形」へと進化できました。


