CLOVER🍀

That was when it all began.

Ubuntu Linux 14.04 LTSで始めるDocker Compose

今までDockerを使う時はdockerコマンド単体で扱っていたのですが、最近少し複数コンテナを扱いそうな機会が増えてきまして。これを機に、Docker Composeを試してみようかと思います。

Overview of Docker Compose

Docker Composeを使うと、複数のコンテナをまとめて管理できるのだとか。

早速使ってみます。

参考)
docker-composeを使うと複数コンテナの管理が便利に - Qiita
Docker Compose チュートリアル - ようへいの日々精進XP

インストール

ドキュメントに沿って、まずはインストール。Docker自体は、あらかじめインストールが済んでいるものとします。

Install Docker Compose

なお、当方の環境はUbuntu Linux 14.04 LTSです。

で、ドキュメントのインストールコマンドだと

curl -L https://github.com/docker/compose/releases/download/VERSION_NUM/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

と書いているのですが、「VERSION_NUM」のところは自分で置き換えてあげる必要があります。

どのバージョンを使うかは、こちらを見て選ぶことになるのでは。

https://github.com/docker/compose/releases

今回は、1.4.1を使用します。

また、このコマンドは

/usr/local/bin/docker-compose

というファイルに書き込めと言っているのですが、rootで実行しているわけでもないのでsudo越しになります。

というわけで、結果このようなコマンドになりました。

$ sudo sh -c 'curl -L https://github.com/docker/compose/releases/download/1.4.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose'

権限変更。

$ sudo chmod +x /usr/local/bin/docker-compose

オプションな手順ではありますが、コマンド補完ができるようにbashの設定も追加しておきます。こちらも案内されている手順は

curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

ですが、やはりこのままだとroot権限がないので、以下のようにsudo越しになりました。

$ sudo sh -c "curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose"

バージョン確認。

$ docker-compose --version
docker-compose version: 1.4.1

以上で、インストールはおしまいです。

アップグレードする時は、削除して再度インストールするみたいです。

Upgrading

Uninstallation

動かしてみる

では、Docker Composeを動かしてみます。Docker Composeを使うには、「docker-compose.yml」というYAMLファイルを作成することになるみたいです。

適当なディレクトリを作成して、そちらに移動。

$ mkdir docker-compose-getting-started
$ cd docker-compose-getting-started

ドキュメントに載っているDjango、Rails、Wordpressにチャレンジするのもちょっと微妙な気がしたので、ここは自分で書いてみました。

コンテナは、MySQL

https://hub.docker.com/r/library/mysql/

それから簡単なWebアプリケーションとしてGroovyでスクリプトを書いて試すことにします。

https://hub.docker.com/r/webratio/groovy/

今回は、自分でDockerイメージは作りません。docker-compose.ymlを書いて、その中に「build:」があれば以下のコマンドでDockerイメージのビルドが

$ docker-compose build

またDockerイメージのpullは以下のコマンドで行うそうなのですが、今回は使いません。

$ docker-compose pull

で、用意したdocker-compose.ymlはこちら。
docker-compose.yml

web:
  image: webratio/groovy:2.4.4
  ports:
    - "18080:8080"
  volumes:
    - ./web/script:/source
    - ./web/graperoot:/graperoot
  links:
    - mysql
  command: "rest-server.groovy"
mysql:
  image: mysql:5.6.26
  ports:
    - "3306"
  environment:
    MYSQL_ROOT_PASSWORD: my-secret-pw

webと付けた方のコンテナは、後述しますがgroovyコマンドで実行するスクリプト(ここでは「rest-server.groovy」)でサーバーが立ち、ポート8080でリッスンします。これをホスト側の18080ポートにあてています。また、いくつかVOLUMEを設定し(webratio/groovyイメージ参照)、mysqlとリンク設定を入れています。

最後のcommandは、DockerfileのENTRYPOINTの引数となります。

mysqlは、ポート3306を定義していますが、ホスト側の定義を書いていないので、ホスト側の適当なポートにマッピングされるようになります。パスワードは、環境変数で設定。

