ホットリロードされるGo言語環境のDockerコンテナを作成する

Blog Single

開発環境を整えるためにDockerイメージを作成したはいいものの、ローカルのファイルを変更してもコンテナ内のコードが変更されないため、コードを変更する度に毎回イメージを作り直す必要があるといったことがありました。

Docker Composeを使うことにより解決できたので、実際に簡単なwebアプリケーションを用いて、ローカルの変更でホットリロードされるDockerコンテナを動かすまでの手順を紹介していきます。

環境

$ go version
go version go1.10.4 darwin/amd64
$ docker --version
Docker version 18.09.0, build 4d60db4

プログラム

今回動かすプログラムとして、echo というGo言語のwebフレームワークを利用します。

webフレームワークのインストール

$ go get -u github.com/labstack/echo/...

プログラム
server.go

package main

import (
    "net/http"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World From Echo!")
    })
  e.GET("/tom/:name", func(c echo.Context) error {
    name := c.Param("name")
        return c.String(http.StatusOK, "Hello," + name + "!")
    })
    e.Logger.Fatal(e.Start(":1323"))
}

起動

$ go run server.go

出力

$ curl localhost:1323/hello/Tom
Hello,Tom!

これで、アプリが立ち上がりましたが、ファイルを変更しても起動時のビルドの状態のまま実行され、変更が反映されません。

そこで、freshというツールを使い、ファイルの変更に応じて自動ビルドされるようにします。

インストール

$ go get github.com/pilu/fresh

実行

$ fresh

これでローカルではファイルの変更に応じて自動ビルドされホットリロードできるので、これをDockerコンテナとして実行するための準備をしていきます。

パッケージ管理

depというパッケージ依存管理ツールのコマンドdep initでパッケージの依存関係を記したGopkg.tomlGopkg.lockファイルを生成します。
このファイルによって、アプリケーションに必要なパッケージの管理が容易にできるようになります。

パッケージ依存管理ツールのインストール

$ go get -u github.com/golang/dep/cmd/dep
$ dep init

Dockerfileの作成

次に、Dockerイメージの元となるDockerfileを作成して、Dockerイメージを作成します。

# 元となるイメージを指定
FROM golang:1.10

# 作業ディレクトリを指定
WORKDIR /go/src/go-echo-docker-example
# 左側(ローカル)のディレクトリをイメージの作業ディレクトリ(ここではWORKDIRで指定した/go/src/go-echo-docker-example)にコピー
COPY . .

# イメージのビルド時に実行するコマンド
# depのインストールとdep ensureで依存関係のパッケージをインストール
RUN go get -u github.com/golang/dep/cmd/dep \
  && dep ensure

# freshのインストール
RUN go get github.com/pilu/fresh
# イメージからコンテナを作成する際に実行
CMD ["fresh"]

このDockerfileからDockerイメージを作成してコンテナを実行することもできますが、それだとイメージ作成時のソースが動くコンテナが実行されるため、ファイルの変更を行う度にイメージを作成し直す必要が出てきます。

Dockerfileをイメージから作成する場合は、以下のように行いますが、今回はDocker Composeを用いて作成するため、コマンドでイメージを作成する必要はありません。

# カレントディレクトリ(./)のからgo-echo-docker-exampleというイメージ名のイメージを作成
$ docker build ./ -t go-echo-docker-example

Docker Compose

次に、Docker Composeを用いてローカルの変更をコンテナに反映されるようにします。

Docker Composeというのは、複数のコンテナを連携する際の設定などを定義し、実行する機能です。

Dockerは、1コンテナ1プロセスのみ実行されるという制約のため、一つのコンテナでwebサーバーを立ち上げて、更にDBサーバーも立ち上げるといったことができません。
そのため1つのコンテナにすべての機能を詰め込まず複数のコンテナを起動する必要があるため、Docker Composeが使われます。

ここでは一つのコンテナしか動かしませんが、Docker Composeを用いることによって、ローカルのディレクトリをコンテナのディレクトリにマウントすることによって、コンテナにもファイルの変更を適用させる事ができるため利用します。

Docker Composeで実行するコンテナやコマンドなどの設定はdocker-compose.ymlというファイルで定義して、そのファイルを元に実行を行います。

docker-compose.yml

# docker-compose.ymlファイルのフォーマットのバージョンを指定
version: '3'
# services下にコンテナで作られるサービスを定義
services:
  app:
    build: .
    # (左)ホストマシンのディレクトリを(右)コンテナのディレクトリにマウント
    volumes:
    - ./:/go/src/go-echo-docker-example
    # 公開するportを指定 (左 ホストマシンのポート, 右 コンテナのポート)
    ports:
    - "1323:1323"

docker-compose.ymlファイルがあるディレクトリで以下のコマンドを実行するとコンテナが起動します。

$ docker-compose up

実際に接続して、正しく動いているか確認します。

$ curl http://localhost:1323/hello/Tom
Hello,Tom!

ホットリロードが実現されているか確認するために、server.goにファイルの一部に変更を加えます。

e.GET("/hello/:name", func(c echo.Context) error {
        name := c.Param("name")
        //return c.String(http.StatusOK, "こんにちは、" + name + "さん!")
        return c.String(http.StatusOK, "Hello," + name + "!")
    })

出力

$ curl http://localhost:1323/hello/Tom
こんにちは、Tomさん!

ローカルの変更がコンテナに反映されていることが確認できました。

まとめ

今回は、シンプルな構成のアプリケーションだったので、Dockerfileやdocker-compose.ymlがシンプルな記述になりましたが、実際のアプリケーションでは複雑になり、様々な設定の記述が必要になります。
Dockerコンテナはホストマシンのカーネルを利用して1プロセスとして実行しているという都合上、Vagrantを使ったような仮想マシンの代わりになるものではないですが、簡単に環境を作れ、すぐに立ち上がり気軽にコンテナを削除する事ができるというのはかなり便利だと思うので、積極的に使って手に馴染むようにしたいです。

参考

Posted by MatsumotoKazuki
PHPやJavaで開発を行っているエンジニア。 LOLというゲームの試合を観戦するのが好きです。

Other Posts: