docker-compose の bind mount を1行で書くな

4 min read読了の目安(約4000字

TL;DR

  • docker-compose では bind mount の構文が "short", "long" の2通りあるが, それぞれ挙動が異なる
  • docker-compose.ymlvolumes に略記法 (short syntax) を用いると, コンテナ内で non-root user を用いる際にエラーの発見が遅れる可能性があるので避けよう

概要

Docker アプリケーション (コンテナ) は, 実行時にホストのファイルやディレクトリをコンテナ内にマウントし, ホストとのファイル共有を行うことができます (bind mount).

docker-compose では, Compose ファイル (デフォルトでは docker-compose.yml) にマウント情報を記述することができますが, 構文によっては意図せぬ挙動や, パーミッションエラーを引き起こす原因となります.

bind mount の構文

Compose ファイルの bind mount の構文は2通り存在し, どちらを用いても同じマウント設定を行うことができます.

ここでは, ホストの cwd に存在する ./config ディレクトリを, コンテナ内に /config としてマウントする場合を考えます.

short syntax

ホストのディレクトリとコンテナ内のマウントパスを, コロン区切りの1行で記述する方法です.

services:
  app:
    image: nginx
    volumes:
      - "./config:/config"

long syntax

マウントの設定をマッピングの配列として記述する方法です.

short syntax に加え, より詳細なオプションの設定が可能です.

services:
  app:
    image: nginx
    volumes:
      - type: bind
        source: "./config"
        target: "/config"

問題点

構文ごとの挙動の違い

short syntax, long syntax のいずれも同じ bind mount を実現できますが, 実は両者には大きな挙動の違いが存在します.

long syntax での設定では, アプリケーション起動時にホストに対象 (ディレクトリ, ファイル) が存在しない場合はエラーを吐いて終了しますが, short syntax での設定では, 自動でホストにディレクトリを作成します.

これは, いくつかの奇妙なバグを引き起こす原因となります.

ファイルをマウントしたつもりが…

例えば, ホストにある単一のファイルをコンテナ内にマウントしようとした場合, short syntax では, もしホストに対象ファイルが存在しなかった場合, 代わりにディレクトリを作成します. short syntax により自動で作成されるのは常にディレクトリです.

以下の Compose ファイルによって, ホストのソケットファイルをコンテナ内にマウントする場合を考えます:

services:
  app:
    image: nginx
    volumes:
      - "./app.sock:/var/run/app.sock"

このとき, もしホストに ./app.sock が存在しない場合, Docker は ./app.sock/ というディレクトリを自動で作成し, コンテナ内にマウントします. よって, ファイルを参照しようとしたコンテナ内のアプリケーションはエラーを吐き, ユーザは無限に苦しむことでしょう.

ディレクトリが自動で作成されたのに…

用途によっては, ホストにマウント対象のディレクトリが存在しない場合に自動作成される機能はありがたいかもしれません. しかし, ホストが Linux の場合, 作成され, コンテナ内にマウントされるディレクトリの所有者は root です[1]. よって, コンテナ内で non-root ユーザを用いている場合に, 権限が足らずにマウントされたディレクトリにアクセスできない場合があり, コンテナ内のアプリケーションはエラーを吐き, ユーザは無限に苦しむことでしょう.

エラーの発見が遅れる

こういったファイル/ディレクトリ関連のエラーは, コンテナ内のアプリケーションが対象にアクセスしようとして初めて発覚する場合がほとんどでしょう. よって, docker-compose up によるアプリケーション起動時に発見できなかったエラーが, アプリケーションがしばらく動いた後に遅れて発覚し, 問題をより大きくし, また, トラブルシュートを困難にする可能性があります.

対策

short syntax を使わず, 常に long syntax を使いましょう! 配布を考えている Compose ファイルなら尚更です.

long syntax では, マウント対象がホスト上に存在しない場合, docker-compose up 時に適切なエラーを吐き, 早期発見を促してくれます:

$ sudo docker-compose up
Building with native build. Learn about native build in Compose here: https://docs.docker.com/go/compose-native-build/
Creating run_nginx_1 ... error

ERROR: for run_nginx_1  Cannot create container for service nginx: invalid mount config for type "bind": bind source path does not exist: /home/sarisia/run/config

ERROR: for nginx  Cannot create container for service nginx: invalid mount config for type "bind": bind source path does not exist: /home/sarisia/run/config
ERROR: Encountered errors while bringing up the project.

親切ですね.

まとめ

Compose ファイルに2行多く書くだけで, 多くのエラーが防げそうです. 今すぐ bind mount を1行で書くのをやめて, long syntax を使っていきましょう!

余談1

本文中の volumes の short syntax, long syntax はそれぞれ docker run コマンドの --volume , --mount オプションに対応しています. Docker でも, --volume または -v オプションの使用を辞め, --mount オプションを使うことで, 似たようなトラブルを事前に回避することが可能です:

$ sudo docker run --mount "type=bind,src=$(pwd)/config,dst=/config" -d nginx
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /home/sarisia/run/config.
See 'docker run --help'.

余談2

実は, Docker Desktop for Windows の WSL2 Integration で動作している Docker 及び docker-compose では, long syntax もしくは --mount オプションを用いても, 問答無用でホストにディレクトリが自動で作成され, エラーを吐いてくれません.

バグっぽいので Issue を建てていますが, 今のところは運用に気をつけるしか無さそうです.

参考

Compose file version 3 reference

Use bind mounts

How to mount a single file in a volume

脚注
  1. user namespace remapping を用いている場合など, 一部例外があります ↩︎