DockerComposeでElixirPheonixの開発環境を整える

# 概要
DockerComposeを使用して、Phoenixプロジェクトの開発環境を構築します。ローカル環境で開発する場合にも、Dockerコンテナ内で開発する場合にも対応可能な設定を目指します。

この記事は、開発環境を構築する学習過程をまとめたものです。今回は、dev環境で正常に動作することを目標としています。

リポジトリは以下になります

https://github.com/BlackTanuTanuNinjas/tudo_docker

# 環境

この記事では、以下の環境で開発を行っています。

* PC: MacBook
* OS: macOS 15.2

またElixir(Erlang)はasdf上で管理しています。
それぞれのバージョンは以下になります。

```
% asdf current
Name            Version         Source                                    Installed
elixir          1.18.2-otp-27   /Users/linsei/Docker/Qiita/.tool-versions true
erlang          27.2.2          /Users/linsei/Docker/Qiita/.tool-versions true
nodejs          19.3.0          /Users/linsei/.tool-versions              true
```

# 手順

DockerComposeを使用してPhoenixの開発環境を構築するために、以下の手順を進めます。

1. [Phoenixプロジェクトを作成し、Dockerファイルを準備する](#Phoenixプロジェクトを作成し、Dockerファイルを準備する)
2. [docker-compose.ymlを作成する](#docker-compose.ymlを作成する)
3. [dockerコマンドを使用してサービスを起動する](#dockerコマンドを使用してサービスを起動する)

# Phoenixプロジェクトを作成し、Dockerファイルを準備する

まず、Phoenixプロジェクトを作成します。データベースは特に指定していないので、デフォルトでPostgreSQLが使用されます。

```shell
$ mix phx.new tuto_docker
```

この時点で、プロジェクトのディレクトリ構成は以下のようになっています。

```
tuto_docker % tree -L 1
.
├── README.md
├── _build
├── assets
├── config
├── deps
├── lib
├── mix.exs
├── mix.lock
├── priv
└── test
```

## Dockerfileを作成する

次に、Phoenixを起動するためのDockerfileを作成します。
下記コマンドを実行すると、リリース用のDockerfileが自動生成されます。

以下のコマンドを実行します

```bash
mix phx.gen.release --docker
```

これによりphoenixを起動するためのDockerfileが作成されます。

詳しくはこちらをご覧ください。

https://hexdocs.pm/phoenix/releases.html

生成後、プロジェクトのディレクトリ構成は以下のようになります。

```bash
tuto_docker % tree -L 1
.
├── (省略)
├── Dockerfile
└── rel
```

## Dockerfile.devの作成・編集

次に、生成されたDockerfileを編集し、開発用のDockerfile.devを作成します。
まず、元のDockerfileをコピーしてリネームしましょう。

そしたら、以下のようにファイルを編集します。

```diff_yaml:Dockerfile.dev
ARG ELIXIR_VERSION=1.18.2
ARG OTP_VERSION=27.2.2
ARG DEBIAN_VERSION=bullseye-20250203-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"

FROM ${BUILDER_IMAGE} as builder

- RUN apt-get update -y && apt-get install -y build-essential git \
-   && apt-get clean && rm -f /var/lib/apt/lists/*_*

+ RUN apt-get update -y && apt-get install -y build-essential npm inotify-tools git \
+   && apt-get clean && rm -f /var/lib/apt/lists/*_*

- WORKDIR /app これを削除

RUN mix local.hex --force && \
    mix local.rebar --force

# これより下を削除
```

この編集により、Phoenixを起動するために必要なパッケージインストールのみに内容を絞っています。
このDockerfile.devは、後ほど作成するdocker-compose.ymlで使用します。

## postgres

次に、PostgreSQLをサービスとして起動するためのイメージを探します。
PostgreSQLのDockerイメージは以下から確認できます。

https://hub.docker.com/_/postgres/tags

現在の最新イメージは 15.10-alpine3.21 です。
これを後ほどdocker-compose.ymlで指定します。



# docker-compose.ymlを作成する

Phoenixプロジェクト内にdocker-compose.ymlを作成します。

以下のように基本構成を記述しておきます。

```yml
services:
  web:
    (後で記述)
  db:
    (後で記述)  
volumes:
  dbdata:
```

ここで定義しているvolumesは、データ永続化のために使用する名前付きボリュームです。dbdataという名前で作成します。

## webサービスの設定

webはPhoenixを起動するサービスです。次のように記述します。

```yml
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/work
    working_dir: /work
    links:
      - db
    environment:
      DB_HOST: db
    ports:
      - 4000:4000
    command: tail -f /dev/null
```

各セクションの説明
* **build**: Dockerfile.devをDockerfileに指定します
* **volumes**: 現在のディレクトリをコンテナ内の/workにマウントします
    - コンテナ内でローカルと同じディレクトリを使用するために共有が必要です 
* **working_dir**: コンテナ内での作業ディレクトリを/workに設定します
* **links**: dbサービスと通信できるようにリンクを設定しています
* **environment**: 環境変数DB_HOSTにdbサービスのホスト名を設定します
* **ports**: ホストのポート4000をコンテナ内の4000番ポートにマッピングします
    - `config/dev.exs`でサーバーに使用するポートと一致するようにしてください
* **command**: コンテナが即時終了しないようにするコマンドを実行します

## dbサービスの設定

dbサービスはPostgreSQLを起動するための設定です。以下のように記述します。

```yml
  db:
    image: postgres:15.10-alpine3.21
    volumes:
      - dbdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432

```

各セクションの説明
* **volumes**: コンテナ内の/var/lib/postgresql/dataをdbdataボリュームにマウントしています。これにより、データがコンテナを削除しても永続化されます。
* **environment**: PostgreSQLの起動時に必要な環境変数を設定します
    * POSTGRES_PASSWORDは必須項目です
    * このパスワードは`config/dev.exs`の設定と一致させてください
* **ports**: ホストのポート5432をコンテナ内の5432にマッピングしています
    * PostgreSQLのデフォルトポート番号は5432です

## docker-compose.ymlの全文

最終的に記述を終えたdocker-compose.ymlの全体は、以下のリポジトリから確認できます。

## dev.exsの設定を変更する

dev.exsファイルを編集し、データベースのホスト名とサーバーのIPアドレスを設定します。

```diff_elixir
import Config

# データベース設定
config :tuto_docker, TutoDocker.Repo,
  username: "postgres",
  password: "postgres",
- hostname: "localhost",
+ hostname: System.get_env("DB_HOST") || "localhost",
  database: "tuto_docker_dev",
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

# サーバー設定
config :tuto_docker, TutoDockerWeb.Endpoint,
- http: [ip: {127, 0, 0, 1}, port: 4000],
+ http: [ip: {0, 0, 0, 0}, port: 4000],
  # 略

```

# dockerコマンドでサービスを起動する

必要な設定が完了したら、以下のコマンドを実行してサービスを起動します。

```bash
% docker compose up -d
% docker compose exec web /bin/bash
root@コンテナID:/work# iex -S mix phx.server
```

最後のコマンドを実行すると、通常通りPhoenixサーバーのページを表示できるはずです。

# コンテナから起動するメリット

コンテナからPhoenixアプリケーションを起動するメリットを説明します。

コンテナ内で環境変数を設定することで、ローカル環境に余計な環境変数を残さずに済みます。例えば、何のために設定したかわからない環境変数がローカルに残っていると管理が煩雑になります。
一方、コンテナでは環境変数をコンテナ内だけで適用し、不要なときは一括で削除できるため、開発環境をクリーンに保つことができます。

## 環境変数を使ったアプリケーション設定の例

例として、環境変数MYNAMEを使用する設定を例を示します。

```exs:config/config.exs

import Config

# 略

config :tuto_docker,
  myname: System.get_env("MYNAME") || "たぬき"

# 略
```

```yaml:docker-compose.yml
    environment:
      DB_HOST: db
      MYNAME: "ブラックたぬたぬ"
```

環境に応じて`http://localhost:4000/`の表示は以下のように変化します。

* ローカルで起動: `I am たぬき.`

* コンテナで起動: `I am ブラックたぬたぬ.`


# まとめ

ここまでの設定を行うことで、Phoenixプロジェクトをローカル環境でもコンテナ環境でも開発できるようになります。

# おまけ

データベースをコンテナ内で作成することで、ローカル環境に不要なデータベースを作成せずに済むというメリットがあります。

curdブランチを用意しています。
以下の手順でローカルとコンテナ環境の違いを確認してみてください。

1. コンテナでサーバーを起動し、CRUDページにデータを入力します
2. サービスを停止し、ローカルでサーバーを起動します
3. コンテナ内のデータがローカル環境には反映されていないことが確認できます