💡

【設定自動化】docker composeのコンテナごとの設定をすべて一つのenvから注入する

2023/10/01に公開

今回の目的

  • 開発環境で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プロキシを一つにするなど)も自由に変えてください。

実装の流れ

  • イメージしやすいようにまず実装のある程度の流れを説明した後に実際のコード記述に入っていきます。
    • 通常通り、Dockerfilecompose.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
    
  • .envAPP_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ファイルの変数たちがはじめから設定されているような感じになります。

templatessh(scripts)

  • 次にこの環境変数をもとにそれぞれの設定ファイルを記述する必要があるのですが、はじめに実行できるコマンドを設定するのは後述しますが、ここでは例としてnginx-proxy/backend/shnginx-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.shcompose.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に保存された行を追記しています。
    • 保存はこのように行い、他にも様々な設定ファイルに合わせてこのシェルスクリプトをカスタマイズしていけば、コンテナごとにうまく設定できるはずです

コンテナへコマンドの設定

  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.envweb.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を後から追記
      ...
      
  • 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