docker-composeのprofilesタグを設定して特定のサービスだけを選択的に実行する


※ 当ページには【広告/PR】を含む場合があります。
2023/02/19
蛸壺の技術ブログ|docker-composeのprofilesタグを設定して特定のサービスだけを選択的に実行する

最近知りましたが、ちょっと前に
「Docker-Composeのプロファイル機能」というのがサポートされていました。

参考|Compose でのプロファイル利用

Docker-composeのプロファイル機能を利用することで、単一のdocker-compose.ymlで、様々なサービスが細かく場合分けできるようになります。

この記事では、以下のポイントについて取り扱っていきます。

            
            1. Docker-composeでのprofilesタグの使い方
2. runサブコマンドの--service-potsオプションによるポート有効化
3. runサブコマンドで立ち上げたコンテナを止めるときのSIGINTエラー対策
4. Docker-composeのcommandで使うシェル形式とexec形式の違い
        

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

Docker-compseのプロファイル機能の使い方

まずはocker-composeのプロファイルの使いどころから解説していきます。

profiles登場以前のサービス切り分け

プロファイル機能登場以前の古いDocker-composeでは、設定YAMLファイルに記述したサービスは基本的にすべて起動してしまう仕様でした。

このため、区分けしたい実行環境ごとに複数の設定ファイルを分けて、
-fオプション指定で起動していました。

例えば以下のような雰囲気です。

            
            $ docker-compose -f A.yml run app hoge

$ docker-compose -f B.yml run server piyo

$ docker-compose -f A.yml -f B.yml -f C.yml run fuga
        
問題として、どのyamlファイルがどの実行環境に対応するのか、利用者が適切に選択する必要があります。

docker-compose.development.bootstrap.ymlなど役割を示した長い名前を付けて、yamlファイルの数を増やしまくるとプロジェクトが見にくくなります。

また複数の設定ファイルのインジェクションオプション(
-f A.yml -f B.yml -f C.yml ...)の指定スタイルが後々大変になり、コマンド引数が長くなるとDocker-composeの恩恵が薄くなります。

そもそも、
「一つのYAMLファイルの中で定義したサービスを手動で切り分けられたら、こんな面倒なことしなくても良いのでは...」という要望に応える機能が今回説明する『プロファイル機能』です。

profilesタグを使ってサービスを細かく切り替える

では本題の
「profiles」の使い方を解説します。

            
            version: "3.9"

services:
  hoge:
    image: hoge-image

  piyo:
    image: piyo-image

  fuga:
    image: hoge-image
    command: echo hoge
    depends_on:
      - piyo
        
という3つのサービス(hoge/piyo/fuga)をもつcomposeファイルがあったとします。

この場合、例えば
docker-compose run fugaで気持ちはfugaだけ立ち上げたかったとしても、3つ全て立ち上がってしまう仕様です。

fugaにプロファイル指定するとfuga(と依存性のあるpiyoも)だけ立ち上げることが可能になります。

            
            version: "3.9"

services:
  hoge:
    image: hoge-image
    ports:
      - "3000:3000"
    tty: true

  piyo:
    image: piyo-image
    ports:
      - "3001:3001"

  fuga:
    image: hoge-image
    command: echo hoge
    ports:
      - "3002:3002"
    depends_on:
      - piyo
  profiles:
      - fuga-only
    #profiles: ["fuga-only"] でもOK
        
としておいて、

            
            $ docker-compose --profile fuga-only run fuga
#👇もしくはプロファイルとサービスの一意に決まる場合、--profile省略可
$ docker-compose run fuga
        
とすることで起動するコンテナが自由に選択できるようになります。

逆に、profilesが割り当てられたサービスはプロファイル指定の手動コマンドでしか立ち上がらないことになります。

            
            #👇これはhogeとpiyoのみ起動
$ docker-compose up -d
        
つまり、アプリケーションの中心となるサービスは常に有効かつ自動的に起動する必要があるため、profilesの割り当てはしないことが基本となります。

