この記事はElixir Advent Calendar 2021の6日目です。
昨日は @h3_potetoで「ElixirでHot Code Reloadingするときに気をつけること」でした
ごあいさつ
こんにちは Koyo です!普段はコミュニティで Elixir 開発しつつ、仕事ではRuby on RailsでWebアプリケーションをごにょごにょ書いております。
さて会社外でプロジェクトに参加していると、開発マシンがMacの人もいればWSL2やLinuxもいたりとバラバラだったりしますよね。なのでDockerを用いて開発環境をなるべく統一しよう!・・・と思ったのですが、意外にハマりどころがあって苦戦したので書いてみました。
- Mac
- WSL2 / Linux
- Mac / WSL2 / Linux 混在環境
の3パターンに分けて解説していきたいと思います!
なお説明で使うファイルはこちらにまとめております。
想定バージョン
- elixir 1.13
- phoenix 1.6.x
※20220417 最新版にアップデート
Docker Desktop for Mac で Phoenix が動く Dockerfile / docker-compose
こんな感じで動きます。今回はベースイメージはalpineにしてます!
FROM elixir:1.13-alpine
RUN apk update && apk add \
inotify-tools git build-base npm bash
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
version: '3'
services:
web:
build:
context: .
command: mix phx.server
environment:
DB_HOST: db
links:
- db
ports:
- 4000:4000
volumes:
- .:/work
working_dir: /work
db:
image: postgres:14-alpine
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
こんな感じで新しいプロジェクトも作ることができます!
docker compose run --rm --no-deps web mix phx.new sample
※Docker経由でPhoenixを動作させるときの注意点
なお、実際に動作させる場合は Dockerfile
と docker-compose.yml
を作成したプロジェクトのディレクトリ(今回はsampleディレクトリ)にコピーするのを忘れずに。
さらに config/dev.exs
の編集が必要です。
まず hostname を docker-compose.yml
の環境変数で指定したホスト名にしてください。
また、ここがハマりポイントですが、デフォルトだと http: [ip: {127, 0, 0, 1}, port: 4000],
になっているのを http: [ip: {0, 0, 0, 0}, port: 4000]
に修正する必要があります。
config :sample, Sample.Repo,
...
hostname: System.get_env("DB_HOST") || "localhost",
...
config :sample, SampleWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {0, 0, 0, 0}, port: 4000],
Docker for Mac 以外(WSL2 / Linux など)でも動くように
上記でも Docker Desktop for Mac
であれば root でコンテナを動かしてファイル生成しても、ホスト側のUID/GIDと一致するため問題ありません(ドキュメント探してもパッと見つからなかったので、理由分かる人いたら教えてくださいw)。
一方で他の環境(例えば Docker Desktop for Windows
)で先ほどの mix phx.new
コマンドを実行してみると以下のようになってしまいます。
$ ls -l
drwxr-xr-x 9 root root 4096 Dec 5 20:24 sample/
コンテナのユーザが root であるため、作成されたファイルももちろん root になってしまいます。
mix phx.new
済みのプロジェクトであっても mix deps.get
などでコンテナ経由でファイル生成することはよくあるため、これだとパーミッション周りのエラーが頻発し、困ってしまいますね。
コンテナ内にホストと同一UID/GIDのユーザを作る
ということで、これを解決するために「コンテナ内にホストと同一UID/GIDのユーザを作りたい」と思います。
下準備として下記のようなMakefileを書いてみます。
.PHONY: setup
setup:
echo "host_user_name=${USER}" > .env
echo "host_group_name=${USER}" >> .env
echo "host_uid=`id -u`" >> .env
echo "host_gid=`id -g`" >> .env
実行すると .env
ファイルが生成されます。
$ make setup
さて、これを踏まえて下記のようにDockerfile/docker-composeを作成します。
環境変数をdocker-compose.ymlに渡し、Dockerfileへはビルド時の引数という形でホストのユーザ情報を渡します。そしてコンテナビルド時にホストと同じUID/GIDのユーザが作られるようにします。
FROM elixir:1.13-alpine
RUN apk update && apk add \
inotify-tools git build-base npm bash
ARG host_user_name
ARG host_group_name
ARG host_uid
ARG host_gid
RUN apk add shadow && groupadd -g $host_gid $host_group_name \
&& useradd -m -s /bin/bash -u $host_uid -g $host_gid $host_user_name
USER $host_user_name
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
version: '3'
services:
web:
build:
context: .
args:
- host_user_name=${host_user_name}
- host_group_name=${host_group_name}
- host_uid=${host_uid}
- host_gid=${host_gid}
command: mix phx.server
environment:
DB_HOST: db
links:
- db
ports:
- 4000:4000
volumes:
- .:/work
working_dir: /work
db:
image: postgres:14-alpine
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
さて、これで実行してみると今度はrootにならずにできていますね!
$ docker compose run --rm web mix phx.new sample
...
$ ls -l
drwxr-xr-x 9 koyo koyo 4096 Dec 5 20:52 sample/
Mac / WSL2 / Linux 混在環境でも動くように
これで解決・・・と思いきや Mac 環境で上記の Dockerfile をビルドしてみると問題が発生してしまいます。
どうも Mac 側の GID と衝突してしまうよう・・・(いい回避策知ってたら教えてくださいw)。
> [3/4] RUN apk add shadow && groupadd -g 20 koyo && useradd -m -s /bin/bash -u 504 -g 20 koyo:
#6 0.388 (1/2) Installing linux-pam (1.5.1-r1)
#6 1.901 (2/2) Installing shadow (4.8.1-r0)
#6 1.962 Executing busybox-1.33.1-r6.trigger
#6 1.968 OK: 264 MiB in 55 packages
#6 2.008 groupadd: GID '20' already exists
プロジェクトがMacのみ!とかMac以外のみ!であれば、上記のどちらかを選択して使えばいいかと思います。
しかし実際には混在していることが多いのではないでしょうか。
筆者が参加したプロジェクトでは Mac / WSL2 / Linux と様々なOSで開発している方が集まっていました笑
ということで頑張ってみましょう!
…とその前に、 docker-compose.yml
を上書きする手法と、マルチステージビルドについて説明します。
docker-compose の設定を上書きする
docker compose
コマンドは標準で docker-compose.override.yml
という上書き用のファイルをサポートしています。これが存在する場合 docker-compose.yml
の設定を docker-compose.override.yml
で上書きしてくれます。
マルチステージビルド
Dockerfile には実は「複数のFROM」を書くことができます。例えば1つ目のFROMフェーズでビルドを行い、そこで作成したビルド結果だけを後続のステージに渡すことで、コンテナ容量を削減できます。
今回はこちらをビルド処理の共通化に使ってみたいと思います。
FROM ... AS base
FROM base AS ...
Mac / WSL2 / Linux 混在環境でも動く Dockerfile/docker-compose.yml
まず Dockerfile
を以下のようにします。 base
フェーズで共通の処理を行い、その後 setup_user
フェーズを経由してユーザを作成する build_as_user
と、経由せずユーザを作成しない build_as_root
フェーズに分岐します。
FROM elixir:1.13-alpine AS base
RUN apk update && apk add \
inotify-tools git build-base npm bash
FROM base AS setup_user
ARG host_user_name
ARG host_group_name
ARG host_uid
ARG host_gid
RUN apk add shadow && groupadd -g $host_gid $host_group_name \
&& useradd -m -s /bin/bash -u $host_uid -g $host_gid $host_user_name
USER $host_user_name
FROM setup_user AS build_as_user
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
FROM base AS build_as_root
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
docker-compose.yml
では target: build_as_root
とすることでユーザを作成せずビルドが行われるようにしておきます。
version: '3'
services:
web:
build:
context: .
target: build_as_root
command: mix phx.server
environment:
DB_HOST: db
links:
- db
ports:
- 4000:4000
volumes:
- .:/work
working_dir: /work
db:
image: postgres:14-alpine
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
このままだと Docker Desktop for Mac
環境でしか動かないため、ここからさらに手を加えます。
まず docker-compose.yml
とは別のディレクトリに以下の docker-compose.override.yml
を配置します。
target: build_as_user
とすることでユーザを作成してビルドが行われるようにしつつ、環境変数経由でホストのユーザ情報をDockerfileに渡します。
services:
web:
build:
target: build_as_user
args:
- host_user_name=${host_user_name}
- host_group_name=${host_group_name}
- host_uid=${host_uid}
- host_gid=${host_gid}
そして Makefile を以下のようにして、 docker/docker-compose.override.yml
を docker-compose.yml
があるディレクトリにコピーするようにしておきます。
.PHONY: setup_for_docker_user
setup_for_docker_user:
cp docker/docker-compose.override.yml .
echo "host_user_name=${USER}" > .env
echo "host_group_name=${USER}" >> .env
echo "host_uid=`id -u`" >> .env
echo "host_gid=`id -g`" >> .env
こうすることで 「Macユーザはそのまま、そうでない場合は make setup_for_docker_user
を実行してもらう」 とすることで、異なるOSが混在する環境でも Elixir/Phoenix を動作させることができます!あとは README.md
にでも書いておくといいですね。
便利!
まとめ
- Mac
- WSL2 / Linux
- Mac / WSL2 / Linux 混在環境
で動く Dockerfile と docker-compose.yml を作ってみました。特に「Mac / WSL2 / Linux 混在環境」で動くようにするのにだいぶ苦労したので、ぜひ使ってください笑
今回のDockerfileは一例ですので、alpineを使わないようにしてみるなど、プロジェクトの構成に併せて適宜カスタマイズしてみてください!(alpine使わずに動かす Dockerfile 書いたらぜひ記事にして教えてください!)
今回紹介したDockerfile群はこちらのリポジトリで公開しています!
READMEにも動かし方を書いてあるので参考にしてみてください^w^
私の記事はここでおしまいです!
明日は、@kn339264の「Elixir/Phoenix入門者向けコミュニティpiyopiyo.exを立ち上げた話」です
ぜひぜひ読んでみてください!