インスタンスのブート時に Rootless Docker で compose.yml を開始する

## 概要

Docker を Rootless モードで動かす記事をいくつか拝見しましたが、

- 内容が古くなっている記事が多い。
- systemd に compose.yml を登録するところまでやっている記事が探した限り無い。

ということで、2023年8月時点で最新(のはず)のミニマルな手順をまとめてみました。

::: note
本稿の手順を実施することで

- ブート時に Rootless モードの Docker エンジンのサービスを開始する。
- Docker エンジンのサービス開始と同時に、任意の compose.yml 構成を開始する。

が実現できます。
:::

ベースとして AWS EC2 の Debian 12 インスタンスを使用します。

VirtualBox も使えます。その場合は VirtualBox で作成した仮想マシンに Debian 12 をインストールしたところに Docker エンジンをインストールしてください。

基本的に以下の公式ドキュメントに沿って実施します。

- https://docs.docker.com/engine/install/debian/
- https://docs.docker.com/go/rootless/

## とりあえず Docker エンジンをインストールする

1. AWS にルートユーザーを作成してクレジットカードを登録します。
2. AWS のルートユーザーで IAM アカウントを作成して PowerUserAccess ロールを付与します。
3. IAM アカウントで t2.micro インスタンスをストレージ 30GB の Debian 12 で作成します。
    - インスタンス作成前に「ポート 22 をインバウンドで許可」とするセキュリティグループを作成しておきます。インスタンス作成時にそのセキュリティグループを指定します。
    - インスタンス作成前にキーペアを作成しておきます。キーペア作成と同時に秘密鍵が自動的にダウンロードされます。インスタンス作成時にそのキーペアを指定します。
4. インスタンス作成後に admin ユーザーで ssh ログインできることを確認します。
5. Debian 12 に Docker エンジンをインストールします。 https://docs.docker.com/engine/install/debian/
    - 公式ドキュメントの最後で `sudo docker run hello-world` を実行していますが、本稿の手順のこの時点で実行する必要はありません。

## Rootless モードの Docker をインストールする

Docker を Rootless モードで起動するようにします。 https://docs.docker.com/go/rootless/

### 通常インストールの Docker エンジンを停止する

まず最初に通常インストール状態で起動している Docker エンジンを停止します。

Docker エンジンが普通にインストールされている状態で、AWS EC2 の Debian 12 であれば admin ユーザーで

```
sudo systemctl disable --now docker.service docker.socket
```

を実行します。

### 必要なパッケージを追加でインストールする

Debian 12 であれば

```
sudo apt-get install uidmap
```

を実行します。

Ubuntu の場合も同じで良いはずです。(未確認)

参照している公式ドキュメントには「dbus-user-session が入っていなかったらインストールする」とありますが、AWS EC2 の Debian 12 の場合は最初から入っています。

### rootless ユーザーを作成する

新規に rootless ユーザーを作成します。

これは AWS EC2 の Debian 12 のデフォルトユーザー admin だと「パスワード無しで sudo」が実行できてしまうので、セキュリティリスク低減を目的として「sudo できないユーザー」を使いたいからです。

admin ユーザーの状態で以下を実行します。

```
sudo useradd -m rootless             # ホームディレクトリ有りでユーザーを新規に作成する
sudo chsh -s /bin/bash rootless      # シェルをbashに変更する(必須ではない)
sudo passwd -l rootless              # 新規で作成した rootless ユーザーのパスワードを無効化する(ログインは ssh のみ)
```

rootless ユーザーが作成できたら、admin ユーザーの状態で

```
sudo su - rootless
```

として rootless ユーザーになり、`.ssh` ディレクトリを作り、公開鍵を記載した `authorized_keys` ファイルを配置します。

対応する秘密鍵で rootless ユーザーに ssh ログインできるところまで確認します。

### Rootless モードのインストール

ssh で rootless ユーザーにログインして

```
dockerd-rootless-setuptool.sh check
dockerd-rootless-setuptool.sh install
```

を実行します。

公式ドキュメントに沿って Docker エンジンをインストールしていれば `dockerd-rootless-setuptool.sh` はインストールされているはずです。

ssh でログインしないと、`dockerd-rootless-setuptool.sh` の内部で実行される `systemctl --user show-environment` が正しく動作せず(環境変数 `XDG_RUNTIME_DIR` が不足するため) `dockerd-rootless-setuptool.sh` の実行がうまくいきません。

ssh でログイン(正確には ssh でなくともよいのでとにかく「ログイン」のプロセスを経てアクセス)すると、環境変数 `XDG_RUNTIME_DIR` が自動的にセットされ、`dockerd-rootless-setuptool.sh` が正常に実行できるようになります。

最後に rootless ユーザーに ssh でログインした状態で

```
docker run hello-world
```

が成功すれば Rootless モードの導入は完了です。

この状態では rootless ユーザー以外のユーザー(admin ユーザーや root ユーザーなど)では `docker ps` などの docker コマンドが機能しません。これは正常な状態で、環境が壊れたわけではありません。このまま次のステップに進んでください。

### compose.yml によるサービスの起動

ssh で rootless ユーザーにログインして以下の操作を実行します。

/home/rootless/work/test などとしてディレクトリを作成し、/home/rootless/work/test/compose.yml を作成します。

```compose.yml
version: "3"

services:
  nginx:
    image: nginx:latest
    ports:
      - 8080:80
```

AWS EC2 のセキュリティグループに、「ポート 8080 をインバウンドで許可」というルールを追加しておくのを忘れずに実施しておきます。

この状態で

