hidemium's blog

日々学んだことをアウトプットする。

Play Framework(Java)でEbeanを使ってManyToManyのリレーションを行う

この記事は Play framework Advent Calendar 2014の 14 日目です。

前回、Ebeanを使ってリレーションを持つテーブルの操作を試してみました。しかし、多対多のリレーションについて、更新時に関係付けしたインスタンスが削除される事象が発生し、しばらくはまってしまいました。今回は、その時に確認した内容についてまとめました。*1

構成

サンプル用のアプリについて

ソースコードhideakihal/play-ebean-relation · GitHubで公開しています。

ManyToManyを試すために以下のの2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

models パッケージに,Message.javaとMember.javaを作成し、以下のように@ManyToManyのアノテーションを追加します。

Message.java

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

controller パッケージに,以下のようにApplication.javaとContacts.javaを作成します。

Application.java

Messageインスタンスを作成、更新するためにApplication.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.save();
public static Result update() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.update();
public static Result delete(Long id) {
	Form<Message> f = new Form(Message.class).fill(
		Message.find.byId(id)
	);
	if (!f.hasErrors()) {
		Message data = f.get();
		if(data != null){
			data.delete();

Contacts.java

Memberインスタンスを作成、更新するためにContacts.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.save();
public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();
public static Result delete(Long id) {
	Form<Member> f = new Form(Member.class).fill(
		Member.find.byId(id)
	);
	if (!f.hasErrors()) {
		Member data = f.get();
		if(data != null){
			data.delete();

サンプルアプリの実行画面

実行画面は以下のようになっています。MessageとMemberに対して作成、読み込み、更新、削除ができるCRUDのアプリになります。

以下は右サイドバーのMemberをクリックした画面になります。messagesには関係付けたMessageインスタンスのIDが表示されます。

f:id:hidemium:20141214100433p:plain

Member側からユーザ名のみを登録することができ、以下はMember>+addをクリックした画面になります。

f:id:hidemium:20141214100421p:plain

以下は右サイドバーのMesssageをクリックした画面になります。memberには関係付けたMemberインスタンスの名前が表示されます。

f:id:hidemium:20141214100341p:plain

Messsage側からはMemberを複数登録できることができ、以下はMesssage>+addをクリックした画面になります。

f:id:hidemium:20141214100358p:plain

問題になったこと

Message側からの作成、更新、削除は問題ありませんでしたが、Member側からMemberインスタンスの更新を行った際に、関係付けたMessageインスタンスが削除されてしまう事象が発生しました。

Model Create Update Delete Annotation
Message @ManyToMany(cascade = CascadeType.ALL)
Member × @ManyToMany(mappedBy="members", cascade = CascadeType.ALL)

画面イメージとして、以下はMember>+editをクリックした画面になります。
以下のようにMemberのpasswordを変更して更新を行います。

f:id:hidemium:20141214113111p:plain

Memberの一覧を見ると、messagesにてMessageのidが削除されていることが分かります。

f:id:hidemium:20141214113125p:plain

原因について

当初は、cascadeやmappedByといったアノテーションの指定方法に問題があるかと思い、いくつかバリエーションを試してみましたが、問題は解決しませんでした。

次に、Memberインスタンスの更新には、Contacts.javaのupdateメソッドを使用しているため、updateメソッド注目してどこでMessageインスタンスが消失するか確認しました。
updateメソッドでは、bindFromRequestでフォームから渡された値を保管するFormインスタンスを作成し、FormインスタンスからMemberインスタンスを取得しています。

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();

このMemberインスタンスの値を確認したとろ、すでにMessageインスタンスが消失した状態となっていました。つまり、bindFromRequestはフォームに入力されてpostされたものしか取得されないため、関係レコードのオブジェクトは取得できないことが原因となっていました。当初は、bindFromRequestで関係レコードのオブジェクトも取得できると勘違いしていたため原因をつかむのに時間がかかってしまいました。

解決方法について

原因は判明しましたが、すでに関係付けたMessageインスタンスをどのように取得するか悩みました。例えば、View側で関係付けたMessageインスタンスを一旦IDにし、IDの配列として渡す方法など考えましたが、あまりいい方法とも思えませんでした。最終的に、MemberのIDからfindByIdでMemberインスタンスを取得することで、更新前に関係付けたMessageインスタンスが取得することが確認できたため、以下のような実装を行いました。

Contacts.java

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		Member name = Member.findById(data.id.toString());
		List<Message> arr = name.messages;
		for(Message m : arr){
			data.messages.add(m);
		}
		data.update();

Member.findById(data.id.toString())で更新前に関係付けたMessageインスタンスを取得しています。また、cascade = CascadeType.ALLを使用しているため、messages側からaddするだけで自動的に更新を行うことができます。

Member.javaのfindByIdメソッドは以下のように作成します。

Member.java

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

細かな実装については、hideakihal/play-ebean-relation · GitHubを確認してみてください。

おわりに

Ebeanを使ったManyToManyのリレーションについて、更新処理が上手くできずにはまっていましたが、なんとか解決することができました。今回の問題は、Stack Overflowに似たような問題が上がっていましたが、欲しい情報が見つからず解決に時間がかかってしまいました。
今回の記事がみなさんのお役に立てられればと思います。

*1:あくまで個人的に検証した結果であるため、正しい実装ではないかもしれません。よい方法があればどなかた教えてください。

Play Framework(Java)でEbeanを使ってリレーションを持つテーブルを操作する

前回、EBeanを使ったデータベースの操作を試してみましたが、Ebeanには複数のModelを関連付けて処理する仕組みも用意されています。Modelクラスのフィールドにアノテーションを付けることで、1対1、1対多、多対多といった関係付けを行うことができます。今回は、Ebeanを使ってリレーションを持つテーブルの操作について試してみました。

構成

Modelの連携の種類について

Modelの連携には以下の種類があります。

  • OneToOne(1対1)
  • OneToMany(1対多)、ManyToOne(多対1)
  • ManyToMany(多対多)

サンプル用のModelについて

それぞれの連携を説明するために、今回以下の2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

まずはmodels パッケージに,以下のようにMessage.javaとMember.javaを作成します。

Message.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Required
  public String name;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}
Member.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity
public class Member extends Model {

  @Id
  public Long id;

  @Required
  public String name;

  @CreatedTimestamp
  public Date postdate;

  public static Finder<Long, Member> find = 
    new Finder<Long, Member>(Long.class, Member.class);
}

OneToOne

OneToOneは、あるModelのエンティティ1つに対して、別のModelのエンティティ1つが対応する関係です。

Message.java

Messageインスタンスに1つのMemberインスタンスが関連するようにするには、Message.javaに以下の内容を追加します。@OneToOneというアノテーションが、OneToOneの関係を指定するものになります。

@OneToOne(cascade = CascadeType.ALL)
public Member member;

エンティティと関連付けられたエンティティに対する処理を自動で行う場合はCascadeTypeを指定します。cascade = CascadeType.ALLを追加することで、参照や更新などのすべての操作に対して処理を自動で行うことができるようになります。この設定がない場合は、操作ごとにApplication.javaに処理を書く必要があります。

Member.java

ユーザ名からMemberインスタンスを取得できるように、Member.javaにfindByIdメソッドを追加します。

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。Message.javaに追加したmemberがmember_idとして作成されていることが分かります。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  message                   varchar(255),
  member_id                 bigint,
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

OneToMany、ManyToOne

OneToManyは、あるModelのエンティティ1つに対して、別のModelの複数のエンティティが対応する関係です。ManyToOneは、逆にあるModelの複数のエンティティが、別のModelのエンティティ1つが対応します。
Memberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@OneToMany、@ManyToOneというアノテーションが、OneToMany、ManyToOneの関係を指定するものになります。

Message.java

@OneToMany
public Member members;

Member.java

@ManyToOne(cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。
Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、DLLが自動的に作成されますが、OneToManyやManyToOneでは変化はありません。

ManyToMany

ManyToManyは、あるModelのの複数のエンティティに対して、別のModelの複数のエンティティが対応する関係です。
複数のMemberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@ManyToManyというアノテーションが、ManyToManyの関係を指定するものになります。Message.javaには、複数のMemberインスタンスを格納するために、配列を追加しました。

Message.java

public List<String> name;

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。
また、ManyToManyではエンティティの主従関係を明示的にするためmappedByを指定しています。mappedBy="members"は、Messageが主、Memberが従であることを示しています。また、mappedByを指定しない場合、エンティティと関連付けした状態でインスタンスを削除をすると参照整合性制約違反が発生するため、自動的に処理をするには指定したほうがいいかもしれません。

Application.java

MemberのIDの配列を取得し、それらのIDからfindByIdでMemberインスタンスを取得しています。取得した複数のMemberインスタンスをmembersにaddにより追加します。saveによりMessageインスタンスに保存し関連付けをすることができます。Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
    		List<String> names = data.name;
    		for (String nm : names){
    			Member m = Member.findById(nm);
    			data.members.add(m);
    		}
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。MessageとMemberの関係テーブルがmessage_memberとして作成されていることが分かります。

create table message_member (
  message_id                     bigint not null,
  member_id                      bigint not null,
  constraint pk_message_member primary key (message_id, member_id))
;

Play Framework(Java)でEbeanを使ってデータベースを操作する

Play FrameworkにはEbeanというORMが標準搭載されており、Ebeanを継承したModelクラスを作成することでデータベースを操作することができます。今回は、Ebeanを使ったデータベースの操作ついて試してみました。

構成

データベース接続の準備

application.confの設定

H2でデータベースを利用するには、application.confの設定を行います。confフォルダ内のapplication.confが対象となります。
application.confに以下の内容を追加します。

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
ebean.default="models.*"

上の2行で、JDBCドライバと、H2のメモリ上にデータが格納されるDBインスタンス、最後の1行で、EBeanを使用する設定を行っています。

モデルの作成

Modelクラスは、play.db.ebean.Modelというクラスとして用意されています。
app/models パッケージに,play.db.ebean.Model を継承したModelクラスを作成することで、Modelクラスを使用できます。app/models パッケージに,以下のようにMessage.javaを作成します。

package models;

import java.util.*;
import javax.persistence.*;

import play.db.ebean.*;
import play.data.format.*;
import play.data.validation.*;
import play.data.validation.Constraints.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Constraints.Required
  public String name;

  public String mail;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}

Modelクラスには、@がついたコードがありますが、これはJavaオブジェクトとデータベースのテーブルの間の関係を定義するアノテーションというものです。
@Entityは、このクラスがエンティティであることを示します。
@idは、記述したフィールドをプライマリキーであることを示します。
@Requiredは、入力必須項目であることを示しています。
@CreatedTimestampは、作成時の時間を格納することを示しています。

コントローラーの作成

controllersパッケージの配下にApplication.javaに以下のようなcreateメソッドを作成します。bindFromRequestのところでフォームから渡された値を保管するFormインスタンスを作成しています。FormインスタンスからMessageインスタンスを取得し、saveを呼び出すことでインスタンスが保存されます。一見インスタンスを操作しているだけですが、実際にはEBeanによりデータベースのテーブルに必要な値が保存されます。
※routersやviewについての説明は省いています。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionの実行

Play frameworkはEvolutionというマイグレーションシステムを持っており、Modelを作成し、ブラウザからアクセスすると、Evolutionを実行するか確認する画面が表示されます。Evolutionを実行すると、自動的に作成されたDLLが実行されmessageテーブルを作成してくれます。DLLはconf/evolutions/default/1.sqlに作成され、messsegeテーブルのcreateは以下のように作成されます。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  mail                      varchar(255),
  message                   varchar(255),
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

デフォルトの状態では、モデル構成が変更される度にデータベースをクリーンする動作になっているため、この機能を停止するには1.sqlで以下の箇所を削除します。

# To stop Ebean DDL generation, remove this comment and start using Evolutions

Play Framework(Java)の開発環境を構築する

Play Frameworkという、Scala/Javaで書かれたWebアプリケーションフレームワークがあります。Play Frameworkは,Ruby on Railsから影響を受けており、生産性の向上を目的として開発されています。今回は、Play Frameworkの開発環境の構築を行ってみました。

構成

Play Frameworkの特徴

Play Frameworkのインストールに入る前に、Play Frameworkの特徴についてまとめてみました。

コーディングのしやすさ、DB操作のしやすさ、開発環境の導入のしやすさなど、生産性を向上させるための工夫がいくつも取り入れられています。

インストール

Java

Java SE Downloadsにアクセスします。Java 8とJava 7がダウンロード可能ですが、今回はJava 7をダウンロードします。
Java Platform, Standard Edition>Java 7uXXにて、JDKの下のDownloadボタンをクリックします。
Java SE Development Kit 7uXXにて、「Accept License Agreement」をチェックし、「jdk-7uXX-windows-x64.exe」をクリックしてダウンロードします。

jdk-7uXX-windows-x64.exe」をクリックして、Javaをインストールします。
インストール時の設定はデフォルトのままで良いかと思います。

Javaの実行ファイルへ環境変数PATHを設定します。

C:\Program Files\Java\jdk1.7.0_55\bin;

インストール後に、コマンドプロンプトにて以下のコマンドを実行し、Javaがインストールされていることを確認します。

> java -version
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

Play Framework

Play frameworkの公式サイトにアクセスします。
最新バージョンはここからダウンロードできますが、旧バージョンはこちらからダウンロードします。
「play-2.2.3.zip」をクリックしてダウンロードします。

ダウンロードした圧縮ファイルを解凍し、任意のディレクトリに配置します。

C:\Users\<your-user-name>\tools\play-2.2.3

Playの実行ファイルへ環境変数PATHを設定します。

C:\Users\<your-user-name>\tools\play-2.2.3;

インストール後に、コマンドプロンプトにて以下のコマンドを実行し、Playがインストールされていることを確認します。

> play help
       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.3 built with Scala 2.10.3 (running Java 1.7.0_55), http://www.playframe
work.com
Welcome to Play 2.2.3!

These commands are available:
-----------------------------
license            Display licensing informations.
new [directory]    Create a new Play application in the specified directory.

You can also browse the complete documentation at http://www.playframework.com.

Eclipse

Pleiadesにアクセスし、Eclipseの本体と日本語化プラグインがセットになったPleiades All in Oneをダウンロードします。
Eclipse 4.3 Kepler」をクリックしてダウンロードします。

ダウンロードした圧縮ファイルを解凍し、任意のディレクトリに配置します。

C:\Users\<your-user-name>\tools\eclipse-4.3

Play frameworkアプリの起動

Play frameworkの新規アプリケーションを作成します。
play newコマンドの後に、アプリケーション名を付けて作成します。このあたりはRailsと似ています。

> cd C:\Users\<your-user-name>\dev
> play new PlayApp
       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.3 built with Scala 2.10.3 (running Java 1.7.0_55), http://www.playframe
work.com

The new application will be created in C:\Users\your-user-name\dev\PlayApp

アプリケーション名を聞かれますが、変更はしないのでここはEnterキーを押しておきます。

What is the application name? [PlayApp]
> [Enterキー]

ScalaJavaのどちらで開発するか聞かれますが、今回はJavaの開発環境を構築するので2を入力して、Enterキーを押します。

Which template do you want to use for this new application?

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 2 [Enterキー]
OK, application PlayApp is created.

Have fun!

アプリケーションが起動できるか確認しておきます。
また、Playのエラーが表示されるときの文字化けを防ぐために、環境変数_JAVA_OPTIONSの設定を行っておきます。

> set _JAVA_OPTIONS="-Dfile.encoding=SJIS"
> cd C:\Users\<your-user-name>\dev\PlayApp
> play run
:
--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

ブラウザから、以下のアドレスにアクセスすると、playのデフォルトページに接続することができます。

http://localhost:9000

Ctrl+Dキーでアプリケーションを停止することができます。

Eclipseプロジェクトへのインポート

Eclipseで開発する場合、Eclipseのプロジェクトに変換する必要があります。
以下のコマンドを実行して、Eclipseのプロジェクトに変換します。

> play eclipse

Eclipseを起動し、メニューのファイル>インポートをクリックします。
「既存プロジェクトをワークスペースへ」を選択して、次へをクリックします。
「ルートディレクトリの選択」に作成したアプリケーションのパスを入力します。
「プロジェクト」に作成したアプリケーションが表示されるので、完了をクリックします。
Eclipse上にアプリケーションのプロジェクトが表示されます。

DockerでKandanとHubotを動かす

チャット上のHubotを通じて運用を自動化するChatOpsという運用スタイルが注目されています。ローカルでチャット上で動くHubotを試すために、HipChatクローンのKandanとHubotを動作するDockerfileを書いてみました。

構成

  • Ubuntu 14.04
  • Docker 1.3.0
  • Kandan 1.2
  • hubot 2.4.7
  • hubot-kandan adapter 1.0

Dockerfile

Dockerfileを以下のように作成します。
ソースコードこちら@githubで公開しています。

Dockerfileの構成

Dockerfileの構成は以下の通りです。

docker-kandan-hubot
├──Dockerfile
├──hubot
│     ├──hubot-scripts.json
│     ├──hubot.conf
│     ├──hubot.sh
│     └──package.json
├──kandan
│     ├──database.yml
│     └──kandan.conf
├──sources.list
└──sshd.conf

Dockerfile

Dockerfile以外にもいくつかファイルはありますが、今回はDockerfileのみ説明します。

$ docker-kandan-hubot
$ vi Dockerfile
FROM ubuntu:14.04

MAINTAINER hideakihal

# install basic package
ADD sources.list /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
    apt-get -y install \
    openssh-server \
    supervisor \
    build-essential \
    curl \
    unzip \
    git-core \
    ruby1.9.1-dev \
    ruby-bundler \
    libxslt-dev \
    libxml2-dev \
    libpq-dev \
    libsqlite3-dev \
    gcc \
    g++ \
    make && \
    curl -sL https://deb.nodesource.com/setup | bash - && \
    apt-get install -y nodejs && \
    apt-get clean

# root user
RUN echo 'root:root' | chpasswd

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
RUN sed -ri 's/PermitRootLogin without-password/PermitRootLogin yes/g' /etc/ssh/sshd_config

# hubot user
RUN useradd -m -s /bin/bash hubot 
RUN echo 'hubot:hubot' | chpasswd
RUN echo 'hubot ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/hubot
ENV HOME /home/hubot

# install Kandan
USER hubot
WORKDIR /home/hubot
RUN wget https://github.com/kandanapp/kandan/archive/v1.2.tar.gz
RUN tar xvf v1.2.tar.gz
RUN mv kandan-1.2/ kandan
RUN echo 'gem: --no-rdoc --no-ri' >> /home/hubot/.gemrc
WORKDIR /home/hubot/kandan
RUN sudo gem install execjs
RUN sudo sed -ri "s/gem 'pg'/gem 'sqlite3'/g" /home/hubot/kandan/Gemfile
RUN bundle install --without development test
ADD kandan/database.yml /home/hubot/kandan/config/database.yml
RUN RAILS_ENV=production bundle exec rake db:create db:migrate kandan:bootstrap
RUN sed -ri 's/config.serve_static_assets = false/config.serve_static_assets = true/g' \
    /home/hubot/kandan/config/environments/production.rb
RUN RAILS_ENV=production bundle exec rake assets:precompile
RUN RAILS_ENV=production bundle exec rake kandan:boot_hubot
RUN RAILS_ENV=production bundle exec rake kandan:hubot_access_key | awk '{print $6}' > hubot-key

# install Hubot
WORKDIR /home/hubot
RUN wget https://github.com/github/hubot/archive/v2.4.7.zip
RUN unzip v2.4.7.zip
WORKDIR /home/hubot/hubot-2.4.7
RUN sudo npm install -g mime@1.2.4 qs@0.4.2
RUN npm install
RUN make package
WORKDIR /home/hubot/hubot-2.4.7/hubot
RUN git clone https://github.com/kandanapp/hubot-kandan.git node_modules/hubot-kandan
RUN npm install faye
RUN npm install ntwitter
ADD hubot/package.json /home/hubot/hubot-2.4.7/hubot/package.json
ADD hubot/hubot-scripts.json /home/hubot/hubot-2.4.7/hubot/hubot-scripts.json
RUN sed -ri 's/"version":     "1.0",/"version":     "1.0.0",/g' node_modules/hubot-kandan/package.json
USER root
ADD hubot/hubot.sh /etc/profile.d/hubot.sh
RUN awk '{print "export HUBOT_KANDAN_TOKEN="$0}' /home/hubot/kandan/hubot-key >> /etc/profile.d/hubot.sh 

# add supervisor config file 
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf
ADD kandan/kandan.conf /etc/supervisor/conf.d/kandan.conf
ADD hubot/hubot.conf /etc/supervisor/conf.d/hubot.conf
RUN awk '{print " HUBOT_KANDAN_TOKEN="$0}' /home/hubot/kandan/hubot-key >> /etc/supervisor/conf.d/hubot.conf 

# expose ports
EXPOSE 22 3000

# define default command
CMD supervisord -n
  • 最新のKandanだと、Hubotからうまく認証できないため、Kandan 1.2を使用します。
  • KandanのDBにはsqliteを使用しています。
  • hubotのアクセスキーは一度ファイルに出力し、supervisorの定義ファイルで環境変数を設定するために使用しています。
  • hubot-kandan adapterは、最新のhubotには対応していないため、hubot 2.4.7を使用します。

インストール

GitHubにあるソースコードを取得して、Dockerイメージをビルドします。

$ git clone https://github.com/hideakihal/docker-kandan-hubot.git
$ sudo docker build -t kandan-hubot docker-kandan-hubot

コンテナを起動します。KandanとSSHの使用ポートである3000と22を起動時に指定します。

$ sudo docker run -d -p 22 -p 3000:3000 kandan-hubot

ブラウザから、以下のアドレスにアクセスすると、Kandanのログイン画面に接続することができます。

http://<your-host-ip>:3000

f:id:hidemium:20141103012204p:plain

デフォルトでユーザ名「Admin」、パスワード「kandanappadmin」でログインできます。
ログインすると、Hubotがチャットに参加していることが確認できます。

f:id:hidemium:20141103012459p:plain

動作確認のため、「@hubot ping」や「@hubot pug me」などを投稿すると、hubotが応答してくれます。

f:id:hidemium:20141103012800p:plain

Hubotスクリプトの追加

Hubotのスクリプトを追加した場合、Hubotの再起動が必要になります。今回supervisorによりHubotを起動しているため、以下のコマンドを実行し、Hubotを再起動します。

$ ssh root@localhost -p <your-container-22port>
password:root
# supervisorctl restart hubot

おわりに

Dockerを使ってKandan上でHubotを動かす環境を作ることができました。ローカルで試してみたい場合や、Hubotのスクリプトをチャット上で検証してみたい場合などに使えるか思います。

※SlackとKandanを比較した場合、Slackの出来がいいため、どうしてもという理由がない限りSlackを使ったほうがいいようです。

ShipyardでDockerのWebUIを構築してみた

Dockerでどのようなコンテナが起動しているか、コンテナの状態を確認するためにWebUIを提供するツールがいくつか開発されています。今回は、Shipyardをインストールし、コンテナの状態がどのように確認できるのか試してみました。

構成

  • Ubuntu 14.04
  • Docker 1.3.0
  • Shipyard 2.0.3

Shipyardのインストール

Dockerデーモンの外部接続を許可する必要があるので、/etc/default/dockerのDOCKER_OPTSに以下の内容を追加します。

$ vi /etc/default/docker
DOCKER_OPTS='-H tcp://0.0.0.0:4243/ -H unix:///var/run/docker.sock'

設定を反映させるため、Dockerを再起動します。

$ service docker restart

ShipyardはRethinkDBという分散データベースを使用しているので、こちらをインストールします。上のコマンドは、RethinkDB用のデータ格納コンテナ、下のコマンドはRethinkDBコンテナを起動しています。

$ docker run -it -d --name shipyard-rethinkdb-data --entrypoint /bin/bash shipyard/rethinkdb -l
$ docker run -it -P -d --name shipyard-rethinkdb --volumes-from shipyard-rethinkdb-data shipyard/rethinkdb

Shipyardコンテナを起動します。今回はソケット通信によりホストの情報を取得するため、「-v /var/run/docker.sock:/docker.sock」を追加します。

$ docker run -it -v /var/run/docker.sock:/docker.sock -p 8080:8080 -d --name shipyard --link shipyard-rethinkdb:rethinkdb shipyard/shipyard

これらのコマンドを実行すると、DockerHubからイメージを取得して、コンテナを起動してくれます。※なお、Shipyard 2からはAgentは不要になったようです。

ブラウザから、以下のアドレスにアクセスすると、ShipyardのWebUIに接続することができます。

http://<your-host-ip>:8080

f:id:hidemium:20141026191535p:plain

デフォルトでユーザ名「admin」、パスワード「Shipyard」でログインできます。

Engineの追加

初回ログイン時はDockerのホスト情報が登録されていないので、登録を行います。
「Engines」タブをクリックして、「ADD」ボタンをクリックします。
Name、Label、CPUs、Memoryは任意の値を入力し、Addressに「unix:///docker.sock」を入力し、「ADD」ボタンをクリックします。

f:id:hidemium:20141026195815p:plain

登録ができると、以下のようにホスト情報が表示されます。

f:id:hidemium:20141026195853p:plain

Containerの操作

設定に問題がなければ、「Containers」タブをクリックすると、以下のように起動中のコンテナの一覧が表示されます。また、コンテナをUIから起動する、「Deploy」ボタンが用意されています。

f:id:hidemium:20141026195934p:plain

WebUIを提供しているshipyard/shipyardのコンテナをクリックすると、以下のように表示されます。restart、stop、destroy、scaleといったボタンが用意されています。

f:id:hidemium:20141026200105p:plain

コンテナの一覧画面に戻って、「Deploy」ボタンをクリックすると、以下のような画面になります。Dockerのイメージ名やホスト名、CPUやメモリのリソースの制限について設定することができます。

f:id:hidemium:20141026200114p:plain

Dashbordの操作

EngineとContainerで設定した値を元に、CPUやメモリのリソースの利用状況を円グラフで表示してくれます。
※Dockerが稼動しているサーバのCPU使用率やメモリ使用率をリアルタイムで表示するものではないようです。(このあたりの仕様はまだよく分かってません。)

f:id:hidemium:20141026200123p:plain

おわりに

通常コンテナの状況はCUIベースで確認していますが、複数ユーザでどのようなコンテナが起動しているか確認するにはShipyardがあると便利かと思います。ただ、WebUIベースで細かなことするにはまだまだ難しいかもしれません。

Figで複数のDockerコンテナをまとめて起動して、Redmineを構築する

以前sameersbn/redmineというRedmineのイメージを試してみましたが、アプリケーションとMySQLでコンテナを分離した場合、複数のコンテナ起動や、DBの作成、マイグレーションの実行が個別に必要となり手間がかかっていました。

そこで今回は、マルチコンテナ構成ツールであるFigを用いて、アプリケーションとMySQLでコンテナを分離した場合でも、Redmineを一括起動するDockerfileを書いてみました。

構成

  • Ubuntu 14.04
  • Docker 1.0.0
  • Fig 0.5.2

コンテナとDockerfileの割り振り

Redmineの起動のためには、DBサーバ、アプリケーションサーバ、Webサーバが必要となります。これらを1つのコンテナの中で動かす事も出来ますが、安定性等を考え、以下のように2つのコンテナで動かす事にします。

また、Dockerfileのメンテナンス性を考え、APコンテナはRubyまでを導入したイメージと、そのイメージをベースにRedmineを導入したイメージに分離させています。Redmineの設定を変更したい場合、Rubyのインストールを省くことができます。

  • DBイメージ  : 共通設定、MySQL
  • Rubyイメージ : 共通設定、Ruby
  • APイメージ  : Redmine、Nginx、Unicorn (Rubyイメージをベースに差分ビルド)

Dockerfile

Dockerfileを以下のように作成します。
ソースコードこちら@githubで公開しています。

Dockerfileの構成

Dockerfileの構成は以下の通りです。
このほかにもコンテナに追加する定義ファイルもいくつかあります。

fig-redmine 
├──fig.yml           ... Figのコンテナ起動定義ファイル
├──mysql
│ └──Dockerfile      ... MySQL用のDockerfile
├──ruby     
│ └──Dockerfile      ... Ruby用のDockerfile 
└──redmine     
   └──Dockerfile      ... Redmine用のDockerfile 

MySQL

$ vi mysql/Dockerfile
FROM ubuntu:14.04

MAINTAINER hidemium

# install basic package
ADD sources.list /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
    apt-get -y install \
      mysql-server \
      openssh-server \
      supervisor && \
      apt-get clean

# root user
RUN echo 'root:root' | chpasswd

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
ADD id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
RUN sed -ri 's/PermitRootLogin without-password/PermitRootLogin yes/g' /etc/ssh/sshd_config

# config supervisor
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD mysqld.conf /etc/supervisor/conf.d/mysqld.conf
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf

# install MySQL
RUN rm -rf /var/lib/mysql/mysql
RUN rm -rf /var/lib/apt/lists/*
ADD my.cnf /etc/mysql/my.cnf
ADD mysql-listen.cnf /etc/mysql/conf.d/mysql-listen.cnf
RUN chown -R mysql:mysql /var/lib/mysql
RUN chmod 700 /var/lib/mysql

# initialize MySQL 
RUN mysql_install_db --user mysql > /dev/null
RUN /usr/bin/mysqld_safe & \
    sleep 3 && \
    mysqladmin -u root password "root" && \
    mysql -u root -proot -e "GRANT ALL ON *.* TO 'root'@'172.17.%.%' IDENTIFIED BY '' WITH GRANT OPTION;" && \
    mysql -u root -proot -e "CREATE USER 'redmine'@'%.%.%.%' IDENTIFIED BY 'redmine';" && \
    mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS redmine_production DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;" && \
    mysql -u root -proot -e "GRANT ALL PRIVILEGES ON redmine_production.* TO 'redmine'@'%.%.%.%';" && \
    mysql -u root -proot -e "FLUSH PRIVILEGES;"

# expose ports
EXPOSE 22 3306 

# define default command.
CMD ["supervisord", "-n"]
  • MySQLのDockerfile内でRedmine用のユーザ作成、データベース作成を定義しています。

Ruby

$ vi ruby/Dockerfile
FROM ubuntu:14.04

MAINTAINER hidemium

# install basic package
ADD sources.list /etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
    apt-get -y install \
    openssh-server \
    supervisor \
    build-essential \
    curl \
    unzip \
    git-core \
    subversion \
    mercurial \
    libcurl4-openssl-dev \
    libreadline-dev \
    libssl-dev \
    libxml2-dev \
    libxslt1-dev \
    libyaml-dev \
    zlib1g-dev && \
    apt-get clean && \
    curl -O http://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz && \
    tar -zxvf ruby-2.1.2.tar.gz && \
    cd ruby-2.1.2 && \
    ./configure --disable-install-doc && \
    make && \
    make install && \
    cd .. && \
    rm -r ruby-2.1.2 ruby-2.1.2.tar.gz && \
    echo 'gem: --no-document' > /usr/local/etc/gemrc

# root user
RUN echo 'root:root' | chpasswd

# install sshd
RUN mkdir -p /root/.ssh /var/run/sshd
ADD id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 700 /root/.ssh
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config
RUN sed -ri 's/PermitRootLogin without-password/PermitRootLogin yes/g' /etc/ssh/sshd_config

# install supervisor
RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d
ADD sshd.conf /etc/supervisor/conf.d/sshd.conf

# install bundler
RUN \
  echo 'gem: --no-rdoc --no-ri' >> /.gemrc && \
  gem install bundler
  • rbenvを使ってRubyをインストールする方法もありますが、複数バージョンのRubyを使い分ける必要はないため、ソースからのビルドしています。DockerはImmutableなので、異なるバージョンのRubyを使用したい場合は、コンテナを作り直せばいいという考えです。
  • Redmineに必要なパッケージもこちらでインストールしておきます。

Redmine

$ vi redmine/Dockerfile
FROM ubuntu-ruby:14.04

MAINTAINER hidemium

ENV REDMINE_VERSION 2.5-stable
ENV REDMINE_HOME /var/redmine
ENV SETUP_DIR /app/setup
ENV RAILS_ENV production

# install basic package
RUN apt-get update && \
    apt-get -y install \
      nginx \
      imagemagick \
      libmagickcore-dev \
      libmagickwand-dev \
      libmysqlclient-dev

# install Redmine
RUN git clone -b ${REDMINE_VERSION} https://github.com/redmine/redmine.git ${REDMINE_HOME}
WORKDIR /var/redmine 
RUN echo 'gem "mysql2", "~> 0.3.11"' >> Gemfile
RUN echo 'gem "unicorn"' >> Gemfile
RUN bash -l -c 'bundle install --without development test'

# install plugins
RUN mkdir -p ${SETUP_DIR}
ADD setup/themes_install ${SETUP_DIR}/themes_install
ADD setup/plugins_install ${SETUP_DIR}/plugins_install
RUN chmod 755 ${SETUP_DIR}/*
RUN ${SETUP_DIR}/themes_install
RUN ${SETUP_DIR}/plugins_install

# add config file
ADD config/database.yml ${REDMINE_HOME}/config/database.yml
ADD config/configuration.yml ${REDMINE_HOME}/config/configuration.yml
ADD config/unicorn.rb ${REDMINE_HOME}/config/unicorn.rb
RUN mkdir ${REDMINE_HOME}/public/plugin_assets

# install nginx
ADD redmine.conf /etc/nginx/conf.d/redmine.conf
RUN rm -f /etc/nginx/sites-enabled/default
RUN ln -s /etc/nginx/sites-available/redmine /etc/nginx/sites-enabled/redmine
RUN echo "daemon off;" >> /etc/nginx/nginx.conf

# add supervisor config file 
ADD unicorn.conf /etc/supervisor/conf.d/unicorn.conf
ADD nginx.conf /etc/supervisor/conf.d/nginx.conf

# expose ports
EXPOSE 22 80

# define default command
CMD bash -l -c 'bundle install --without development test' && \
    bash -l -c 'bundle exec rake generate_secret_token' && \
    mv plugins plugins-1 && \
    bash -l -c 'bundle exec rake db:migrate RAILS_ENV=${RAILS_ENV}' && \
    mv plugins-1 plugins && \
    bash -l -c 'bundle exec rake redmine:plugins:migrate RAILS_ENV=${RAILS_ENV}' && \
    supervisord -n
  • 「FROM ubuntu-ruby:14.04」で先に作成したRubyイメージをベースにしています。
  • RedmineソースコードGitHubリポジトリから取得しています。
  • 一度bundle installすれば、Dockerのキャッシュが使われるので再実行が短縮できます。そのため、最初にbundle installを実行しています。ただし、RedmineのGemfileは、database.yml内の情報からデータベースの種類を判別し、必要なgemを選択しています。database.ymlを先に追加すると処理が失敗するため、「'gem "mysql2", "~> 0.3.11"' >> Gemfile」で追加しています。
  • テーマとプラグインのインストールは、「themes_install」と「plugins_install」というシェルから実行しています。
  • RedmineをNginx + Unicornで動作させる設定を行っています。
  • CMDでマイグレーションコマンドを定義することで、Redmineコンテナ起動時にマイグレーションを自動で行うことができます。

Dockerfileのビルド

以下のコマンドにより、Dockerfileの取得とDockerイメージのビルドを行います。

$ git clone https://github.com/hideakihal/docker-fig-redmine.git
$ cd docker-fig-redmine
$ sudo docker build -t ubuntu-mysql:14.04 mysql
$ sudo docker build -t ubuntu-ruby:14.04 ruby
$ sudo docker build -t ubuntu-redmine:14.04 redmine

Fig

Dockerイメージの用意ができたため、ここからはFigを使用方法を説明します。

インストール

FigはPythonで書かれているので、パッケージ管理をするpipをインストールしておきます。pipを使ってFigをインストールします。

$ sudo apt-get install -y python-pip python2.7-dev
$ sudo pip install -U fig 

fig.yml

複数のコンテナをどのように起動するかはfig.ymlというファイルで定義します。
fig.ymlは、以下のような構成になり、起動するDockerイメージやポート番号、コンテナ間通信の設定などが行えます。
fig.ymlのリファレンスは公式サイトにあります。

$ vi fig.yml
db:
  image: ubuntu-mysql:14.04
  ports:
    - 22
    - 3306
web:
  image: ubuntu-redmine:14.04
  ports:
    - 22
    - 80
  environment:
    - DB_HOST=db_1
  links:
    - db

上記の設定を通常のDockerコマンドで表現すると以下のようになります。
毎回以下のようなコマンドを打つのは大変なので、とても簡単できるようになります。

$ sudo docker run -d -p 22 -p 3306 --name figredmine_db_1 ubuntu-mysql:14.04
$ sudo docker run -d -p 22 -p 80 -e "DB_HOST=db_1" --link figredmine_db_1:db_1 --name figredmine_web_1 ubuntu-redmine:14.04

環境変数について

fig.yml内で、DB_HOSTという環境変数を定義しています。これは、APコンテナのdatabase.ymlにDBコンテナをホスト名を渡すために行っています。
当初は、以下のようにlinkオプションの起動でDBコンテナのIPアドレスを取得していましたが、DBコンテナ名が変わるとDockerイメージのリビルドが必要でした。

$ vi /var/redmine/config/database.yml
production:
  adapter: mysql2
  database: redmine_production
  host: <%= ENV.fetch('DB_1_PORT_3306_TCP_ADDR') %>
  port: <%= ENV.fetch('DB_1_PORT_3306_TCP_PORT') %>

Figのissueを見てみると、linkオプションにより/etc/hostsも変更されることが分かり、以下のように設定を修正しました。

$ vi /var/redmine/config/database.yml
production:
  adapter: mysql2
  database: redmine_production
  host: <%= ENV.fetch('DB_HOST') %>
  port: 3306
...

Dockerの公式ドキュメントを見てみると、コンテナのポート番号やIPをもった環境変数と/etc/hostsが追加されることが記載されています。
実際のコンテナでも確認したところ、確かにコンテナの環境変数と/etc/hostsが更新されていることが分かりました。

root@be9520e48d64:~# env
DB_1_PORT_3306_TCP_ADDR=172.17.3.117
DB_1_PORT_3306_TCP_PORT=3306
...

ホスト名が、db_1のように「コンテナ名_数字」となっていますが、これはFigの仕様で同じDockerイメージをスケールさせる場合に末尾の数字がカウントアップされていきます。今回はスケールさせないため、末尾の数字は1として表示されます。

root@be9520e48d64:~# cat /etc/hosts
172.17.3.117    db_1
172.17.3.117    figredmine_db_1
...

複数コンテナの一括起動

以下のコマンドを実行し、複数コンテナを一括起動します。

$ sudo fig up -d

起動の進捗を確認する場合は、以下のコマンドを使用できます。

$ sudo fig logs
Attaching to figredmine_db_1, figredmine_web_1
web_1 | Don't run Bundler as root. Bundler can ask for sudo if it is needed, and
web_1 | installing your bundle as root will break this application for all non-root
web_1 | users on this machine.
web_1 | Your Gemfile lists the gem vcard (~> 0.2.8) more than once.
web_1 | You should probably keep only one of them.
web_1 | While it's not a problem now, it could cause errors if you change the version of just one of them later.
web_1 | Your Gemfile lists the gem capybara (~> 2.1.0) more than once.
web_1 | You should probably keep only one of them.
web_1 | While it's not a problem now, it could cause errors if you change the version of just one of them later.
...

コンテナの起動を確認する場合、以下のコマンドを使用できます。APとDBのコンテナが起動していることが分かります。
docker psコマンドより情報がまとまってシンプルに表示できます。

$ sudo fig ps
    Name                   Command               State               Ports
---------------------------------------------------------------------------------------
figredmine_db_1    supervisord -n                Up      49177->22/tcp, 49178->3306/tcp
figredmine_web_1   /bin/sh -c bash -l -c '...    Up      49179->22/tcp, 49180->80/tcp

上記で、80番ポートにフォワードされているポート番号を確認し、ブラウザから、「http://localhost:ポート番号」にアクセスし、Redmineのログイン画面が表示されることを確認します。

本番環境で動作させる場合

fig upコマンドを再実行すると、既存のコンテナは削除され、コンテナが再作成されます。開発中は不要なコンテナの削除を自動的に行ってくれるため便利ですが、本番環境で現行のコンテナを残して、Blue-Green Deploymentを行いたい場合は不便です。また、開発環境と本番環境でコンテナの起動設定を変えたい場合などがあるかと思います。
そこで、fig upコマンドでは、-fオプションによりデフォルトパスのfig.yml以外のfig.ymlを指定することができます。

以下のように、fig-production.yml(任意)を作成します。
上記のfig.ymlとは異なり、コンテナ名の変更や、データの永続性のため共有ディレクトリの設定を追加しています。また、DB_HOSTのコンテナ名も合わせて変更します。

$ vi fig-production.yml
dbBlue:
  image: ubuntu-mysql:14.04
  ports:
    - 22
    - 3306
  volumes:
    - /path/in/host:/var/lib/mysql
webBlue:
  image: ubuntu-redmine:14.04
  ports:
    - 22
    - 80
  environment:
    - DB_HOST=dbBlue_1
  volumes:
    - /path/in/host:/var/redmine/files
  links:
    - dbBlue
  • コンテナ名は記号は不可で、英数字しか使用できないようです。

以下のコマンドを実行し、本番環境用のコンテナを起動します。

$ sudo fig -f fig-production.yml up -d

おわりに

Figによって、複雑になったdocker runコマンドを定義ファイルにまとめることができ、複数コンテナが連携するDockerfileの開発がとても楽になりました。さらに、複数コンテナのDockerfileとFigの定義ファイルを1つのフォルダで管理できるため、リポジトリもとても見やすくなったかと思います。

Figを開発したOrchard社は、Docker社は買収されたようなので、今後の展開に注目しています。