Read Only 化した Raspberry Pi で Docker を動かす

## はじめに

最近の Raspberry Pi は性能の向上が著しく、Docker も難なく動かすことができます。
Docker でサービスを構築しておくと、普通のデスクトップ PC と Raspberry Pi の間でサービス環境を共通化することができるので非常に便利です。
しかし、Raspberry Pi には故障しやすいという欠点があります。
故障の原因は様々ですが、電源ぶち切りによる SD カードのデータ破損が主な原因のようです。
これについては ```su氏``` が下記の記事で検証しております。

https://zenn.dev/su_/articles/97b9e25d0ae41c

記事内でいくつかの対策が提案されていますが、そのうちの１つとして SD カードを読み込み専用にしてシステムデータを RAM 上に展開する、いわゆる Read Only 化（以下、RO化）があります。

RO化自体は非常に簡単でコマンド一発でできるのですが、RO化した状態で Docker を利用するためには一工夫必要だったので、備忘録としてこの記事に残しておこうと思います。
本記事は「[Run Docker on your Raspberry Pi read-only file system (Raspbian) | by Grafolean | Medium](https://grafolean.medium.com/run-docker-on-your-raspberry-pi-read-only-file-system-raspbian-1360cf94bace)」を基にしています。

## 検証環境

- Raspberry Pi4 (raspios-buster-arm64 21-05-07)
- Docker 20.10.16

## セットアップの流れ

1. Docker のインストール
2. イメージ退避用、復元用スクリプトの作成
3. 自動起動用のスクリプトを作成
4. スタートアップに登録
5. スクリプト実行、RO化

## セットアップ方法

### 1. Docker のインストール

基本的には Docker の公式ドキュメントに記載されている最新の方法でインストールします。
検証環境では以下のコマンドでインストールを行いました。

```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
sudo apt install docker-compose-plugin
sudo reboot now
```

### 2. イメージ退避用、復元用スクリプトの作成

#### A. イメージ退避用スクリプト

```/usr/local/bin/dockersave.sh``` を作成し、下記のコードを記載します。
スクリプトは[参考元](https://grafolean.medium.com/run-docker-on-your-raspberry-pi-read-only-file-system-raspbian-1360cf94bace)からそのまま引用しています。

```bash
#!/bin/bash
set -e
PERSIST_DIR="/var/lib/docker.persist"

# make sure docker is NOT running:
systemctl is-active -q docker && (echo "Docker service must NOT be running, please stop it first!"; exit 1)
# containers need to be writeable, so we don't allow saving when they exist either:
[ `ls /var/lib/docker/containers/ | wc -l` -eq 0 ] || (echo "We can't save Docker configuration with containers; remove them first!"; exit 1)

mkdir -p -m 700 "$PERSIST_DIR"

# copy from tmpfs to persistent storage:
# but be careful when copying overlay/ - it might contain links to the persistent storage, which we must simply leave there:
for subdir in `ls "/var/lib/docker/"`
do
  if [ "overlay2" != "$subdir" ]
  then
    rm -rf "$PERSIST_DIR/$subdir"
    cp -ra "/var/lib/docker/$subdir" "$PERSIST_DIR/$subdir"
  else
    # remove all links beecause we don't want to overwrite the persistent storage with them:
    find "/var/lib/docker/overlay2/" -maxdepth 1 -type l -exec rm '{}' ';'
    # and overlay2/l/ will be copied verbatim, so remove it in persistent storage:
    rm -rf "$PERSIST_DIR/overlay2/l"
    # everything else is copied over to persistent storage: (l/ and any new image overlays)
    mkdir -p -m 700 "$PERSIST_DIR/overlay2"
    cp -ra /var/lib/docker/overlay2/* "$PERSIST_DIR/overlay2/"
  fi
done
```

スクリプトに実行権限を付与します。

```sudo chmod +x /usr/local/bin/dockersave.sh```

#### B. イメージ復元用スクリプト

```/usr/local/bin/systemctl.dockerload.sh``` を作成し、下記のコードを記載します。
スクリプトは[参考元](https://grafolean.medium.com/run-docker-on-your-raspberry-pi-read-only-file-system-raspbian-1360cf94bace)からそのまま引用しています。

```bash
#!/bin/bash
set -e
PERSIST_DIR="/var/lib/docker.persist"

# make sure docker is NOT running:
systemctl is-active -q docker && (echo "Docker service must NOT be running, please stop it first!"; exit 1)

if [ ! -d "$PERSIST_DIR" ]
then
  echo "Docker configuration was never saved yet (dockersave.sh), nothing to load. Exiting."
  exit 0
fi

# existing directory structure should be copied verbatim, except for overlay2/ (but
# only on top level - image/overlay2 should be copied)
/bin/rm -rf /var/lib/docker/*
for subdir in `ls "$PERSIST_DIR/"`
do
  if [ "overlay2" != "$subdir" ]
  then
    cp -ra "$PERSIST_DIR/$subdir" "/var/lib/docker/$subdir"
  fi
done

# overlay2/ must be read-write, but the existing subdirectories can be links to a
# persistent (readonly) location:
mkdir -m 700 /var/lib/docker/overlay2
for layer_hash in `ls $PERSIST_DIR/overlay2 | grep -v "^l$"`
do
  ln -s "$PERSIST_DIR/overlay2/$layer_hash" "/var/lib/docker/overlay2/$layer_hash"
done
cp -ra "$PERSIST_DIR/overlay2/l" "/var/lib/docker/overlay2/l"
```

スクリプトに実行権限を付与します。

```sudo chmod +x /usr/local/bin/systemctl.dockerload.sh```

### 3. 自動起動用のスクリプトを作成

任意の場所に自動起動用のスクリプトを作成します。
筆者は `~/docker-ro-mode.sh` を作成し、下記のスクリプトを記載しました。

```bash
#!/bin/bash

echo "stop docker..."
service docker stop
echo "mount tmpfs"
mount -t tmpfs tmpfs /var/lib/docker
echo "run copy script"
bash /usr/local/bin/systemctl.dockerload.sh
echo "restarting docker..."
service docker start

# Complete restore docker images
# Write docker compose or docker run script below

ここにコンテナ起動スクリプトを書く
例）cd ~ && docker compose up -d
```

### 4. スタートアップに登録

`/etc/systemd/system/docker-ro-mode.service` を作成し、下記のコードを記載

```
[Unit]
Description = "Run docker in read-only mode"

[Service]
ExecStart = bash /home/pi/docker-ro-mode.sh
Type = simple

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

### 5. スクリプト実行、RO化

Docker のイメージ退避を行うので、使用するイメージをダウンロードしておきます。
とりあえず起動したいコンテナを一度起動しておけば OK です。

ダウンロードが完了したら下記のコマンドを実行し、Docker イメージの退避を行います。

```bash
sudo service docker stop
sudo bash /usr/local/bin/dockersave.sh
sudo systemctl daemon-reload
```

スタートアップを有効化します。

```bash
sudo systemctl enable docker-ro-mode
```

最後に、RO化して完了です。

```bash
sudo raspi-config nonint enable_overlayfs
```

## 補足１

RO化した状態では再起動をかけると変更がすべてリセットされます。
もし何かしらの設定を変えたい場合は一度RO化を解除する必要があります。

```bash
# RO化解除
sudo raspi-config nonint disable_overlayfs
sudo reboot now
```

設定変更が終わったら再度RO化してください。

## 補足２

RO化すると Docker が起動できない理由ですが、```/var/lib/docker``` が読み込み専用になってしまうからです。
Docker は起動時に ```/var/lib/docker``` が書き込み可能な状態であるか確認し、
書き込みができない状態だと起動を中断してしまいます。

この問題を回避するために ```tmpfs``` という一時的なファイルシステムを利用しています。
```tmpfs``` を ```/var/lib/docker``` にマウントすることで、RO化したシステムでも ```/var/lib/docker``` を書き込み可能な状態にできます。
しかし、```/var/lib/docker``` 内が ```tmpfs``` に上書きされる、すなわち空のディレクトリになってしまうため、予め ```/var/lib/docker``` 内のファイルを適当な場所に避難させておき、Raspberry Pi の起動し ```tmpfs``` がマウントされたタイミングでファイルを復元する、という手法をとっています。


## 参考資料

- [Raspberry Piは本当に壊れやすいのか](https://zenn.dev/su_/articles/97b9e25d0ae41c)
- [Run Docker on your Raspberry Pi read-only file system (Raspbian) | by Grafolean | Medium](https://grafolean.medium.com/run-docker-on-your-raspberry-pi-read-only-file-system-raspbian-1360cf94bace)
- [ubuntu - How to run docker daemon on read-only host file system - Stack Overflow](https://stackoverflow.com/questions/58731258/how-to-run-docker-daemon-on-read-only-host-file-system)
- [RaspberryPiでRAMディスク化する方法 | アバンドコアの技術情報サイト](https://abundcore.net/archives/1435)