PythonのDockerfile入門: 効率的な開発のためのガイド

# 1. はじめに

このガイドでは、基本的なDockerfileの書き方を解説します。Dockerを利用することで、アプリケーションの環境設定が簡単になり、開発から本番環境への移行がスムーズになります。

成果物のリポジトリはこちらです。

https://github.com/sugiyama404/introduce_to_python_dockerfile

## Dockerとは何か？

Dockerは、アプリケーションとその依存関係を単一の軽量で移植可能なコンテナにパッケージングするためのオープンソースプラットフォームです。従来の仮想マシンとは異なり、オペレーティングシステム全体ではなく、アプリケーションに必要なものだけを含んでいるため、迅速かつ効率的な実行が可能です。

Dockerは、開発者、システム管理者、および運用担当者によって、次のようなさまざまな目的に使用されています。

+ **アプリケーションの開発とテスト**: 開発者は、Dockerを使用して、アプリケーションとその依存関係を分離された環境で実行できます。これにより、開発サイクルの効率が向上し、テストが容易になります
+ **アプリケーションのデプロイ**: システム管理者は、Dockerを使用して、アプリケーションを本番環境に迅速かつ簡単にデプロイできます
+ **アプリケーションの運用**: 運用担当者は、Dockerを使用して、アプリケーションをスケーリングし、監視し、トラブルシューティングすることができます

## Dockerfileの役割と利点

Dockerfileは、Dockerイメージを作成するための設計図となるテキストファイルです。Dockerイメージは、コンテナの作成に使用されるテンプレートであり、Dockerfileには、イメージを構築するために必要なすべてのコマンドが記述されています。

Dockerfileを使用する利点は次のとおりです。

+ **再現性**: Dockerfileを使用すると、さまざまな環境で同じDockerイメージを作成できます。これは、アプリケーションのデプロイと更新を容易にするため、非常に重要です
+ **移植性**: Dockerイメージは、オペレーティングシステムに依存しないため、あらゆる環境で実行できます
+ **効率性**: Dockerイメージは軽量で、従来の仮想マシンよりも迅速に起動および実行できます


参考文献

https://www.docker.com/

https://docs.docker.com/reference/dockerfile/


# 2. Dockerfileの基本構文

## FROM: ベースイメージの指定

Dockerfileの最初に記述する命令で、どのベースイメージを使用するかを指定します。これにより、ベースイメージ上に新しいレイヤーを追加して独自のイメージを作成します。

```Dockerfile
FROM python:3.11.9-slim-bookworm
```

## RUN: コマンドの実行

イメージのビルド時に実行されるコマンドを指定します。複数のRUN命令を記述することで、各ステップごとに新しいレイヤーが作成されます。

```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    wget=1.21.3-1+b2 \
    && apt-get -y clean \
    && rm -rf /var/lib/apt/lists/*
```

## COPY: ファイルの複製

ホストマシン上のファイルやディレクトリをイメージ内にコピーします。ビルドコンテキストから指定したパスの内容をコンテナにコピーします。

```Dockerfile
COPY requirements.txt ./
```

## WORKDIR: 作業ディレクトリの指定

RUNやCMD、ENTRYPOINT命令が実行される作業ディレクトリを指定します。指定したディレクトリが存在しない場合は自動的に作成されます。

```Dockerfile
WORKDIR /app
```

## ENV: 環境変数の設定

コンテナ内で使用する環境変数を設定します。これにより、アプリケーションの設定や動作を環境変数で調整できます。

```Dockerfile
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
```

## ARG: 変数定義の設定

Dockerfile内でビルド時に使用する変数を定義するために使用されます。これにより、ビルドプロセス中にカスタマイズ可能なパラメータを指定することができます。

```Dockerfile
ARG VERSION=1.0
```

### ARGとENVの違い

+ ARG: ビルド時にのみ有効な変数。コンテナ実行時には使用不可
+ ENV: ビルド時およびコンテナの実行時に有効な環境変数


## EXPOSE: ポートの公開

コンテナがリスンするネットワークポートを指定します。この命令自体はポートを開くものではなく、情報提供の役割を果たします。

