DockerComposeで管理されたコンテナをsystemdで動かすときのコツ

# はじめに
DockerCompose は、関連するDockerイメージを複数実行できる便利なものですが、システム上の一部のサービスをdockerで運用するような場合、他のサービスと同様にOS標準のサービス管理機構で管理したくなります。

今回、[Redmine](http://www.redmine.org/)のDockerイメージ([sameersbn/docker-redmine](https://github.com/sameersbn/docker-redmine))をsystemd経由で実行できるようにしてみました。

単にsystemdで動かすだけなら難しくありませんが、具体的な運用シーンでの要求(後述)を実現するときに苦労したポイントを中心に紹介します。

## 何を実現したいのか

Redmineには、メールによる通知機能があります。筆者の所属する会社では Gmail を使用していますが、送信するだけでもアカウントが必要になります。送信専用の共用アカウントがありますが、そのアカウント情報はあまりばらまきたくないため、社内ネットワークにGmailにリレーできる送信専用のメールサーバをPostfixで構築し、そのメールサーバ経由でメール送信できるようにしました。

Redmineのコンテナがそのリレーメールサーバと同じホストで動いている時には、ホストの物理IPアドレス(下の図では 192.168.1.2)ではなく、DockerコンテナからみたホストのIPアドレス(172.17.0.1)を指定しなければ、メールサーバにアクセスできません。

![network.png](https://qiita-image-store.s3.amazonaws.com/0/222989/1fab4f86-0c0c-f24a-7ca2-a881c3d181a0.png)

また、このDockerホストのIPアドレス（正確にはDockerの仮想ブリッジに接続されたホスト側のIPアドレス）はDockerの設定によって変化する可能性があるため、ハードコードにしたくありません。このため、Redmineからみたホストは、IPアドレスではなくホスト名で参照できるようにすることが望ましいです。

また、使用するのは一般公開されているDockerイメージなので、極力ここに手をつけたくありません。

要件をまとめると以下のようになります。特にホストのIPアドレスを動的に取得してコンテナに渡す方法で困ったのですが、考える過程や情報原も含めて紹介したいと思います。

- 一般公開されているDockerイメージを使用する。このイメージには手をつけない
- DockerコンテナはSystemdでサービスとして実行したい
- Dockerコンテナ上から、ホストをホスト名で参照できるようにしたい

なお、Docker 本来の思想からいえば、メールサーバも Docker 上で動かしてリンクするのが筋かもしれません。

しかし、今回のように「基本はDockerは使わないが、部分的にDockerで動かしたいサービスがある」といったニーズもあるように思います。


### 実行環境
今回は、以下のような環境で試しました。

```console:実行環境
# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)

# uname -r
3.10.0-693.2.2.el7.x86_64

# docker -v
Docker version 1.12.6, build 85d7426/1.12.6

# docker-compose -v
docker-compose version 1.14.0, build c7bdf9e
```

# 導入編


## systemd について

systemd について、あまりなじみがない方もいるかもしれませんので、簡単に紹介しておきます。

### systemd とは
systemd は、最近のほとんどのLinuxディストリビューションで採用されているサービス管理機構です。サーバ向けディストリビューションとしてシェアの高いRHEL(Red Hat Enterprize Linux)やCentOSでは、2014年に公開された Version7 から systemd が採用されています。

従来は、「SysV init」と呼ばれるUNIXの伝統的なサービス管理機構が多く利用されており、サービスの制御には `service` コマンドが使われていました。RHEL/CentOS7 でも `service` コマンドは健在ですが、内部の動きは systemd で置き換えられています。systemd では、`service` コマンドに替わり、`systemctl` コマンドでサービスを制御します。

Linux の古い解説書ではサービス制御に `service` コマンドが使用されているので、このあたりの変化を知らないと混乱するかもしれません。

### systemd の特長

systemd の大きな特長は、各サービスの依存関係(たとえば、データベースが起動していなければ、それを使用するアプリケーションは起動できない)を考慮した上で、できるかぎり並列に起動処理を行うようにしていることです。このため、RHEL/CentOS 7 以降ではOSの起動/終了がそれ以前と比較して圧倒的に速くなりました。

また、今回のようにサービスを作る側にとって大きな変更点は、サービス制御用のシェルスクリプトをいちいち作成する必要がなくなったことです。「ユニットファイル」とよばれる、iniファイル形式の定義ファイルを作成して登録するだけで、プロセスをサービスとして管理させることができるようになります。

【参考】[なぜsystemdなのか？ \| インフラ・ミドルウェア \| POSTD](http://postd.cc/why-systemd/)


### systemd のデメリット
一方でデメリットもあります。サービス制御用のシェルスクリプトが排除されたことで、自由度が失われたことです。従来であればシェルスクリプトを駆使して実現できていたことが、systemdの制約で簡単に実現できないこともあります。その場合は、systemd の機能を深く調べていかねばなりません。今回詰まったのはまさにこの部分でした。

幸い、systemd は man ページのドキュメントが非常に充実しています。systemd(3) を開くと、一番最後の SEE ALSO にこれだけの man ページが紹介されています。すべて英語ですが丹念に読めば必要な情報は得られます。(私もまだすべて読んでいません)

| systemd 関連の man ページ |
|:--|
| systemd(3) |
| systemd-system.conf(5) |
| locale.conf(5) |
| systemctl(1) |
| journalctl(1) |
| systemd-notify(1) |
| daemon(7) |
| sd-daemon(3) |
| systemd.special(5) |
| pkg-config(1)|
| kernel-command-line(7) |
| bootup(7) |
| systemd.directives(7) |


## Docker Compose について
[Docker Compose](https://docs.docker.com/compose/)は、Dockerのサブプロダクトの1つで、複数の関連するDockerコンテナを束ねて制御できるようにするものです。

Redmineの場合、PostgreSQLやMySQLなどのデータベースが必須です。Dockerの運用思想としては1つのコンテナで1つのアプリケーションを動かすことが基本なので、この場合、2つのコンテナを動かすことになります[^compose1]。

[^compose1]: もちろんRedmineとPostgreSQLをひとまとめにしたコンテナを作ることも可能です。

この場合、RedmineのコンテナはPostgreSQLのコンテナのIPアドレスを知らなければアクセスすることはできません。しかし、DockerコンテナのIPアドレスは通常DHCPによって割り振られるため不定です。DockerComposeを使用すると、このように連携する複数のコンテナの起動/終了を同時に制御しつつ、「[リンク機能](https://docs.docker.com/compose/compose-file/compose-file-v2/#links)」によって、互いのコンテナのIPアドレスを直接意識することなく互いにアクセスできるようになります。

DockerComposeが提供する機能はこれだけではありませんが、このような機能が提供されているため、複数コンテナから構成されるコンテナを実行する場合には DockerComposeを使用することが増えています。

### Redmine を Docker で動かしたい理由
Redmineを自分で構築した経験があるかたはご存じかもしれませんが、RedmineはRuby、データベース、運用方法によっては Web サーバ等、依存するミドルウェアが多く、インストールが比較的大変です。特に Ruby 関連では各種ライブラリパッケージの依存もあるため、同一ホストで他にRubyを使用するアプリケーションを同居させようとすると、要求されるライブラリのバージョンが異なるなど、トラブルを多く生みます[^compose2]。従来であれば、[rbenv](https://github.com/rbenv/rbenv)のようにRuby環境を分離するツールを併用することで乗り切っていました。

[^compose2]: 私の偏見かもしれませんが。

Dockerを使用すると、このような依存関係を含めてコンテナの中に閉じ込めてくれるため、ホストの環境を汚すことがありません。


今回使用する Redmine のDockerイメージは、公式のイメージではありませんが、Redmineに加えPostgreSQL(MySQLに切り替えも可能)もセットで提供されており、docker-composeの定義ファイルも提供されているため、とても手軽にRedmineを導入できます。


## DockerCompose の難点

一方で、DockerComposeは systemd のようなOSのサービス管理機能とは無縁です。このため、DockerCompose で管理されるコンテナを起動するには、DockerComposeの定義ファイル(docker-compose.yml)があるディレクトリに移動するか、起動引数に docker-compose.yml のパスを指定して `docker-compose` コマンドを実行するといった、独自の起動手段を取らなければなりません。

Dockerコンテナだけで構成されるシステムであればこれでも構いませんが、今回のように一部サービスだけDockerで実現するような場合、サービス管理は systemd で統一しておきたいのがシステム管理者の心境です。


# RedmineをDockerComposeで起動する

前置きが長くなりましたが、具体例を説明します。ここからは、DockerとDockerComposeは既にインストールされているものとして話しを進めます。

まず、[公式サイトのQuickStart](https://github.com/sameersbn/docker-redmine#quick-start)を参考に docker-compose で Redmine を起動します。

```console:docker-composeファイルの取得と実行
# mkdir /opt/redmine
# cd /opt/redmine

# wget https://raw.githubusercontent.com/sameersbn/docker-redmine/master/docker-compose.yml
# docker-compose up -d
```
※ docker-composeの `-d` はバックグラウンド起動するためのオプションです

このイメージでは、10083ポートでRedmineが待ち受けるようになっています。あとはファイアウォールを解放するだけで、Redmineが使えるようになります。

```console:10083ポートを解放
# firewall-cmd --add-port 10083/tcp --zone public
```

# systemd で起動できるようにする

さて、ここからが本題。これを systemd 配下でサービスとして動くようにします。たとえば、redmine というサービス名で登録すれば、他のサービスと同様、以下のようなコマンドで制御できます。

```console
起動
# systemctl start redmine

終了
# systemctl stop redmine

自動起動設定
# systemctl enable redmine

ログ表示
# journalctl -u redmine
```

## ユニットファイルの作成
systemd で動かすには、ユニットファイルという定義ファイルを所定の場所に登録するだけです。

以下のように、`/etc/systemd/system/サービス名.service` という名前のファイルを作成します。ユニットファイルは ini ファイル形式で、`ExecStart` にサービス起動時に実行するコマンド、`ExecStop`にサービス終了時に実行するコマンドを指定するだけで良いです。冒頭でも説明したとおり、これが systemd の大きなメリットです。

`docker-compose` コマンドでは、 `up` や `down` といったサブコマンドでコンテナの制御ができますから、これらを `ExecStart` や `ExecStop` に指定するだけです。

```ini:/etc/systemd/system/redmine.service
[Unit]
Description=Redmine

[Service]
ExecStart=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up
ExecStop=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml down
ExecReload=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml restart
Restart=always

Type=simple

[Install]
WantedBy=multi-user.target
```

docker-compose の `-p` オプションで指定しているのは DockerComposeのプロジェクト名で、DockerComposeが起動したコンテナ名の一部として使用されます。省略時は docker-compose.yml がおかれているディレクトリの名前になりますので、ここでは変化しないように固定しておきました。(変化してもあまり困るものではありませんが。。)

-【参考】
 - [docker-compose の -p オプションの説明](https://docs.docker.com/compose/reference/overview/#use--p-to-specify-a-project-name)
 - [COMPOSE_PROJECT_NAME 環境変数](https://docs.docker.com/compose/reference/envvars/#compose_project_name)

ユニットファイルを追加/修正したときは、以下のコマンドを実行しないと systemd に認識されないので注意してください。

```console
# systemctl daemon-reload
```
以上で、`systemctl`コマンドでRedmineの制御ができるようになりました。実際に試してみましょう。

```console:サービスの起動
# systemctl start redmine
```

```console:ステータスの確認
# systemctl status redmine
● redmine.service - Redmine
   Loaded: loaded (/etc/systemd/system/redmine.service; enabled; vendor preset: disabled)
   Active: active (running) since 水 2017-12-20 20:35:52 JST; 4s ago
  Process: 21874 ExecStop=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml down (code=exited, status=0/SUCCESS)
  Process: 22368 ExecStartPre=/bin/sh -c /bin/systemctl set-environment DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'` (code=exited, status=0/SUCCESS)
 Main PID: 22374 (docker-compose)
   Memory: 37.6M
   CGroup: /system.slice/redmine.service
           ├─22374 /usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up
           └─22376 /usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up
```

## ログの確認

systemdではプロセスの標準出力をそのままログとして journald で管理してくれます。今回使用しているRedmineのDockerイメージもログを標準出力に出しているので、特に何もせずに `journalctl` コマンドでログを見られるようになります。

従来であればログの確認といえば、以下のように tail コマンドを使用するのが定番でした。この方法では、ログファイルの場所を知らなければ、確認することができません。

```console
# tail -f ログファイル
```

journald では、以下のように `journalctl` コマンドでサービス名を指定するだけで、サービスのログを見ることができます。

```console:ログの確認
# journalctl -f -u サービス名
```
`-u` オプションの引数で指定しているのがサービス名で [^journalctl1] 、`-f`オプションはtailの同名オプションと同じ動きをします。`-f` をつけなければ、デフォルトで less 等のページャ経由で表示されます。単に出力させたいだけならば、`--no-pager` オプションを指定します。

[^journalctl1]: 正確にはユニット名で、`サービス名.service` としてしますが、`.service` を省略しても大丈夫です。

私が利用したことがないのですが、Dockerでは [logging drivers](https://docs.docker.com/engine/admin/logging/overview/)という機能で標準出力を journald や各種ログ管理ツールに転送する機能もあるので、住み分けは考え無ければならないと思います。


## systemd配下のdockerコンテナにパラメータを渡す

ここからが応用編です。

今回は、systemd でサービスとして動かしているDockerコンテナから、ホストを名前で参照させたいのでした。

たとえば、コンテナから見たホストのIPアドレスが `172.17.0.1` ならば、コンテナ側の`/etc/hosts`に以下のように記述できれば、`docker-host`という名前でホストにアクセスできます。

```text:/etc/hosts
172.17.0.1　docker-host
```

### extra_hosts で hosts ファイルを動的に設定する
DockerComposeでは、`docker-compose.yml`に [extra_hosts](https://docs.docker.com/compose/compose-file/compose-file-v2/#extra_hosts-1) を記述すると、コンテナ起動時に/etc/hostsにホスト名とIPアドレスの対応を挿入してくれます。

```yaml:docker-compose.yml
   ・・・
extra_hosts:
- docker-host:172.17.0.1
   ・・・
```

ただし、このままでは固定値しか書き込めません。IPアドレスが変化しても大丈夫なように、環境変数から参照できるようにします。

以下のようにして、redmineサービスの定義に`extra_hosts`を追加しておきます。

```yaml:docker-compose.yml
version: '2'

services:
  postgresql:
   ・・・(省略)・・・

  redmine:
    image: sameersbn/redmine:3.4.2
    depends_on:
    - postgresql
    extra_hosts:                         # ← 追加箇所
    - docker-host:${DOCKER_HOST_IP}      # ← 追加箇所
    environment:
    - TZ=Asia/Kolkata
   ・・・(省略)・・・
```

これで、docker-compose でコンテナを起動する前に、前述のワンライナーでホストのIPアドレスを調べておき、環境変数DOCKER_HOST_IPに設定するだけでOKです。いったん、systemd を挟まずに確認してみましょう。

```console:DOCKER_HOST_IPを指定してコンテナを起動
# DOCKER_HOST_IP=172.17.0.1 docker-compose up -d
Creating network "redmine_default" with the default driver
Creating redmine_postgresql_1 ...
Creating redmine_postgresql_1 ... done
Creating redmine_redmine_1 ...
Creating redmine_redmine_1 ... done
```
ここでは、docker-compose コマンド実行前に `DOCKER_HOST_IP=172.17.0.1` と記述していますが、コマンド実行時に一時的に環境変数を変更することができます[^env1]。

[^env1]: よく出てくるのは `LANG=C man XXX` とかで一時的に英語版の man を参照するとかですね。

```console:/etc/hostsを確認
# docker exec -it redmine_redmine_1 cat /etc/hosts
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
fe00::0         ip6-localnet
ff00::0         ip6-mcastprefix
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
172.17.0.1      docker-host          # ← 追加されている
172.18.0.3      989962a01424
```

たしかに設定されていることが確認できました。


## ホストのIPアドレスを調べる

さらに、ホストのIPアドレスを動的に取得できるようにします。この方法はさまざまなところで紹介されていますが、コンテナからみたホストのIPアドレスは、ホスト側で `docker0` インターフェース[^ip1]のIPアドレスを抽出することで得られます。

[^ip1]: Dockerのネットワークを知るにあたっては、こちらのブログエントリの図が参考になりました。`docker0` はホストとコンテナを接続する仮想ブリッジで、今得ようとしているのはこの仮想ブリッジにつながったホストのIPアドレスです。[Dockerのネットワークを理解するために覚えたことまとめ](http://christina04.hatenablog.com/entry/2016/07/22/193000)

たとえば、以下のようなワンライナーで取得できます。

```shell
ip route | grep ' docker0 ' | awk '{ print $9 }'
```

これをコマンド置換[^cmd-rep]で実行すれば、環境変数に代入できます。

```shell
DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'`
```

[^cmd-rep]: コマンド置換には、`｀コマンド｀` のようにコマンドをバッククォートで括る方法と、`$(コマンド)` のように記述する方法の2種類があります。後者の方がより新しい方法ですが、使い分けは好みのように思えます。私はバッククォートが好みです。

<details><summary>(おまけ) ワンライナーの解説</summary><div>

----
たいしたワンライナーではありませんが、自分が Linux を勉強しはじめたとき、このようにいきなり出てくるワンライナーの意味がわからずに困ったことが多かったため、解説してみます。

まず、`ip`コマンドでインタフェースとIPアドレスの一覧を列挙させます。

```console
# ip route
default via 10.7.255.254 dev eno1 proto static metric 100
10.7.0.0/16 dev eno1 proto kernel scope link src 10.7.30.74 metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev br-fbe3e623b303 proto kernel scope link src 172.18.0.1
172.19.0.0/16 dev br-3aae67cf78ba proto kernel scope link src 172.19.0.1
172.20.0.0/16 dev br-4e0536b6e7e4 proto kernel scope link src 172.20.0.1
192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1
```

欲しいのは3行目なので、これを grep で絞ります。このとき、`docker01` のような文字列があったときに引っかからないように、あえて` docker0 `のように前後に空白を入れています。

```shell
ip route | grep ' docker0 '
```

より厳密に `docker0` が `docker1` だったりしても引っかけられるようにするなら、正規表現にした方が良いかもしれません。

```shell
ip route | grep -E ' docker[0-9]+ ' | head -n 1
```

これで、求める行が得られます。

```console
# ip route | grep -E ' docker[0-9]+ ' | head -n 1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
```

この中から最後に `172.17.0.1` を `awk` コマンドで抽出します。`'{ print $9 }'`の部分がawkのプログラムです。入力をスペースで区切られたカラムと見なして、9番目のカラムを抽出するという命令です。

```console
# ip route | grep -E ' docker[0-9]+ ' | head -n 1 | awk '{ print $9 }'
172.17.0.1
```
----

</div></details>

## Systemd に対応させる

あとは systemd で起動する前に上記のワンライナーを実行すればよいように思えます。

ちょっと長いですがこんな感じです。

```
ExecStart=DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'` /usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up
```

しかし、これだとうまくいきません。コマンド実行前に一時的に環境変数を変更する機能は、bash などのシェルが提供している機能です。systemd の ExecStart はシェルを介さずに直接実行しているので、この機能が使えないのです。

環境変数を一時設定するには、以下のようにbashの`-c`オプションでコマンド文字列を渡すことで、bash経由で実行させる必要があります。

```
ExecStart=/bin/bash -c "DOCKER_HOST_IP=`～` docker-compose～"
```

少し長いですが正確に書くと以下のようになります。

```
ExecStart=/bin/bash -c "DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'` /usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up"
```

さて、systemd を使用するときのハマリポイントですが、以下のように systemctl の外側で環境変数を設定しても、systemd で起動されるプロセスには引き継がれません。ユニットファイル内の環境変数は独立した空間になっている[^7]ためです。

[^7]: 原文が見つかりませんでしたが、systemdの仕様のようです。

```
# DOCKER_HOST_IP=172.17.0.1 systemctl start redmine
```

## サービスにパラメータを渡す方法を極める

この問題を解決するには、どうすればよいでしょうか。
systemd で起動されるサービスに環境変数を渡すには、以下のいずれかの手段を取ります。

1. ユニットファイル内で `Environment` や `EnvironmentFile` ディレクティブで文字列やファイルから渡す。(`systemd.exec` のmanページを参照)
2. プロセスを bash 経由で起動して環境変数を設定する。(今回の方法)
3. `ExecStartPre` ディレクティブで `ExecStart` 前にコマンドを実行、`systemctl` の `set-environment` サブコマンドで設定する。

### 1. `Environment`, `EnvironmentFile` を使用する方法

1 番目の方法は、固定値を渡したい場合に一番簡単な方法です。たとえば、`Environment` ディレクティブでは以下のように記述できます。

```ini:Environmentの利用
Environment="DOCKER_HOST_IP=172.17.0.1"
```

また、`EnvironmentFile`ディレクティブでは、以下のようにして外部ファイルから読み込ませることもできます。(多くの標準サービスでは、この方式で利用者がパラメータを変更できるようにしています)

```ini:EnvironmentFileの利用
EnvironmentFile=/etc/sysconfig/redmine
```

```bash:/etc/sysconfig/redmine
DOCKER_HOST_IP=172.17.0.1
```

しかし、いずれの場合も変数展開などで値を動的に変更することはできません。`man systemd.exec` で man ページを参照すると、`Environment` ディレクティブの解説には以下のような記述があり、変数展開が行われないことがわかります。

> Variable expansion is not performed inside the strings, however, specifier expansion is possible. The $ character has no special meaning.

> (特別な展開を除き、文字列の中で変数展開は行われません。\$ 文字(シェルで \$FOO のように変数を表すのに使用する文字) は特別な意味を持ちません)

なお、「特別な展開」というのは、たとえば `%H` でホスト名が得られるなど、ユニットファイルの中だけで利用できる識別子のことを言っています[^8]。

[^8]: 詳細は `man systemd.unit` の `Table 2. Specifiers available in unit files` に記載されています。

### 2. bash を使って環境変数を設定する方法

さきほど説明した、`ExecStart` ディレクティブで bash 経由で環境変数を設定しつつ、プロセスを起動する方法です。

```
ExecStart=/bin/bash -c "DOCKER_HOST_IP=`～` docker-compose～"
```

この方法はうまく行きますが、複数の環境変数を渡したいような場合、コマンドラインが長くなって可読性が下がるのが欠点です。

### 3. `ExecStartPre` ディレクティブで `set-environment` サブコマンドを使用する方法

最後に、もう少しスマートな3番目の方法を紹介します。

ユニットファイルには `ExecStartPre` というディレクティブがあります。その名の通り、`ExecStart` の前に実行するコマンドを記述するものです。

さきほども書きましたが、systemd にはシェルが管理するものとは別の環境変数の空間を持っています。systemd が起動するプロセスが参照できるのは、こちらの環境変数の空間のようです[^9] 。先の `Environment` や `EnvironmentFile` で環境変数が設定される先もこの空間です。適切な呼び方かわかりませんが、本稿ではこれを仮に「systemd環境変数空間」と呼ぶことにします。

[^9]: おそらく、悪意あるユーザが環境変数を勝手に変更してサービスの挙動に影響を与えないようにするためだと思います。

`systemctl` の `set-environment` サブコマンドを使用すると、systemd環境変数空間に値を設定することができます。たとえば、以下のような使い方です。

```console:systemdへの環境変数の設定
systemctl set-environment DOCKER_HOST_IP=172.17.0.1
```

これを `ExecStartPre` ディレクティブでシェル経由で実行すれば、動的に取得したIPアドレスを systemd 環境変数空間に設定し、サービスに渡すことができます。なお、シェル経由で起動しているのはコマンド置換を使用するためです。

```ini
ExecStartPre=/bin/sh -c "/bin/systemctl set-environment DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'`"
```

また、`ExecStartPre` は `ExecStart` とは違って複数記述できるため、複数の環境変数を渡したい場合はその数だけ `ExecStartPre` を並べておけば良いので、ユニットファイルの見通しが良くなります。

`systemctl` コマンドには、他にも `show-environment`、`unset-environment`、`import-environment` といった systemd 環境変数空間を操作するためのサブコマンドが用意されています。詳しくは、`man systemctl` で調べてみてください。


## 最終版のユニットファイル
以上を踏まえた、最終版のユニットファイルです。

```ini:/etc/systemd/system/redmine.service
[Unit]
Description=Redmine

[Service]
ExecStartPre=/bin/sh -c "/bin/systemctl set-environment DOCKER_HOST_IP=`ip route | grep ' docker0 ' | awk '{ print $9 }'`"
ExecStart=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml up
ExecStop=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml down
ExecReload=/usr/local/bin/docker-compose -p redmine -f /opt/redmine/docker-compose.yml restart
Restart=always

Type=simple

[Install]
WantedBy=multi-user.target
```

これでサービスを起動し、ホストのIPアドレスがコンテナに渡されているか確認します。

```console:systemdでサービス起動
# systemctl redmine start
```

```console:/etc/hostsを確認
# docker exec -it redmine_redmine_1 cat /etc/hosts
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
fe00::0         ip6-localnet
ff00::0         ip6-mcastprefix
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
172.17.0.1      docker-host          # ← 追加されている
172.18.0.3      989962a01424
```

これで、無事コンテナから `docker-host` というホスト名で、ホスト上のメールサーバを指定できるようになりました。

# まとめ

本稿では以下のようなことを紹介しました。

- systemd と DockerCompose の概要
- RedmineのようなアプリケーションをDockerで動かすと、なぜ嬉しいのか
- DockerCompose で管理されるコンテナを systemd で制御する方法
- systemd と連携するときの、Docker コンテナへのパラメータの渡し方
- systemd には独自の環境変数の空間があることに注意！

特に、Systemd から Docker コンテナにパラメータを渡す方法についてはいろいろな方法があるので、今回取り組んでみてとても勉強することができました。皆さんもぜひ試してみてください。