で、用意したGroovyスクリプトはこちら。
web/script/rest-server.groovy

@Grab('mysql:mysql-connector-java:5.1.36')
@GrabConfig(systemClassLoader = true)
import groovy.sql.Sql

@Grab('org.jboss.resteasy:resteasy-jackson2-provider:3.0.13.Final')
@Grab('org.jboss.resteasy:resteasy-jdk-http:3.0.13.Final')
import javax.ws.rs.*
import javax.ws.rs.core.*

import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder

import com.sun.net.httpserver.HttpServer

class Entry {
  int id
  String name
}

@Path("mysql")
class MySqlResource {
  @GET
  @Path("select")
  @Produces(MediaType.APPLICATION_JSON)
  def select() {
    def sql =
      Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver')

    def result = sql.rows("SELECT * FROM entry ORDER BY id").collect { row ->
      new Entry([id: row.id, name: row.name])
    }

    sql.close()

    result
  }

  @PUT
  @Path("insert")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  def insert(Entry entry) {
    def sql =
      Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver')

    sql.execute("INSERT INTO entry(id, name) VALUES(${entry.id}, ${entry.name})")

    sql.close()
  }
}

def server = HttpServer.create(new InetSocketAddress(8080), 10)
def contextBuilder = new HttpContextBuilder()

[MySqlResource].each {
    contextBuilder.deployment.actualResourceClasses.add(it)
}

def context = contextBuilder.bind(server)
server.start()

println("[${new Date()}] RestEasyJdkHttpd startup[${server.address}].")

作りはかなり適当ですが、ご愛嬌…。

JDBCの接続URLでのホスト名は、Dockerでリンクを貼った物にしています。

      Sql.newInstance('jdbc:mysql://mysql:3306/test', 'root', 'my-secret-pw', 'com.mysql.jdbc.Driver')

このスクリプトを、先ほどVOLUMEで定義した「./web/script」配下に置いています。

では、docker-compose.ymlファイルをカレントディレクトリに置いた状態で、「up」。

$ docker-compose up
dockercomposegettingstarted_mysql_1 is up-to-date
dockercomposegettingstarted_web_1 is up-to-date
Attaching to

起動したまま、待ち状態になります。「docker run -d」のようにするには、同じく「-d」オプションを付与します。

いったんCtrl-Cで終了して、コンテナ削除。

$ docker-compose rm

「-d」付きで起動。

$ docker-compose up -d
Creating dockercomposegettingstarted_mysql_1...
Creating dockercomposegettingstarted_web_1...

今度は、端末に制御が戻ってきます。

「docker-compose ps」で、コンテナの一覧が見れます。

$ docker-compose ps
               Name                              Command               State            Ports          
------------------------------------------------------------------------------------------------------
dockercomposegettingstarted_mysql_1   /entrypoint.sh mysqld            Up      0.0.0.0:32795->3306/tcp 
dockercomposegettingstarted_web_1     groovy -Dgrape.root=/grape ...   Up      0.0.0.0:18080->8080/tcp

ログを確認するには、「docker-compose logs」。

$ docker-compose logs

停止。

$ docker-compose stop
Stopping dockercomposegettingstarted_web_1... done
Stopping dockercomposegettingstarted_mysql_1... done

先ほど1度出しましたが、「docker-compose rm」でコンテナ削除。

$ docker-compose rm
Going to remove dockercomposegettingstarted_web_1, dockercomposegettingstarted_mysql_1
Are you sure? [yN] y
Removing dockercomposegettingstarted_web_1... done
Removing dockercomposegettingstarted_mysql_1... done

また、各コマンドは後ろにコンテナ名の指定ができるので、以下のように特定のコンテナのみを起動することも可能です。

$ docker-compose up -d mysql
Creating dockercomposegettingstarted_mysql_1...

複数指定も可。

コンテナ単位に起動していなくても、ログを個別に見れます。

$ docker-compose logs mysql

おお、便利だ!!

動作確認

で、コンテナ操作をしただけで、上に乗っているものを何も動かしていないので、ちょっと動かしてみます。