```Dockerfile
EXPOSE 8000
```

コンテナの実行時に実際にポートを公開するには、
docker run -p フラグを使い、公開用のポートと割り当てるポートを指定します。

```console
docker run -d -p 8000:8000 -it apsrver:latest
```


## CMD: デフォルトコマンドの設定

コンテナが起動した際に実行されるデフォルトのコマンドを指定します。複数のCMD命令がある場合、最後に記述したものが有効になります。コマンドライン引数で上書き可能です。

```Dockerfile
CMD ["/app/opt/main.py"]
```

以下の場合、このコマンドを実行すると、コンテナはデフォルトの/app/opt/main.pyを実行する代わりに、/bin/bashシェルを起動します。

```console
docker run -p 8000:8000 -it apsrver:latest /bin/bash
```

## ENTRYPOINT: エントリーポイントの設定

コンテナが起動した際に実行されるコマンドを指定します。ENTRYPOINTで指定されたコマンドは固定されており、コマンドライン引数で上書きされません。

```Dockerfile
ENTRYPOINT ["./docker-entrypoint"]
```

```bash:docker-entrypoint
#!/usr/bin/env bash

set -euo pipefail

function tmp_file_exits() {
  if [ "$(ls -A /tmp/nitro/)" ]; then
    rm /tmp/nitro/*
    echo "delete successful tmp file"
  else
    echo "file doesn't exits"
  fi
}

function main() {
  tmp_file_exits
  npm run dev
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi
```

参考文献

https://docs.docker.jp/engine/reference/builder.html

# 3. ベストプラクティスとセキュリティ

## Dockerfileの整理と可読性

Dockerfileの整理と可読性を上げると、メンテナンス性が向上し、チームメンバー全員の理解度が上がります。以下はそのための基本的なポイントです。

+ **コメントを活用する**：Dockerfile内の各セクションやコマンドについての説明をコメントとして追加します。これにより、他の開発者が意図を理解しやすくなります
+ **適切な命名規則**：変数やディレクトリ名は、意味が明確で一貫性のある名前を使用します
+ **コマンドの分離**：複数の操作を一つのRUNコマンドでまとめず、適切に分けることで可読性を高めます。ただし、これはレイヤー数とトレードオフになります


## レイヤーの最適化

Dockerイメージはレイヤーを重ねた構造で、各命令（RUN、COPY、ADDなど）ごとに新しいレイヤーが生成されます。このレイヤーを最適化することで、イメージの軽量化とビルド時間の短縮を実現できます。

+ **不要なファイルを含めない**：Dockerイメージを小さくするには、不要なファイルを持ち込まないことが重要です。.dockerignoreというファイルを使って、コピーしたくないファイルやディレクトリを指定することで、無駄なファイルがイメージに含まれるのを防ぎましょう
+ **RUNコマンドの統合**：関連する操作は一つのRUNコマンドにまとめます。例えば、複数のパッケージをインストールし、キャッシュをクリアする場合、一つのRUNコマンドにまとめて書くことで、不要なレイヤーを減らせます

### 悪い例
```Dockerfile
RUN apt-get update
RUN apt-get install -y --no-install-recommends wget=1.21.3-1+b2
RUN apt-get -y clean
RUN rm -rf /var/lib/apt/lists/*
```

### 良い例
```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    wget=1.21.3-1+b2 \
    && apt-get -y clean \
    && rm -rf /var/lib/apt/lists/*
```

## ビルドキャッシュの活用

Dockerはビルド時にキャッシュを利用します。これを有効に活用することで、ビルド時間を大幅に短縮できます。

+ **頻繁に変わらない部分を上部に配置**：Dockerfileの上部にキャッシュを利用できる部分（例：基盤イメージの取得や共通ライブラリのインストール）を配置し、頻繁に変更される部分（例：ソースコードの追加）は下部に配置します

```Dockerfile
FROM python:3.11.9-slim-bookworm（頻繁に変わらない）

WORKDIR /app（頻繁に変わらない）

COPY requirements.txt ./（頻繁に変わらない）
RUN pip install --no-cache-dir -r requirements.txt（頻繁に変わらない）

COPY . .（頻繁に変わる）
```

