LoginSignup
14

More than 3 years have passed since last update.

docker run -v のホストパスの制限について調べた

Last updated at Posted at 2017-05-01

この情報は古く、現在のバージョンではソースコードが改変されているので、実情と異なる場合があります。

パスに:を含むディレクトはマウント出来ない

Dockerコンテナにホストのディレクトリをマウントしようとしたら以下のようなエラーが発生しました。

% docker run --rm --name phpfpm -it -p 8000:8000 -v $(pwd):/var/www/backend phpfpm /bin/bash
docker: Error response from daemon: invalid bind mount spec "/Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend": invalid mode: /var/www/backend.
See 'docker run --help'.

var/www/backendmode として不正だということのようです。

Docker Compose でも発生する

% docker-compose up --build
Building php
Step 1/2 : FROM php:5.6-fpm
 ---> 3458979c7744
Step 2/2 : RUN docker-php-ext-install mysqli
 ---> Using cache
 ---> f223a09ff0c9

Successfully built f223a09ff0c9
Successfully tagged backend_php:latest
backend_adminer_1 is up-to-date
Creating backend_php_1 ...
Creating mysql         ...
Creating backend_nginx_1 ... error
Creating mysql           ... error
Creating backend_php_1   ... error

ERROR: for mysql  Cannot create container for service db: invalid mode: /var/lib/mysql

ERROR: for backend_php_1  Cannot create container for service php: invalid mode: /var/www/html

ERROR: for nginx  Cannot create container for service nginx: invalid mode: /var/www/html

ERROR: for db  Cannot create container for service db: invalid mode: /var/lib/mysql

ERROR: for php  Cannot create container for service php: invalid mode: /var/www/html
ERROR: Encountered errors while bringing up the project.

docker-compose.ymlvolumes: ディレクティブで指定していても同じ原因でエラーが発生うする。

-v オプションの仕様

-v オプションの仕様についてドキュメントで確認します。
https://docs.docker.com/engine/reference/run/#volume-shared-filesystems

-v, --volume=[host-src:]container-dest[:]: Bind mount a volume.

こちらからもわかるように -v <ホストのパス>:<コンテナのパス>:<オプション> が最長の指定方法で、区切り文字に : が使われています。
例えば -v /home/user/project/htdocs:/var/www/html:ro と指定すると、
ホストの /home/user/project/htdocs がコンテナの /var/www/html にReadOnlyでマウントされます。

エラーメッセージの mode とは <options> の指定部分の事のようです。

ghq を使ってリポジトリを管理していた

ホストのディレクトリは ghq で管理しているリポジトリのディレクトリでした。
ghq はクローンしてきたリポジトリを管理してくれるツールで、 % ghq get URL でリポジトリをクローンしてくれます。

% ghq get ssh://my.repo.io:12345/myrepo/backend.git

今回使っているリポジトリはポートを変えてホスティングしているためドメインにはポート番号が含まれています。
このリポジトリのフルパスを確認するとこのようになります。

% ghq list -p 
/Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend

このポート指定の : が事の原因だったようです。

結論

  • 回避する方法がない。
  • コードにコミットしてプルリクを出すしか無い。

結論は回避方法がないということで、: が含んでいると意図通りにマウント出来ないことがわかりました。
以下はその調査について書いていますので興味のある方はどうぞ。

dockerのソースコードを読む

: のエスケープ方法を検索してもめぼしい情報が見つからなかったのでDockerのソースコードを調べてみることにしました。
DockerのRepositoryをcloneして該当箇所を探して確認します。

エラーメッセージの出力

闇雲にソースコードを読んでもしかたありません。
現状で有力な情報はエラーメッセージなので invalid bind mount spec の出力箇所を探してみることにします。

# Docker Repositry のディレクトリで実行する
% grep -rin 'invalid bind mount spec' .
./runconfig/config.go:98:           return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)

どうやらここでエラーが出力されているようです。

runconfig/config.go
    for _, spec := range hc.Binds {
        if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil {
            return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)
        }
    }

Dockerコマンドは Go言語 で書かれているんですね!!

-v 引数の解析