```
docker compose up -d
docker compose down
```

して NGINX のウェルカムページが表示されたり表示されなくなったりすることを確認してください。

#### 使用できるポート番号について

ここで `ports: - 8080:80` としているポート番号には2種類の制約があります。

- Rootless モードの制約として 1024 以下のポート番号は使えません。(設定すれば使えますが本稿の範囲外とします)
    - `ports: - 80:80` としていないのはこれが理由です。
- 最近のブラウザの制約として 10080 などの特定のポート番号は「ブラウザが」表示を制限します。
    - `ports: - 10080:80` など該当するポート番号を使用してしまうと「curl ではデータが正常に受信できるがブラウザだと表示できない」という状況になります。
    - AWS などであれば HTTPS を終端するロードバランサの配下で 10080 番ポートで HTTP を待ち受けるという構成が可能と思われます。(未確認)

## compose.yml を開始するサービスを systemd に登録する

全て ssh でログインした rootless ユーザーでの操作となります。
/home/rootless/.config/systemd/user/sample-nginx.service を作成します。

```sample-nginx.service
[Unit]
Description=Sample NGINX
Requires=docker.service
After=docker.service

[Service]
ExecStart=/usr/bin/docker compose -f /home/rootless/work/test/compose.yml up
ExecStop=/usr/bin/docker compose -f /home/rootless/work/test/compose.yml down
Restart=always

[Install]
WantedBy=default.target
```

systemctl の操作には --user オプションを付ける必要があります。

```
systemctl --user daemon-reload
systemctl --user status sample-nginx.service
systemctl --user start sample-nginx.service
systemctl --user stop sample-nginx.service
systemctl --user enable sample-nginx.service
```

start すると NGINX のウェルカムページが表示されるようになり、stop すると表示されなくなります。

enable にすると「再起動した EC2 インスタンスに ssh で rootless ユーザーとしてログイン」した時に Docker サービスと同時に sample-nginx.service も start するようになります。

逆に言うと、現時点の設定だけでは「ssh で rootless ユーザーとしてログイン」しないと、Docker サービスも sample-nginx.service も動かず、ブラウザでアクセスしても NGINX のウェルカムページは表示されません。

まとめると、ここまでの手順を実施できていれば、

1. AWS EC2 インスタンスをブート(もしくは再起動)する。
2. root, admin, rootless いずれのアカウントでもログインせずに、ブラウザでインスタンスのIPアドレス(とポート番号)にアクセスする。
    - この時点では、ブラウザは NGINX のウェルカムページを表示できない。
3. rootless ユーザーに ssh でログインしてから、ブラウザでインスタンスのIPアドレス(とポート番号)にアクセスする。
    - ブラウザに NGINX のウェルカムページが表示される。

という動作になっているはずです。

## インスタンスのブート時に compose.yml を開始する

root 権限で(rootless ユーザーではなく root 権限で)

```
loginctl enable-linger rootless
loginctl show-user rootless
```

を実行します。
show-user の実行結果は以下のようになります。Linger の項目が yes になっています。

```
UID=1001
GID=1001
Name=rootless
Timestamp=Mon 2023-08-28 08:18:24 JST
TimestampMonotonic=13729055
RuntimePath=/run/user/1001
Service=user@1001.service
Slice=user-1001.slice
Display=2
State=active
Sessions=2
IdleHint=no
IdleSinceHint=1693180144047894
IdleSinceHintMonotonic=1853696593
Linger=yes
```

インスタンスを再起動して、なにもせずに(root, admin, rootless でログインせずに)ブラウザで NGINX のウェルカムページが表示されることを確認します。

compose.yml の構成が起動してくるまで、インスタンス再起動を実行してから数分ほど待つ必要がある点に注意します。

また、

```
loginctl disable-linger rootless
```

を実行すると、インスタンス再起動だけでは NGINX のウェルカムページが表示できなくなり、「rootless ユーザーに ssh でログインするとウェルカムページが表示できるようになる」という状態に戻ることを確認します。

最後にもう一度

```
loginctl enable-linger rootless
```

しておきましょう。

## まとめ

1. Rootless モードの Docker をインストールする。
2. 配置したサービスについて `systemctl --user enable 配置したサービス` を実行しておく。(1回だけ)
3. `loginctl enable-linger rootless` を実行しておく。(1回だけ)

以上の3つのポイントを押さえることで「ブート時に Rootless モードの Docker で compose.yml 構成を自動で開始する」が実現できます。

## おまけ： VSCode で Rootless コンテナにアタッチする

rootless ユーザーの `.profile` に1行追加すると、Rootless モードで動いているコンテナに VSCode でアタッチすることができるようになります。

```
rootless@ip-172-31-44-83:~$ id
uid=1001(rootless) gid=1001(rootless) groups=1001(rootless)

rootless@ip-172-31-44-83:~$ diff -u ~/.profile.2023-0829-2231 ~/.profile
--- /home/rootless/.profile.2023-0829-2231      2023-08-29 13:32:27.460746100 +0000
+++ /home/rootless/.profile     2023-08-29 13:27:01.819819290 +0000
@@ -25,3 +25,6 @@
 if [ -d "$HOME/.local/bin" ] ; then
     PATH="$HOME/.local/bin:$PATH"
 fi
+
+export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
+
```

.profile を編集したら VSCode の Remote-SSH 拡張で AWS EC2 インスタンスに rootless ユーザーで接続しましょう。
接続した状態で Dev Container 拡張でコンテナにアタッチしようとすると Rootless モードで動いているコンテナがアタッチ先に選べるようになっています。