## マルチステージビルド

マルチステージビルドを使用することで、最終的なイメージに不要なビルドツールや中間生成物を含めず、より小さくセキュアなイメージを作成できます。

```Dockerfile
# 開発用ステージ
FROM python:3.11.9-slim-bookworm AS developer

# ビルドステージ
FROM python:3.11.9-slim-bookworm AS builder

# 本番ステージ
FROM gcr.io/distroless/python3-debian12:nonroot AS production
```

## Docker Composeの利用

Docker Composeは、マルチコンテナのDockerアプリケーションを定義して実行するためのツールです。複数のサービス（コンテナ）をまとめて管理でき、開発環境を簡単に構築し、実行することができます。

```yml:compose.yaml
services:
  api:
    restart: always
    build:
      context: ./apserver
      target: developer
    container_name: "api_ec_site"
    environment:
      DATABASE_NAME: ecsiteproject
      DATABASE_USER: pythonMySql1
      DATABASE_PASSWORD: pythonMySql1
      DATABASE_HOST: db
      DATABASE_PORT: 3306
      DEBUG_MODE: "true"
    command: ["python", "opt/main.py"]
    volumes:
      - ./apserver:/app
    tty: true
    ports:
      - 8000:8000
```
## ボリュームを使ったホットリロード

開発効率をさらに向上させるために、ボリュームを使ったホットリロードを利用することができます。ホットリロードとは、ソースコードの変更をリアルタイムで反映させる機能です。これにより、コードを変更するたびにコンテナを再起動する必要がなくなります。

```yml:compose.yaml
    volumes:
      - ./apserver:/app
```

# lintツールを使う

Dockerfileの品質を維持することは、Dockerイメージのビルドやデプロイの成功に不可欠です。hadolintを利用することで、Dockerfile内の問題を事前に発見し、品質を向上させることができます。

https://github.com/hadolint/hadolint

## セキュリティ対策

Dockerイメージのセキュリティを確保するためのベストプラクティスも重要です。

+ **公式イメージの使用**：可能な限り公式のベースイメージを使用し、信頼性を確保します
+ **不要なパッケージをインストールしない**：最小限のパッケージのみをインストールし、攻撃対象領域を減らします
+ **脆弱性スキャン**：定期的にイメージの脆弱性スキャンを行い、セキュリティリスクを低減します。例えば、ClairやTrivyなどのツールを使用します
+ **最小権限の原則**：コンテナ内で実行されるプロセスは、必要最小限の権限で実行するようにします。例えば、ユーザーをrootではなく専用の非特権ユーザーに変更します


詳しくは以下の記事を参考にして下さい。

https://qiita.com/sugiyama404/items/3f28f06008f15d4f4ae7

# 4. PythonアプリケーションのDockerfile作成
## ベースイメージの選択

まず、Pythonアプリケーションを実行するために適切なベースイメージを選択する必要があります。Pythonアプリケーションを構築する場合、公式のPythonイメージが最適です。最新のPythonイメージを使用することをお勧めします。ベータ版の場合仮想環境の設定がされていないことがあるので注意が必要。

```Dockerfile
FROM python:3.11.9-slim-bookworm
```

### ベースイメージの違い

+ **python**: Pythonのデフォルトのベースイメージです。最新の安定版が含まれています
+ **python:alpine**: Alpine Linuxは軽量かつセキュアなLinuxディストリビューションで、Dockerイメージのサイズを最小限に抑えることができます
+ **python:slim**: DebianベースのPythonイメージのスリム版です
+ **python:buster**: Debian BusterをベースにしたPythonイメージです。Debianは安定性と広範なパッケージのサポートを提供し、Python開発に必要な多くの追加パッケージが含まれています
+ **bookworm**: bookwormイメージは、最新のDebianテスト版を使用しており、最新の機能やパッケージにアクセスできますが、安定版よりも安定性に欠ける可能性があります
+ **bullseye**: bullseyeイメージは、Debianのテスト版であり、安定性と将来の互換性を考慮しています
+ **distroless**: Distrolessイメージは、ディストリビューションレス（Distroless）として知られる、非常に軽量でセキュアなDockerイメージです。これらのイメージには、基本的なOSパッケージが含まれていません

