💡
【設定自動化】docker composeのコンテナごとの設定をすべて一つのenvから注入する
今回の目的
- 開発環境でdockerを利用することで開発環境を統一できるようにはなるものの、laravelなど設定ファイルの記述が多いものや、コンテナ自体が多い場合などにdockerを立ち上げるまでにいろいろ設定ファイルを記述する必要があります。
- これが少し面倒だなと感じ、自動的に注入できるようにはできないかと考えました(単なる横着です🤫)。
前提知識
- Dockerの
compose.yml
の記述方法 - 使用したいアプリケーションの設定ファイルの記述方法
サンプルのプロジェクト構成
myproject(root) /
├─ .vscode /
│ ├─ setting.json
│
├─ apps /
│ ├─ api / # laravel用のコンテナ
│ ├─ scripts /
│ ├─ entrypoint.sh
│ ├─ templates /
│ ├─ .env.template
│ ├─ myproject-api /
│ ├─ .env
│ ├─ その他laravelディレクトリ群
│ ├─ Dockerfile
│ ├─ docker.conf
│ ├─ php.ini
│ ├─ web / # react(vite)用のコンテナ
│ ├─ scripts /
│ ├─ entrypoint.sh
│ ├─ templates /
│ ├─ .env.local.template
│ ├─ myproject-api /
│ ├─ .env.local
│ ├─ その他reactディレクトリ群
│ ├─ Dockerfile
│
├─ postgres /
│ ├─ Dockerfile
│
├─ nginx-proxy /
│ ├─ frontend
│ ├─ sh
│ ├─ entrypoint-nginx.sh
│ ├─ templates
│ ├─ default.conf.template
│ ├─ log /
│ ├─ backend
│ ├─ sh
│ ├─ entrypoint-nginx.sh
│ ├─ templates
│ ├─ default.conf.template
│ ├─ log /
│ ├─ sh
│ ├─ entrypoint-nginx.sh
│ ├─ templates
│ ├─ default.conf.template
│ ├─ domain_list.template
│ ├─ log /
│ ├─ .gitignore
│
├─ .env
├─ .env.example
├─ .dockerignore
├─ .gitignore
├─ compose.yml
└─ README.md
- 汎用的に使えることを示すためにnginxの構成など少し複雑になってはいますが、単純化(nginxプロキシを一つにするなど)も自由に変えてください。
実装の流れ
- イメージしやすいようにまず実装のある程度の流れを説明した後に実際のコード記述に入っていきます。
- 通常通り、
Dockerfile
やcompose.yml
、その他必要なファイルを構成していく。 - プロジェクト直下に
.env
ファイルを作成して、プロジェクト全体での設定を記載していく。 - この
.env
を各々のコンテナに適用させることでこれがコンテナ内の環境変数として渡される。 - 後はこれを起動時に実行するシェルスクリプトと各アプリケーションの設定ファイルのテンプレートを書いていく。
- 通常通り、
- わかりにくいところがあると思うので、githubにてサンプル用のコードを貼っておきます。
- サンプルコード
プロジェクトの立ち上げ
プロジェクトのクローン
git clone https://github.com/ablankz/docker-env-injection.git
事前準備とhosts ファイルの追記
-
今回は作成するプロジェクトを
myproject
、api のプロジェクト名はmyproject-api
、web のプロジェクト名はmyproject-web
とします -
また使用するドメインは、web フロントを
myproject.com
、api サーバーをapi.myproject.com
とします -
今回、localで動作するように設定するのですが、本番でもそのまま使えるコード(あくまでアプリケーション内のコード)にするために、ドメインもwebは
myproject.com
、apiはapi.myproject.com
のように動作するようにしたいのでまずはこれらの名前解決をlocalで行わせるようにします。 -
unix系の場合なら
/etc/hosts
を編集。windowsなら[共通] hostsファイルの編集(WindowsOSのPCの場合)など多く記事が上がっているのでこれを確認して以下のような設定を追記してください(元の記述は残しておいてください)。127.0.0.1 myproject.com 127.0.0.1 api.myproject.com
api サーバーのプロジェクト作成
-
apps/api
まで移動した後、以下のコマンドでプロジェクトを作成する -
myproject-api
の部分は先程決めたapiのプロジェクト名が入るcomposer create-project laravel/laravel --prefer-dist myproject-api
webサーバーのプロジェクト作成
-
apps/web
まで移動した後、以下のコマンドでプロジェクトを作成する - projectが聞かれたときに先程決めたwebのプロジェクト名を入力する
- 他の設定は基本的には以下のような設定で良さそう
npm create vite Need to install the following packages: create-vite@4.4.1 Ok to proceed? (y) y ✔ Project name: … myproject-web ✔ Select a framework: › React ✔ Select a variant: › TypeScript + SWC Scaffolding project in /home/blank/docker-env-injection/apps/web/myproject-web... Done. Now run: cd myproject-web npm install npm run dev
- ターミナルにも書いてくれてるコマンドを実行する(
npm run dev
は今はいらない)cd myproject-web npm install
-
apps/web/myproject-web/vite.config.ts
を以下のように書き換える(docker内でhotreloadを解決するため)import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { host: true, watch: { usePolling: true, }, hmr: { path: "_vite/ws-hmr", }, }, })
envファイルの記載
- プロジェクト直下の
.env
にコメント記述通りの設定を記載していく
コンテナの立ち上げ
docker compose up -d
失敗した時(もしくはコンテナが立ち上がらない時)
- 大抵の場合はwebやapiの作成したプロジェクト名と
.env
で設定したプロジェクト名が異なったり、hosts
で設定したドメインと.env
で記述したドメインが異なるなどだと思います - 以下のような感じのエラーが出た場合
Error response from daemon: error while mounting volume '/var/lib/docker/volumes/docker-env-injection_node_modules/_data': failed to mount local volume: mount /path/to/project/apps/web/myproject-web/node_modules:/var/lib/docker/volumes/docker-env-injection_node_modules/_data, flags: 0x1000: no such file or directory
-
.env
のAPP_ROOT_PATH
の記述ミスだと思うのですが、このエラーになるとdockerがvolumeを記憶させてしまうらしく、通常は.env
を修正後、docker compose down
した後、docker compose up -d
をすると良い(docker compose restart
でも良い)のですが、この場合は一度以下のコマンドでvolumeも削除してから再度startを行ってください(もちろん.env
の修正を行ったあとで)docker compose down --volumes --remove-orphans
重要部分のコードリーディング
- 実装の流れを追っていきたいところですがファイル数が多いため、お手数ですが必要な設定ファイルなどだけ追っていくので、残りはgithubのリポジトリの方をご確認ください🙇
compose.yml
と.env
ファイル
- プロジェクト直下の
.env
ファイルは以下のように記述します変数名=値 ####### 以下に例を示す ###### # プロジェクトのrootパス # unix系ならプロジェクト直下でpwdで確認したものを貼る APP_ROOT_PATH=/path/to/project # デフォルトのデータベースの設定 POSTGRES_DB=myproject # はじめに作るdb(基本的にはプロジェクト名でok) POSTGRES_USER=postgres # 基本的には変更なし POSTGRES_PASSWORD=postgres # 基本的には変更なし POSTGRES_PORT=5432 # 基本的には変更なし ...
- その
.env
ファイルをcompose.yml
の以下の部分などでenvfileに設定していますweb: # reactWebサーバー container_name: ${APP_NAME}-web tty: true build: context: "./apps/web" volumes: - ./apps/web/${WEB_APP_NAME}:/usr/src/app - node_modules:/usr/src/app/node_modules - ./apps/web/scripts:/docker-init/scripts - ./apps/web/templates:/docker-init/env_templates environment: - TZ=Asia/Tokyo - CHOKIDAR_USEPOLLING=true env_file: ./.env # 👈ここ command: sh -c "chmod +x /docker-init/scripts/entrypoint.sh && /docker-init/scripts/entrypoint.sh"
- これを設定したコンテナでは環境変数としてこの
env
ファイルの変数たちがはじめから設定されているような感じになります。
- これを設定したコンテナでは環境変数としてこの
templates
とsh
(scripts
)
- 次にこの環境変数をもとにそれぞれの設定ファイルを記述する必要があるのですが、はじめに実行できるコマンドを設定するのは後述しますが、ここでは例として
nginx-proxy/backend/sh
とnginx-proxy/backend/templates
について説明します。 - おそらく1コンテナさえ理解できればあとは、templateだけが変わって残りは同じです
nginx-proxy/backend/templates/default.conf.template
server {
listen ${BACKEND_PROXY_PORT};
listen [::]:${BACKEND_PROXY_PORT};
server_name "${PROXY_SERVER_BACKEND}";
root /var/www/${API_APP_NAME}/public;
...
}
- このファイルは単に埋め込みたい環境変数の箇所を
${BACKEND_PROXY_PORT}
のように記述しているだけのtemplateファイル。 - これと次に説明する
sh/entrypoint-nginx.sh
はcompose.yml
の以下の部分でvolumeに登録しているのでこのファイルは起動時、コンテナにマウント?される... backend-server: container_name: ${APP_NAME}-backend-server image: nginx volumes: - ./apps/api/${API_APP_NAME}:/var/www/${API_APP_NAME} - ./nginx-proxy/backend/templates:/etc/nginx/conf.d/templates # 👈ここ - ./nginx-proxy/backend/sh:/etc/nginx/conf.d/sh # 👈ここ ...
nginx-proxy/backend/sh/entrypoint-nginx.sh
#!/bin/bash
expand_variables() {
local content
content=$(< "$1") # テンプレートファイルの内容を読み込む
# 環境変数を正規表現で検索し、対応する値で置き換える
for var in $(compgen -A variable); do
value="${!var}" # 環境変数の値を取得
content="${content//\$\{$var\}/$value}" # 置換
done
echo "$content" > "$2"
}
expand_variables /etc/nginx/conf.d/templates/default.conf.template /etc/nginx/conf.d/default.conf
nginx -g 'daemon off;'
- 簡単に言えば、/etc/nginx/conf.d/templates/default.conf.templateファイル(compose.ymlにてvolumesに登録済み)の
${変数名}
となっている部分に環境変数のその変数名を埋め込んで/etc/nginx/conf.d/default.conf
(nginxの設定ファイルを記述する場所)に貼り付けるというシェルスクリプトです。 - 例えば
${変数名}
の形がいやな場合はtemplateは別の形式の$\変数名\
などにしたりして、このスクリプトファイルのexpand_variables
関数の中身を変更すればok。 - ここで重要なのは一番最後の行の
nginx -g 'daemon off;'
。これはnginxを立ち上げるためのコマンドです。- 実はdockerではコマンドは一つしか登録することができず、nginxのimageでコンテナを立ち上げた場合、これがコマンドに登録されているわけです。
- しかし、このシェルスクリプトを後述する
compose.yml
にてcommandに登録すると、このshスクリプトがこのコンテナのコマンドとなり、本来実行されるべきnginx -g 'daemon off;'
が実行されず、最終的に実行するべきプロセスがなくなり、コンテナは落ちてしまいます。 - nginxではこのコマンドですが、他のコンテナでこの方法を利用するときはこのことに十分注意してください。
- また
expand_variables
関数では上書き>
で上書きしているのでもとの/etc/nginx/conf.d/default.conf
の設定は残らないことになります。- これの解決策としてはapi(laravel)の方のこのファイルを見ていただければ確認できると思います。
#!/bin/bash expand_variables() { # 省略 } # envの作成 app_key=`grep '^[[:space:]]*APP_KEY=' /var/www/${API_APP_NAME}/.env`/.env` expand_variables /docker-init/env_templates/.env.template /var/www/${API_APP_NAME}/.env #それ以外を追記 echo ${app_key} >> /var/www/${API_APP_NAME}/.env # key保存 # fpmの起動 sh -c "/usr/local/sbin/php-fpm"
- laravelでは
APP_KEY
を保存しとくべきなのですが、何もしなければただ上書きされてこの情報が消えてしまいます。 - そこで
APP_KEY=
から始まる行を元の/var/www/${API_APP_NAME}/.env
から探してきて、見つかれば、それをapp_keyに保存。 -
expand_variables
で上書きした後に今回は>>
でapp_keyに保存された行を追記しています。 - 保存はこのように行い、他にも様々な設定ファイルに合わせてこのシェルスクリプトをカスタマイズしていけば、コンテナごとにうまく設定できるはずです
- これの解決策としてはapi(laravel)の方のこのファイルを見ていただければ確認できると思います。
コンテナへコマンドの設定
backend-server:
...
volumes:
- ./apps/api/${API_APP_NAME}:/var/www/${API_APP_NAME} # laravelのアプリケーションサーバーにはアクセスせず、同じディレクトリ参照するだけ
- ./nginx-proxy/backend/templates:/etc/nginx/conf.d/templates
- ./nginx-proxy/backend/sh:/etc/nginx/conf.d/sh
- ./nginx-proxy/backend/log:/var/log/nginx
restart: always
command: sh -c "chmod +x /etc/nginx/conf.d/sh/entrypoint-nginx.sh && /etc/nginx/conf.d/sh/entrypoint-nginx.sh" # 👈ここ
...
- 最後はコンテナ立ち上げ時に毎回このシェルスクリプトを実行するようにします
- まずコンテナ内にvolumeで置かれただけではshの実行権限がないので、まずは権限を付与します
- そしてそのshを実行
やってみて感じたこと
- これでコンテナを立ち上げる度に各コンテナのtemplateとプロジェクト直下の
.env
のみからコンテナごとの設定ファイルを自動注入できるようになりました - こういった設定ファイルは通常、GitHubなどに挙げるわけにもいきませんが、templateをあげるのにはなんら問題ないため、プロジェクト開始後は複数人で開発するにしても設定ファイルを一つ書くだけで済むようになるというのが意外と便利だなと思ってます
注意点
- コンテナすべてに.envに記載した設定情報が環境変数として組み込まれる(これがはじめに言ったセキュリティの問題)
- もともと開発環境のために作った構成なので、本番環境では使用しない
- もしくは
api.env
やweb.env
など、設定ファイルを分けてenvfile
の指定にそれぞれ設定すれば良いが、これだとそれぞれの設定ファイルを記述するのと変わらない気がする😅
- 例えば、laravelのenvファイルを変更した際、そのコンテナが立ち上がっている際はその設定が保たれるが、一度落とすと
.env.template
の設定に合わせて.env
の内容が消えるので注意- laravelの
APP_KEY
など残したい設定がある場合はapps/api/scripts/entrypoint.sh
にて以下のように記述する... app_key=`grep '^[[:space:]]*APP_KEY=' /var/www/${API_APP_NAME}/.env` # APP_KEYの値をapp_keyに保存 expand_variables /docker-init/env_templates/.env.template /var/www/${API_APP_NAME}/.env # templateを元に環境変数を埋め込み追記 echo ${app_key} >> /var/www/${API_APP_NAME}/.env # keyを後から追記 ...
- laravelの
- postgresサーバーなどdocker内のネットワークで閉じているのでコンテナの外だとから
php artisan migrate:fresh
などデータベースアクセスコマンドが失敗する- portをコンテナ外部に出した後、laravelの設定テンプレートのdb_hostやdb_portをコンテナ内部から外に出たものに変更する
- もしくは毎回、コンテナ内に入ってから実行する、僕は以下のようなスクリプトを作成して簡単にartisanコマンドを実行できるようにしています
#!/bin/bash args=("$@") docker compose exec api php artisan "${args[@]}" sudo chmod -R a+w ./database sudo chmod -R a+w ./app sudo chmod -R a+w ./config sudo chmod -R a+w ./tests
-
./container-artisan migrate:fresh
のように使用できる
Discussion