まずは起動。

$ docker-compose up -d
Starting dockercomposegettingstarted_mysql_1...
Creating dockercomposegettingstarted_web_1...

MySQLにテーブルなどが何もないので、コンテナに入って作成。

$ docker exec -it dockercomposegettingstarted_mysql_1 /bin/bash

データベース、テーブル作成。

# mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.05 sec)

mysql> USE test;
Database changed
mysql> CREATE TABLE entry(id INT, name VARCHAR(255), PRIMARY KEY(id));
Query OK, 0 rows affected (0.06 sec)

ホスト側から確認してみます。

$ curl http://localhost:18080/mysql/select
{}

とりあえず、エラーにはなりません、と。

データ登録。

$ curl -XPUT -H 'Content-Type: application/json' http://localhost:18080/mysql/insert -d '{ "id": 1, "name": "Taro" }'
$ curl -XPUT -H 'Content-Type: application/json' http://localhost:18080/mysql/insert -d '{ "id": 2, "name": "Jiro" }'

確認。

$ curl http://localhost:18080/mysql/select
[{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]

OKそうです。

インスタンス数を増やしてみる

Docker Composeでは、scaleコマンドで実行中のインスタンスを増やすことができるようです。

scale

webを3つにしています。

$ docker-compose scale web=3
The "web" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating and starting 2... error
Creating and starting 3... error

ERROR: for 2  Cannot start container bb19e6f7acab86eaad22eafe51c3ba39e25f1d525c796f276a07a1053ee98afa: Bind for 0.0.0.0:18080 failed: port is already allocated 
ERROR: for 3  Cannot start container 6bab879c70bcb455e5d48329d5d3ba00141b9df456b7358a1a584cbf05d35d80: Bind for 0.0.0.0:18080 failed: port is already allocated 
Removing dockercomposegettingstarted_web_2... done
Removing dockercomposegettingstarted_web_3... done

まあ、ホスト側に18080でバインドしようとするので、失敗しますよね…。

いったん停止。

$ docker-compose stop
Stopping dockercomposegettingstarted_web_1... done
Stopping dockercomposegettingstarted_mysql_1... done

仕方がないので、web側のポートをいったんホスト側で固定するのをやめます。

web:
  image: webratio/groovy:2.4.4
  ports:
    - "8080"

再度起動。

$ docker-compose up -d
Starting dockercomposegettingstarted_mysql_1...
Recreating dockercomposegettingstarted_web_1...

web側も適当なポートに割り当てられました。

$ docker-compose ps
               Name                              Command               State            Ports          
------------------------------------------------------------------------------------------------------
dockercomposegettingstarted_mysql_1   /entrypoint.sh mysqld            Up      0.0.0.0:32800->3306/tcp 
dockercomposegettingstarted_web_1     groovy -Dgrape.root=/grape ...   Up      0.0.0.0:32801->8080/tcp

webを3つにしてみます。

$ docker-compose scale web=3
Creating and starting 2... done
Creating and starting 3... done

確認。

## ポート確認
$ docker-compose ps
               Name                              Command               State            Ports          
------------------------------------------------------------------------------------------------------
dockercomposegettingstarted_mysql_1   /entrypoint.sh mysqld            Up      0.0.0.0:32800->3306/tcp 
dockercomposegettingstarted_web_1     groovy -Dgrape.root=/grape ...   Up      0.0.0.0:32801->8080/tcp 
dockercomposegettingstarted_web_2     groovy -Dgrape.root=/grape ...   Up      0.0.0.0:32802->8080/tcp 
dockercomposegettingstarted_web_3     groovy -Dgrape.root=/grape ...   Up      0.0.0.0:32803->8080/tcp

## web#1
$ curl http://localhost:32801/mysql/select
[{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]

## web#2
$ curl http://localhost:32802/mysql/select
[{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]

## web#3
$ curl http://localhost:32803/mysql/select
[{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"}]

できてそうです。

なるほど、確かにこれは便利そうですね。覚えておきましょう。