https://hub.docker.com/_/python

https://github.com/GoogleContainerTools/distroless

## 必要な依存関係のインストール

アプリケーションが依存するパッケージやライブラリをインストールするために、pipを使用します。

```Dockerfile
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
```

requirements.txtは、Pythonアプリケーションが必要とするすべてのパッケージを含むテキストファイルです。このファイルは、pipを使用してインストールできます。

```txt:requirements.txt
Flask==3.0.3
Flask-Cors==4.0.1
```

## アプリケーションコードの追加

アプリケーションのコードをDockerイメージに追加します。

```Dockerfile
COPY . .
```

## 実行コマンドの設定

Dockerコンテナが起動したときに実行されるコマンドを指定します。

```Dockerfile
CMD ["python", "opt/main.py"]
```

## 環境変数の設定

アプリケーションに必要な環境変数がある場合は、Dockerfile内で設定することができます。

```Dockerfile
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
```

環境変数の例は以下の通りです。

+ **PYTHONDONTWRITEBYTECODE**: この環境変数は、Pythonがバイトコード（.pycファイル）を生成しないようにします。それにより、ディレクトリが綺麗なままになります
+ **PYTHONUNBUFFERED**: この環境変数は、Pythonの標準出力と標準エラー出力をバッファリングせずに直ちに出力するように指示します。これにより、Pythonプログラムの出力がリアルタイムに表示されます
+ **PIP_NO_CACHE_DIR**: この環境変数は、pipがキャッシュディレクトリを使用しないようにします。キャッシュディレクトリは、pipがパッケージをダウンロードしてキャッシュする場所です。この変数を設定すると、pipはキャッシュを無視し、常に最新のパッケージをダウンロードします
+ **PIP_DISABLE_PIP_VERSION_CHECK**: この環境変数は、pipが自身のバージョンをチェックしないようにします。通常、pipは実行時に自身のバージョンをチェックし、新しいバージョンが利用可能かどうかを確認します。しかし、この変数を設定すると、そのチェックを無効にします
+ **PYTHONPATH**: Pythonがモジュールを検索するパスを指定します。特定のディレクトリを指定することで、Pythonがそのディレクトリ内のモジュールをインポートできるようにします。

参考文献

https://docs.python.org/ja/3/using/cmdline.html

## Pythonコードの実行

これでDockerfileが完成しました。

```Dockerfile
# 開発用ステージ
FROM python:3.11.9-slim-bookworm AS developer

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    wget=1.21.3-1+b2 \
    && apt-get -y clean \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# ビルドステージ
FROM python:3.11.9-slim-bookworm AS builder

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# 本番ステージ
FROM gcr.io/distroless/python3-debian12:nonroot AS production

WORKDIR /app

COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11

ENV PYTHONPATH=/usr/local/lib/python3.11/site-packages
COPY --from=builder /app/ .

EXPOSE 8000
USER nonroot

CMD ["/app/opt/main.py"]
```

```txt:requirements.txt
Flask==3.0.3
Flask-Cors==4.0.1
PyMySQL==1.1.1
Jinja2==3.1.4
requests==2.32.3
SQLAlchemy==2.0.30
Flask-SQLAlchemy==3.1.1
pytest==8.2.2
pytest-mock==3.14.0
pytz==2024.1
bcrypt==4.1.3
```

```igorpro:.dockerignore
# Git
.git
.gitignore
.gitattributes

# Docker
docker-compose.yml
compose.yaml
Dockerfile
.dockerignore

# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]

# PyInstaller
*.manifest
*.spec

# Virtual environment
.env
.venv/
venv/

# VS Code
.vscode/

# macOS
.DS_Store
.AppleDouble
.LSOverride

# macOS icon file
Icon?
._*

# logfile
*.log

# cache file
*.cache/
*.pytest_cache/
*.coverage
```

以上です。