エラー出力箇所の前後をみると volume.ParseMountRaw() の結果がエラーのようです。
ParseMountRaw は命名からマウント引数の字句解析をしていそうです。次はこれを探してみます。

% grep -rin 'ParseMountRaw' .
# 結果を抜粋
./volume/volume.go:209:// ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a
./volume/volume.go:212:func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
./volume/volume_test.go:154:func TestParseMountRawSplit(t *testing.T) {

volumne.go に定義されています。

volume/volume.go
func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
    arr, err := splitRawSpec(convertSlash(raw))
    if err != nil {
        return nil, err
    }

ParseMountRaw() の処理内容

関数のコメントにはこのように書かれています。

// ParseMountRaw parses a raw volume spec (e.g. -v /foo:/bar:shared) into a
// structured spec. Once the raw spec is parsed it relies on ParseMountSpec to
// validate the spec and create a MountPoint

要約すると ボリュームの仕様(例. -v /foo:/var:shared)を解析しParseMountSpecを使ってMountPointを作成します。 ということのようです。

ここの処理を理解すれば原因がわかりそうです。順に見てきす。
まずは splitRawSpec() です。

% grep -rin 'splitRawSpec' volume
volume/volume.go:213:   arr, err := splitRawSpec(convertSlash(raw))
volume/volume_unix.go:130:func splitRawSpec(raw string) ([]string, error) {
volume/volume_windows.go:93:fun

volume/volume_unix.go を開きます。

splitRawSpec()

go#volume/volume_unix.go
func splitRawSpec(raw string) ([]string, error) {
    if strings.Count(raw, ":") > 2 {
        return nil, errInvalidSpec(raw)
    }

    arr := strings.SplitN(raw, ":", 3)
    if arr[0] == "" {
        return nil, errInvalidSpec(raw)
    }
    return arr, nil
}

引数に : が2を超えて含まれる時、エラーが発生していることがわかります。
つまり -v /some.domain.io:8080/foo:/bar/:shared という指定が出来ないことがわかります。

今回のエラーは -v /Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend の指定で : は2つなのでここでのエラーが原因ではありません。

次の strings.SplitN(raw, ":", 3) で引数が : で分割され配列に格納されています。

引き続き処理を見ていきましょう。

配列サイズによる分岐

go#volume/volume.go
    var spec mounttypes.Mount
    var mode string
    switch len(arr) {
    case 1:
        // Just a destination path in the container
        spec.Target = arr[0]
    case 2:
        if ValidMountMode(arr[1]) {
            // Destination + Mode is not a valid volume - volumes
            // cannot include a mode. e.g. /foo:rw
            return nil, errInvalidSpec(raw)
        }
        // Host Source Path or Name + Destination
        spec.Source = arr[0]
        spec.Target = arr[1]
    case 3:
        // HostSourcePath+DestinationPath+Mode
        spec.Source = arr[0]
        spec.Target = arr[1]
        mode = arr[2]
    default:
        return nil, errInvalidSpec(raw)
    }

splitRawSpec() の戻り値の配列サイズによって処理が分岐しています。
-v /Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend このように指定しているので配列サイズは3になるはずなので

// このように代入されるはずです
        spec.Source = "/Users/masayuki/.ghq/my.repo.io"
        spec.Target = "12345/myrepo/backend"
        mode = "/var/www/backend"

mode のバリデーション

続いて mode のバリデーションが行われます。ここで mode = "/var/www/backend" が代入されているので ValidMountMode() が通らずエラーが発生してしまいます。

    if !ValidMountMode(mode) {
        return nil, errInvalidMode(mode)
    }

ValidMountMode() を見ると仕様の通り ro,rw などのモードのチェックを行っています。
https://github.com/moby/moby/blob/master/volume/volume_unix.go#L61-L96

つまり

ホストパスに : が含まれているとそこで区切られてしまうので、意図通りの指定が出来ないことがわかります。

現状で出来る対策

  • 絶対パスに : が含むものはマウントしない
  • ghq で clone しない

ソースを見る限り絶対パスに : を含むディレクトリはマウント出来ないので、それを避けるしかありません。ファイルも同様です。

次に出来ることはプルリクを出すことなので、それを目指します。おしまい。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14