vibe.dを使ったサービスをDockerコンテナ上で動かして停止を検出して終了処理する

Dockerコンテナで実行されているサービスは `sudo docker stop <container>` あるいは `sudo docker-compose down` などによって停止することができます。
vibe.dをDockerコンテナ上でサービスとして実行する際、安全に停止するために停止処理を仕込みたい場合があります。

以下、Linux(Ubuntu)での実行を前提とします。


# Dockerコンテナが停止される際何が起こるか

> コンテナ内のメイン・プロセスは SIGTERM を受信します。一定期間経過すると、 SIGKILL を送ります。
> http://docs.docker.jp/engine/reference/commandline/stop.html

`sudo docker stop <container>` を行うと、メインプロセス(PID 1)に対してSIGTERMシグナルを送信します。
正常に終了させたい場合、アプリケーションはこれ(SIGTERM)を正しく処理して終了処理を行う必要があります。
また、ドキュメントの一定期間というのは、デフォルトだと10秒ですので、10秒以内に終了処理を行うのが良いでしょう。

幸いなことに、vibe.dで書かれたアプリケーションはデフォルトでこれ(SIGTERM)を処理するよう仕込まれています。
これは、コマンドライン上で実行した際、`Ctrl-C`でサービスを止める際にSIGINTが発生して止めるのと同じです。
なお、`docker kill <container>`や一定期間後の SIGKILL で停止する際には本当に処理の途中で即死します。

# vibe.dサービスの停止処理を書く場所
vibe.dはアプリケーションがSIGTERM(あるいはSIGINT)を受信すると、イベントループを終了します。したがって、終了処理は `runApplication();` や `runEventLoop();` の後に書きます。

仮に終了処理が、`Exit service!`とログ出力することだとすると、以下のような感じになります。

```d:source/app.d
import vibe.vibe;

void index(HTTPServerRequest req, HTTPServerResponse res)
{
	import diet.html;
	import std.string: chompPrefix, outdent;
	import std.array: appender;
	auto contents = appender!string;
	contents.compileHTMLDietString!(`
		doctype 5
		html
			head
				title Welcome
			body
				h1 Welcome
	`.chompPrefix("\n").outdent);
	res.writeBody(contents.data, "text/html");
}

void main()
{
	auto router = new URLRouter;
	router.get("/", &index);
	
	auto settings = new HTTPServerSettings;
	settings.port = 80;
	settings.bindAddresses = ["0.0.0.0"];
	
	auto listener = listenHTTP(settings, router);
	scope (exit)
		listener.stopListening();
	
	runApplication();
	
	// 終了処理としてログ出力する
	logInfo("Exit service!");
}
```

# コンテナイメージの作り方

アプリケーションで正しくSIGTERMを受信するには、注意点があります。
ドキュメントにはkillコマンドの注意点として記載されていますが、stopも同じです。

> `ENTRYPOINT` と `CMD` を シェル 形式で実行している場合は、 `/bin/sh -c` のサブコマンドとして実行されていますので、シグナルを受け取ることができません。つまり、（シェル形式では）コンテナの PID 1 は Unix シグナルを受け取りません。
> http://docs.docker.jp/engine/reference/commandline/kill.html

とのことですので、コンテナイメージは `ENTRYPOINT` や `CMD` の書き方に注意が必要です。

なお、以下の例では、アプリケーションのバイナリ名は `test-app` です。

```docker:Dockerfile
FROM ubuntu:latest

RUN DEBIAN_FRONTEND=noninteractive \
    apt-get update && \
    apt-get install -y libssl1.1

COPY ./test-app /root/test-app

RUN chmod +x /root/test-app

WORKDIR /root
ENTRYPOINT ["/root/test-app"]
```

ここで、 `ENTRYPOINT` が `ENTRYPOINT /root/test-app` とか `ENTRYPOINT "/root/test-app"` とか書くとシェル形式になるためNGです。
`CMD` にしたい場合も同じで、 `CMD ["/root/test-app"]` のようにしないといけません。

# サービス実行して確認する

Dockerfileが書けたらビルドして、サービスを立ち上げてみます。

```sh
# サービスのビルド
dub build
# コンテナイメージのビルド: イメージ名 = test-image
docker build -t test-image .
# コンテナ立ち上げ: コンテナ名 = test-container / --rm で終了後コンテナ削除 / -d でサービス化
docker run --name test-container --rm -d test-image
```

そして、ちゃんとstop時の処理ができているかを以下で確認します。
今回の場合、ログ(=アプリケーションの標準出力/標準エラー出力=コンテナのログ)に出力されることを確認するので、`docker logs -f --tail=10 test-container`とかしてログを出しながら終了します。
```sh
docker logs -f --tail=10 test-container & docker stop test-container 
```

結果:
```
[1] 26666
[main(----) INF] Listening for requests on http://0.0.0.0:80/
Vibe was run as root, and no user/group has been specified for privilege lowering. Running with full permissions.
[main(----) INF] Received signal 15. Shutting down.
[main(----) INF] Exit service!
[main(----) INF] Stopped to listen for HTTP requests on 0.0.0.0:80
test-container
[1]+  Done                    docker logs -f --tail=10 test-container
```

ちゃんと`[main(----) INF] Exit service!`と出力されることが確認できます。