プロファイル機能のもっと詳しい使い方は、以下の公式ドキュメントを参考にしてください。

参考|Compose でのプロファイル利用

docker-compose runでportsを使うときは--service-portsオプションを忘れずに

例えば、先程のdocker-compose.ymlの例でいうと、
docker-compose runからfugaサービスを使って、ポート3002番を通じてWebサーバーを起動してみたかったとしましょう。

            
            $ docker-compose run fuga http-server
Server is now starting on http://localhost:3002 ...
        
一見、良い感じに見えますが、http://localhost:3002へブラウザからアクセスしてもちゃんとポートフォワードしてくれません。

コンテナの稼働状況をチェックしてみると、

            
            $ docker ps
CONTAINER ID   IMAGE            COMMAND         PORTS     NAMES
755ce5d439ca   node:alpine3.17  "http-server"             fuga_run_a48da6a76102
#...
        
ポートが開いていないことが分かります。

これはdocker-composeの仕様で、
runサブコマンドはデフォルトではポートマッピングしないという運用上のルールがあるためです。

runサブコマンドからポートを有効化したい場合には、
「--service-ports」を指定しましょう。

            
            $ docker-compose run --service-ports fuga http-server
Server is now starting on http://localhost:3002 ...
        
これでポートが有効になります。

            
            $ docker ps
CONTAINER ID   IMAGE            COMMAND         PORTS                                       NAMES
cdd707747dd5   node:alpine3.17  "http-server"   0.0.0.0:3002->3002/tcp, :::3002->3002/tcp,  fuga_run_a48da6a76102
#...
        

よもやま話〜docker-compose.ymlのversionの意味

先程の
docker-compose.ymlでも、現在の"Latest"を意味する3.9をバージョンにしていましたが、ぶっちゃけどのバージョンを指定してもprofilesが正しく動きます。

というか、以下のサイトで詳しく書かれているように、
docker-composeのバージョンに機能の意味はないので、特に理由もないなら先程のように3.9を付けておけばよいでしょう。

参考サイト|Composeファイルでのversion指定は意味なし

では、なんのためにversionが存在しているかというと、
『後方互換性』のためで、将来YAMLファイルをアップデートしてくれる開発者のための参考情報、という意味合いだそうです。

つまり、docker-compose.ymlを実装する場合、「あの機能はこのバージョンに対応してるのかな?」などと正確なスキーマを選択するのにヤキモキするべきではなく、Composeファイルを設計した時点での最新のスキーマを選択すべき、というのが正しい設計指針です。

ということで、versionは後々の利用者が見たときに、「あー、あのときのバージョンが最新のときに作られたYAMLファイルか、理解した」くらいに使われるスキーマなのにご留意ください。

なお、
version: "3"と書くと、これがなんとなく"Latest"を自動で示す3.*とか^3.0みたいに思ってしまわれる方も多いと思いますが、docker-composeのバージョン規約上は、3 = 3.0です。

ついつい面倒で
version: "3"と未だに書いてしまいますが、マイナーバージョンが0のときだけ省略できる、ということなので正確にバージョン管理したい方はきっちりとしたバージョンを書きましょう。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

コンテナの終了時に出てしまうSIGINTエラーに対応する

こちらはプロファイル機能の話題と直接は関係ないのですが、docker-compose runの話ついでにコンテナ停止時のSIGINTエラーの対策も紹介しておきましょう。

例えば何らかのフロントエンド開発環境で、Dockerコンテナ内で、ビルドしてサーバー起動をしたいというケースを考えてみます。

            
            version: '3.9'

services:
    app: &app
        image: node:alpine3.17
        container_name: node-dev
        user: "node:node"
        environment:
            NODE_ENV: development
        volumes:
            - ./:/usr/src/app
        working_dir: "/usr/src/app"
        ports:
          - "3000:3000"
        tty: true

    cmd:
        <<: *app
        container_name: build-and-serve
        command: >
            bash -c '
                cd myapp &&
                yarn build &&
                echo "Server is getting Started!" &&
                yarn serve
            '
        stop_signal: SIGINT
        profiles:
            - mytool
        
で、これでcmdサービスを使ってソースコードのビルド&サーバー起動を行えるようになります。

            
            $ docker-compose run --service-ports cmd
#...ビルドとサーバー起動が行われる
        
で、これで開発作業も終わって、要らなくなったコンテナを停めようとすると(些細な)問題が起こります。

            
            error Command failed with signal "SIGINT".
        
実際にやってもらえばわかりますが、深刻ではないものの、ちょっとしたエラーが発生しています。

docker-composeのcommandとendpointの使い分け

しばしばDockerユーザーを悩ませるのが、
CMDENTRYPOINTの微妙な作法の違いです。

SIGINTエラーを理解するためには、このCMDとENTRYPOINTの使い方の理解が関係してきますので、すこし話を整理しておきましょう。

docker-composeにおいては、
「command」タグがCMD「entrypoint」タグがENTRYPOINTに対応しています。

どちらもコマンドを実行することに違いはないのですが、使い方には注意が必要になります。

また、コンテナがプロセスを開始する際に、
「シェル形式」「exec形式」という2つの形式があることを頭に入れておくことが重要です。

「シェル形式」とはその名の通り、shやbashなどのシェルを呼び出してから、そこでコマンドを呼び出して処理プロセスを開始する方式のことを指します。

反対に「exec形式」はシェルを呼び出しを行わず、新しい子プロセスとして処理を開始する方式です。

つまり、「シェル形式」は通常のシェルによる処理を行いたい場合に利用し、「exec形式」はシェルを使わない処理で利用します。

これを踏まえた上で、Dockerの「CMD」と「ENTRYPOINT」を見ていきましょう。

まず
CMDには3つの利用形式があります。

            
            1. exec形式(推奨):
    CMD ["実行バイナリ", "パラメータ1", "パラメータ2", "パラメータ3", ...]

2. シェル形式:
    CMD <コマンド>

3. ENDPOINTのパラメータ形式:
    CMD ["パラメータ1", "パラメータ2"]
    (※ENTRYPOINTでコマンド指定が必要)
        
他方、ENTRYPOINTはシェル形式とexec形式の2つが存在しています。

            
            1. exec形式(推奨):
    ENTRYPOINT ["実行可能なコマンド", "パラメータ1", "パラメータ2", "パラメータ3", ...]

2. シェル形式:
    ENTRYPOINT コマンド パラメータ1 パラメータ2 ...
        
と、ここまでは良いでしょう。

もう一つ、Docker関連の記事の中のComposeファイルで割と見かけるcommandタグへのテクニックについても触れておきましょう。

docker-compose.ymlに複数行のシェルスクリプトをcommandタグに埋め込むイディオムとして良く紹介されるテクニックがCMDのシェル形式と
bash -cを組み合わせた、以下のようなものです。

            
            #...
command: >
    bash -c '
        cd myapp &&
        yarn build &&
        echo "Server is getting Started!" &&
        yarn serve
    '
#...
        

参考|Commandを複数行実行する方法

で、CMDのシェル形式を使っているからシェル(bash)で起動出来ているだろうと、一見正しく見えます。

bashのcオプションは
こちらでも詳しく解説してありますが、bash -c 'hoge.sh'とすると、bashは新しいプロセス上で、hoge.shという実行可能な即席スクリプトファイルを作って、それを実行している意味になります。

つまり、一度別プロセスで走ったhoge.shの中身はシェルが評価してくれないので、
シェバンの無いスクリプトファイルは実質exec形式と考えても良いでしょう。

シェバンを正しく指定してシェルのプロセスを生存させる

ではSIGINTエラーの問題を改めて考えていきましょう。

通常起動したサービスを停止させる場合、キーボードから
Ctrlキー+Cキーを押すと、コンテナ起動状態から抜け出すことができます。

