CentOS 7のDockerコンテナ内でsystemdを使ってサービスを起動する

# はじめに

CentOS 7ではinitデーモンがsystemdに変わったため、centos:centos7のDockerイメージを使用した場合、普通にコンテナを作ってその中でsystemctlを実行すると「Failed to get D-Bus connection: No connection to service manager.」というエラーメッセージが出力されるだけだ。

```
$ sudo docker run -it centos:centos7 /bin/bash

# systemctl
Failed to get D-Bus connection: No connection to service manager.
```

そのため、サービスを起動するためには別の方法でコンテナを起動しなければならない。

以下の検証にはhttpdを使用した。
他のサービスではまた別の何かをしないといけないかもしれない。

# 前準備

ホスト側もCentOS 7を使用する場合、以下のようにする。

（今回、ホスト側はrootユーザでない管理者ユーザで操作しているのでプロンプトは「$」としている。コンテナ内では「#」になるので、ホスト、コンテナのどちらを操作しているかはそれで区別して欲しい）

Dockerをインストールする。
ちなみに、[extrasリポジトリにあるから](https://bugzilla.redhat.com/show_bug.cgi?id=1108200)、という理由で[EPEL 7 betaからdocker-ioパッケージは削除された](http://permalink.gmane.org/gmane.linux.redhat.fedora.devel/198661)。8月15日に見た時はまだあったはずなのに。

```
$ sudo yum install docker
```

dockerサービスを起動する。

```
$ sudo systemctl enable docker
$ sudo systemctl start docker
```

[nsenter](https://github.com/jpetazzo/nsenter)をインストールしておく。
これはnsenterのインストールコマンドである。念のため。
**2014-08-27追記**: --privilegedオプションが必要なのはSELinux有効時のみである。

```
$ sudo docker run --privileged --rm -v /usr/local/bin:/target jpetazzo/nsenter
```

もしホストにCoreOSを使うならdockerもnsenterも同梱されているのでこれらの準備は不要だ。

# コンテナの操作

試行錯誤の結果、`docker run`を以下のようにして起動するのが一番単純であった。

* 特権モードにする(--privileged)
* コンテナで起動されるコマンドは`/sbin/init`にする

というわけで、次のようにコンテナを起動する。
今回、--nameオプションで名前を設定しているがIDで操作したいなら不要である。

```
$ sudo docker run --privileged -d -p 80:80 --name httpd1 centos:centos7 /sbin/init
```

---

**2014-08-27追記**: SELinuxが無効ならば、本ページでやっている程度の処理では実際のところCAP_SYS_ADMINケーパビリティだけ与えられていれば十分のようだ。そのため、--cap-addオプションが追加されたDocker 1.2.0以降ならば、（セキュリティ上の観点からコンテナに与える権限を小さくする目的で）--privilegedオプションの代わりに--cap-add=SYS_ADMINに変えても動作する。

```
$ sudo docker run --cap-add=SYS_ADMIN -d -p 80:80 --name httpd1 centos:centos7 /sbin/init
```

SELinuxが有効の場合、ケーパビリティの付与ではSELinuxの壁は越えられないようなので--privilegedオプションを使用する。

---

**2015-05-10追記**: @ngyuki さんより頂いた情報を元に検証したところ、2015-04-04頃より以降のcentos:centos7イメージにはfakesystemdではなくsystemd-containerがインストールされており、fakesystemdがインストールされていた頃のようなパッケージの入れ替えをしなくてもサービスが起動するようになったようだ。
（systemd-containerには/sbin/initもある）

```
$ sudo docker run --privileged -d -p 80:80 --name httpd centos:centos7 /sbin/init
$ sudo docker exec -it httpd /bin/bash
# yum install httpd -y
# systemctl enable httpd.service
# systemctl start httpd.service
# exit
$ curl -s http://localhost/ | head -n 1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html><head>
```

以下にfakesystemdがインストールされていた頃の追記を参考までに残しておく。

---

**2014-09-16追記**: 最近のcentos:centos7イメージにはsystemdの代わりにfakesystemdがインストールされている（2014-09-05辺りから？）。
systemctlでサービスを起動するため、またイメージ内に /sbin/init もないので上記コマンドではコンテナの起動すらできないので、fakesystemdをアンインストールしてsystemdをインストールし、docker commitなどでイメージを作る必要がある。

```
$ sudo docker run -it --name temp centos:centos7 /bin/bash
# yum swap fakesystemd systemd
# exit
$ sudo docker commit temp yunano/centos7-systemd
$ sudo docker run --privileged -d -p 80:80 --name httpd1 yunano/centos7-systemd /sbin/init
```

---

nsenterを使用してコンテナに入る。
先程設定したコンテナ名で指定しているがIDでも構わない。

nsenterはコマンドの指定がなければ、ホスト側のログインユーザのデフォルトシェルを起動する。
しかしcentos:centos7イメージにはshかbashしかないため、それ以外をデフォルトシェルとしている場合は以下のようにコマンドに`/bin/sh`あるいは`/bin/bash`を指定してやる必要がある。
ログインユーザのデフォルトシェルがshかbashの場合、あるいは起動されるシェルをコンテナにインストールした場合はコマンドの指定は不要である。

```
$ sudo nsenter -t $(sudo docker inspect --format '{{.State.Pid}}' httpd1) -m -u -i -n -p /bin/sh
```

そのままだとyumの際に日本語が文字化けするので以下を実行。

```
# localedef -vc -i ja_JP -f UTF-8 ja_JP.UTF-8
```

httpdをインストールしてサービスを起動。
エラーなく起動する。

```
# yum install httpd
# systemctl enable httpd
# systemctl start httpd
```

適当に/var/www/html/index.htmlを作ってコンテナから出る。

```
# echo 12345 > /var/www/html/index.html
# exit
```

curlでアクセスできることを確認する。
ここはホストで実行したが、他のコンピュータからホストに向けて実行しても構わない。

```
$ curl http://localhost
12345
```

一度コンテナを停止し、もう一度立ち上げた後でも問題なくcurlでアクセスできる。

```
$ sudo docker stop httpd1
$ sudo docker start httpd1
$ curl http://localhost
```

# ホストがCentOS 6の場合

**2014-08-27追記**

ホストがCentOS 6の場合、このページの記述通りにdocker runを実行してもコンテナ内でsystemctl実行時に「Failed to get D-Bus connection: Failed to connect to socket /run/systemd/private: Connection refused」というエラーメッセージが出る。

これはカーネルのバージョンが古いためであり（[systemdの動作要件はLinux kernel 3.0以上である](http://cgit.freedesktop.org/systemd/systemd/tree/README) - 2015-05-10注: 今見ると3.7以上に変わっている）、ホスト側でカーネルをバージョンアップすることができれば動作する。
例えば[ELRepo-kernel](http://elrepo.org/linux/kernel/el6/x86_64/RPMS/)からインストールするとか（インストールするだけではデフォルトカーネルは変わらないので/etc/grub.confのdefaultを書き換えて再起動するなり、grubのカーネル選択画面でインストールしたものを選ぶなりする必要がある）。なお、確認は`yum install http://elrepo.org/linux/kernel/el6/x86_64/RPMS/kernel-lt-3.10.53-1.el6.elrepo.x86_64.rpm`のみで行った。

# 参考資料

* http://rhatdan.wordpress.com/2014/04/30/running-systemd-within-a-docker-container/
* http://maci0.wordpress.com/2014/07/23/run-systemd-in-an-unprivileged-docker-container/