その際に、正常にプロセスが終わらなった場合、

            
            error Command failed with signal "SIGINT".
        
というエラーメッセージが出ているわけです。

ここでの
「SIGINT」は、キーボードからCtrl+Cキーでの割り込みシグナルという意味です。

SIGINTエラーとは、Ctrl+Cキー割り込みでプロセスが上手く停まらなかった、というになります。

エラーが出てしまう場合、サーバー起動時に
ps aでプロセスの起動状態を一度確認してみましょう。

            
            $ ps a
    PID TTY      STAT   TIME COMMAND
  13724 pts/1    Sl+    0:00 docker-compose run --rm --service-ports cmd
  13851 pts/0    Ssl+   0:00 node /usr/share/node_modules/yarn/bin/yarn.js serve
  14052 pts/0    Sl+    0:01 /usr/bin/node /usr/src/app/******/node_modules/.bin/vite pr
  14063 pts/0    Sl+    0:00 /usr/src/app/******/node_modules/@esbuild/linux-x64/bin/esb
        

すると
bashでシェルで起動していたはずですが、既に消滅してしまったのかシェルのプロセスがリストに存在していないことが分かります。

当然、Cntl + Cで終了シグナルを受け取るはずのシェルが既に終了しているならば、
SIGINTエラーが出るのも仕方ないことです。

なんでシェル形式でコンテナをプロセスを走らせているにも関わらず、シェルが途中でプロセスの面倒を放棄してそのままいなくなってしまったかというと、
シェル形式と勘違いして隠れexec形式が潜んでいたからです。

もうおわかりかも知れませんが、先程説明していた
bash -cを使った複数行のコマンド埋め込みのテクニック、

            
            #...
command: >
    bash -c '
        cd myapp &&
        yarn build &&
        echo "Server is getting Started!" &&
        yarn serve
    '
#...
        
が分かりにくい原因になっていたのです。

ここはちゃんとシェバン付きのスクリプトファイルとして、全てのプロセスをシェルが面倒を見てくれるように書き直しましょう。

            
            #!/bin/bash

cd myapp
yarn build
echo "Server is getting Started!"
yarn serve
        
とファイルを作っておき、ファイルにパーミッションを与えておきます。

            
            $ chmod +x run.sh
        
で、docker-compose.ymlを修正します。

            
            #...
command: bash -c './run.sh'
#👇シェル形式・exec形式どっちでもOK
command: ['./run.sh']
        

ではサービスを実行してみます。

            
            $ ps a
    PID TTY      STAT   TIME COMMAND
  12710 pts/1    Sl+    0:00 docker-compose run --rm --service-ports cmd
  (☆)12835 pts/0    Ss+    0:00 /bin/bash ./run.sh
  13011 pts/0    Sl+    0:00 node /usr/share/node_modules/yarn/bin/yarn.js serve
  13032 pts/0    Sl+    0:01 /usr/bin/node /usr/src/app/******/node_modules/.bin/vite pr
  13048 pts/0    Sl+    0:00 /usr/src/app/******/node_modules/@esbuild/linux-x64/bin/esb
        
この場合、☆で示したbash run.shから発生したプロセスがまだ生き残っているため、SIGINTシグナルを受信することできるわけです。

commandタグに
bash -cで複数行のシェルスクリプトを埋め込むのはいいのですが、意味もわからずに使うのはやめたほうが良いでしょう。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

以上、まとめますと、この記事では以下のポイントについて解説してきました。

            
            1. Docker-composeでのprofilesタグの使い方
2. runサブコマンドの--service-potsオプションによるポート有効化
3. runサブコマンドで立ち上げたコンテナを止めるときのSIGINTエラー対策
4. Docker-composeのcommandで使うシェル形式とexec形式の違い
        
Dockerユーザーならdocker-composeも必須ツールとなっているので、今後もより使いやすい進化を期待したいものです。