DockerでCGI #1: EPWING電子辞書サーバー「let me see...」 (2003年)

# はじめに

次：[DockerでCGI #2: ニコニコチャンネル キャッシュサーバー 「nicochcgi」](https://qiita.com/kurema/items/795f547a5c105b73b792)

## Docker始めました
流行ってから既に数年、今更Docker始めました。
導入手法が明確になり、環境も汚さないのは便利ですね。

使える環境(自宅サーバー)を手に入れたのでSSHでつないでやってます。
正直これで良いのでは、というかそもそもDockerである必要性は…。別に環境汚れても良いし。
まぁ本格的な開発をするならローカルPCでやった方が楽なはず。あと配布は気楽。

## Let me see...
今回Docker対応したのは、電子辞書オープンラボ・かずひこ様による「[let me see...](http://openlab.ring.gr.jp/edict/letmesee/index.html)」。
2000年代くらいまでCD-ROM電子辞書形式として使われた[EPWING形式](https://ja.wikipedia.org/wiki/EPWING)などに対応しています。
最終更新は2003年で、それをDocker対応するのがちょっと面白いですね。

GitHubレポジトリは[kurema/forkedLetMeSee](https://github.com/kurema/forkedLetMeSee)、Dockerイメージは[ghcr.io/kurema/letmesee](https://github.com/users/kurema/packages/container/package/letmesee)です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/e845ac34-eddc-a2e7-daea-17b33eee4c24.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/e111ff27-c7e6-d223-dbcf-472f1cef36f1.png)

## 導入方法
対応手順を説明する前に導入方法は以下です。
あらかじめEPWing形式の辞書を適当な場所に配置しておいてください。

```bash
$ git clone https://github.com/kurema/forkedLetMeSee.git
$ cd forkedLetMeSee
$ nano docker-compose.yml
$ nano conf/letmesee.conf
$ sudo docker-compose up -d
```

辞書の場所に応じ``docker-compose.yml``を編集し、各辞書や設定を``conf/letmesee.conf``に記述します。
``conf/letmesee.conf``から見える場所は``docker-compose.yml``でのマウント先以下になるのは注意です。

# 開発過程
Dockerを初めて二つ目なのでやはり手探りです。
いくつかベストプラクティスではかもしれません。

## Docker対応
Dockerってめんどくさいと思ってましたけど、ベースイメージからのインストール手順を``RUN``に書いていくだけの簡単な作業です。
Dockerfileが導入方法の説明にもなるので便利ですね。

ベースイメージは``httpd:latest``です。

### タイムゾーン設定
最初にDockerを試した時、以下のメッセージが出ました。

```
Please select the geographic area in which you live. Subsequent configuration
questions will narrow this down by presenting a list of cities, representing
the time zones in which they are located.
```

Ubuntu 18.04以降でgitインストール時に発生するらしいです。
httpdでは必要ないかもしれませんが、tzdataは大抵必要だと思うので以下の対処をしています。
ただし、ユーザーが日本在住でない場合は別のタイムゾーンに設定する必要があります。

```dockerfile:Dockerfile
RUN apt-get update -y && \
    apt-get install -y --no-install-recommends tzdata

#Timezone is set to Japan assuming you are in Japan.
ENV TZ=Asia/Tokyo
```

#### 参考記事
眠れない夜 (2018)「[Docker] build tzdata タイムゾーン選択回避方法(ubuntu)」エンジニアの眠れない夜 https://sleepless-se.net/2018/07/31/docker-build-tzdata-ubuntu/
@yagince (2018) 「Docker Ubuntu18.04でtzdataをinstallするときにtimezoneの選択をしないでinstallする」Qiita https://qiita.com/yagince/items/deba267f789604643bab

### CGI有効化
ApacheでCGIの有効化はsedを使って設定ファイルを弄るような必要があるようです。スマートではないですね。将来壊れそうです。
できれば専用コマンドが欲しいところ。

```dockerfile:Dockerfile
RUN sed -ri 's/#LoadModule cgid_module/LoadModule cgid_module/g; \ 
             s/DirectoryIndex index.html/DirectoryIndex index.rb index.cgi index.html/g; \ 
             s/Options Indexes FollowSymLinks/Options Indexes FollowSymLinks ExecCGI/g; \
             s/#Scriptsock cgisock/Scriptsock cgisock/g; \
             s/#AddHandler cgi-script .cgi/AddHandler cgi-script .pl .rb .cgi/g' /usr/local/apache2/conf/httpd.conf
```

ベースコンテナによってhttpd.confの場所が変わります。
扱っているCGIによって``AddHandler cgi-script``や``DirectoryIndex``の拡張子が変わったりします。今回は``.rb``。

``Scriptsock cgisock``をコメントインしないと``Service Unavailable``のエラーが出るようです([参考](https://server-recipe.com/2305/))。出ました。
以前やった記憶がなかったですが、その時はubuntuでa2enmodが使えたのでそのときに導入されるようです。

``mpm_prefork_module``を使う可能性がある場合は``s/#LoadModule cgi_module/LoadModule cgi_module/g;``も追加した方が良いでしょう。``cgid``が``cgi``になります。

#### 参考記事
ワタナベ書店 (2015) 「Docker上でApacheコンテナを作成しCGIのコンテンツを走らせるまで」 https://senyoltw.hatenablog.jp/entry/2015/10/21/175847

### 配置
後はファイルを配置するだけ。
CGIでは実行ファイルにchmodで実行権限を付与すること忘れないようにしましょう。今回は777にしました。

個人的には``README.md``ファイルをイメージに配置するのが良いと思います。
何かの理由でDockerイメージだけあるという状況には多少便利です。
レポジトリ全体を圧縮して配置するとかも良いですが、大きめの画像を配置したりすることもあるのでやめました。
容量を気にするなら辞めるなりgz圧縮なり。

```dockerfile:Dockerfile
COPY edict-devel/letmesee/ /usr/local/apache2/htdocs/
RUN chmod 777 /usr/local/apache2/htdocs/*.rb

COPY README.md /
```

#### トラブル
当初、``gem install``をする段でSSLエラーが出るというトラブルがありました。
一時期証明書の追加で対処しましたが、CGI側の修正で不要になりました。
それについては[こちらの記事](https://qiita.com/kurema/items/85edb082b38a3ca87f32)参照。

### docker-compose
このCGIは設定ファイルがCGIと同じフォルダに配置されるタイプです。
Dockerイメージ内部のファイル自体を触るのは微妙なので``volumes``でマウントします。
辞書自体のマウントも必要です。
dockerコマンド一行にしては長いですし、docker-composeファイルにしました。

```yaml:docker-compose.yml
version: '3'
services:

  letmesee:
    image: ghcr.io/kurema/letmesee:latest
    container_name: letmesee
    restart: always
    ports: 
      - 50002:80
    volumes:
      - ./conf/letmesee.conf:/usr/local/apache2/htdocs/letmesee.conf
      # /home/share/DictionaryをEPWing辞書が保存されている場所に変更してください。
      - /home/share/Dictionary:/usr/local/dict
```

### 公開
Docker HubがPull回数制限や利用されていないイメージの削除(後に保留)などを発表したので、イメージはGitHub Container Registryに保存することにしました。
6ヶ月どころか十年単位で放置する見込みなので、継続性が期待できるGitHubのサービスが確実だと判断しました。
マルチCPUアーキテクチャにも対応していて便利です。なにより無料。

## GitHub Actions
GitHub Actionsで自動化をしたかったので[GitHubコミュニティーの例](https://github.community/t/github-package-registry-does-not-support-multi-cpu-architecture-image/14339/11)のマルチCPUアーキテクチャ対応のワークフローをほぼそのまま使いました。
以下の点を変更しています。

* キャッシュはインラインで保存。GitHub Actions側に保存する方法もあるようです。

```yaml:.github/workflows/docker.yml
          cache-from: type=registry,ref=${{ steps.prep.outputs.latest }}
          cache-to: type=inline
```

* テスト用手動ディスパッチ。手動時のタグはtestになります。

```yaml:.github/workflows/docker.yml
on:
  workflow_dispatch:
```

* デイリービルドは頻度が高すぎるので月刊に。
* プラットフォームは元イメージと同じ。無料だからってやりすぎですね。

```yaml:.github/workflows/docker.yml
          platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7
```

最終的なyamlは[こちら](https://github.com/kurema/forkedLetMeSee/blob/master/.github/workflows/docker.yml)([記事時点](https://github.com/kurema/forkedLetMeSee/blob/6a2157bc88718eb3433add0f5857de854f107b00/.github/workflows/docker.yml))参照。

GitHub Container Registryはベータテスト中なので``GITHUB_TOKEN``が使えない点は注意が必要です。
自分でトークンを発行し、``GHCR_PAT``をシークレットに登録してください。

#### 時間
ビルド時間はキャッシュなしで11分。キャッシュありで2分といったところです(testタグはキャッシュ元として参照しません)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/4a59a8be-654b-5442-a2a2-833e53ac50fa.png)

タグを付けたら自動でイメージをビルドしプッシュしてくれるのは楽です。
マンスリービルドを参照すれば最新のベースコンテナにも追従できますし。
ただ実を言えば個人開発でCI/CDなんてのは建前で、以下みたいな目的が本音ですね。

* **無料なので使いたい**。
* **重い処理をオフロードしたい**。
* ミスって個人情報が混入する事態を避けたい。

なので、テスト目的でもガンガンGitHub Actionsで実行して、GitHub Container Registryからイメージをpullして試してます。自前ビルドはしません。
幸いキャッシュが効いてるとCGIだけの修正では処理時間も短くサーバーの容量も新規では大して食いません。
合わせても100kb未満のCGIそれもダウンロード数が1-2桁(現時点では自分だけ)に数百MBを消費するのはどうなのかと最初は思いましたし、z/Architectureなんて環境でこれを使う人なんて絶対居ませんが、Dockerってのはそういう世界みたいです。怖いですね。

## 改修
流石に古いだけあってそのまますんなり動くわけではありませんでしたが、少しの修正で動きます。
コメントがなくrubyは滅多に書かなくても分かりやすかったです。
元作者さんとruby開発者は見事です。

* Ruby側の更新に追従
    * 相対パスへの``require``を``require_relative``に。
        * 2003年当時は``require 'letmesee.rb'``で同一パスのrbファイルを参照してくれたようです。
        * 現時点では``require './letmesee.rb'``にすれば動作します。
        * 実際には``require_relative './letmesee.rb'``が正しいようです。これを書いている途中に気付いたので修正しました。
        * CGIならカレントディレクトリの問題とかは起きないはずですが、念のため。
    * ``File::open( path )``を``File::open( path , "r:utf-8" )``に修正。
        * 記憶にありませんが、文字化けでもしたんだと思います。
    * ``head['Content-Length'] = body.size.to_s``を``head['Content-Length'] = body.bytesize.to_s``に修正。
        * 2003年当時はstring.sizeでバイト数を取得できたようです。上の修正のせいかもしれません。
        * 出力が途中で途切れていたので不思議でした。
    * [``Iconv.iconv``](https://docs.ruby-lang.org/ja/1.9.3/method/Iconv/s/iconv.html)の利用をやめて[``Encoding::Converter#convert``](https://docs.ruby-lang.org/ja/latest/method/Encoding=3a=3aConverter/i/convert.html)を利用するようにしました。
        * iconvの引数は``iconv(to, from, *strs)``なのに対し、Encodingsでは``new(source_encoding, destination_encoding)``と文字コードの順番が逆なので注意が必要です。
        * iconvのUTF-8は``UTF8``でも通るようですが、Encodingsでは通らないので注意が必要です(元々のバグかも)。
        * Encodingsでは変換元と変換後が同じ文字コードだとエラーが発生するようです。素通りしてくれれば良いのに。
* HTML5対応
    * 音声には``<audio>``を使うようにしました。
        * 動画はMPEG-1なので``<video>``ではまず動きません。
            * JavaScriptで再生する[jsmpeg](https://github.com/phoboslab/jsmpeg)やwasm版のffmpegとかもありますが、十年単位で放置する予定なので辞めました。
    * ヘッダの変更。簡単なスマートフォン対応(cssで``@media``の追加・``viewport``)。

```rhtml:old
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
<%= css_tag %>
```

```rhtml:new
<!DOCTYPE html>

<html lang="ja">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<%= css_tag %>
```

* サーバーの性能向上に伴い、ちょっと富豪的に。
    * 辞書を絞って検索するメニューを削除。
        * 邪魔なので。``<details>``でもいいと思いますが、当時(2017年)は知りませんでした。
    * 外字をデフォルトで最大フォントサイズに。
        * 高解像度モニターも増えてますし。
        * 存在しない場合最小フォントサイズの外字にフォールバックします。
* 高速化。
    * トップページの静的表示
        * ``letmesee.rb``初期化時全辞書を読み込んでいたので、低性能マシンかつ大量辞書の環境では表示に時間が掛かっていました。
        * トップページでは辞書を読み込まず静的ファイルを表示するようにしたので表示が早くなりました。
    * 外字のキャッシュ
        * 外字が大量にある場合に遅かったので、ヘッダを変更し一日はキャッシュするようにしました。
        * 本来は一文字ずつ取得しに行く処理は避けるべきです(後述)。
* XML対応
    * XMLでの取得に対応しました。ただし、常にwell-formedだとは期待できません。
    * Androidアプリでも作ろうかと思いましたが辞めました。
* ``<b>``・``<i>``
    * ``<b>``や``<i>``が入れ子になっても正常動作するように修正しました。
        * 市販されている辞書では存在しないのか、入れ子に対応していないEPWINGソフトは結構あります。
        * 変換された辞書では普通にあって、検索候補の途中から盛大にレイアウトが崩れるので面倒です。
    * 対応しないタグを削除するようにしました。
* その他
    * CSSや設定などを個人的な好みに合わせて変更。
    * 独自テーマを追加し、デフォルトをそちらに変更。
    * ロゴ画像を透過。

改修しなかった点は以下です。

* 設定ファイルでの辞書記述
    * 大量の辞書を保存している場合、設定ファイルに一つずつ辞書のパスを追加するのは面倒です。
    * 自動追加するスクリプトを書くか、最上位パスの指定だけで済むように変更したかったですが面倒なので辞めました。
* 外字表示の高速化
    * 現状では外字一文字ずつサーバーに問い合わせるようになっています。
    * 外字が多いと毎回CGIが起動して大量のIO処理が発生し時間が掛かります。
        * キャッシュはするようにしました。
    * 辞書ファイル側では各サイズ2ファイルで小容量なので一括で渡した方が良いかもしれません。
        * 普通は数kb程度ですが、大きいと2MB程度にはなります。
        * これはかなり大変そうでした。辞めました。
* さらなるデザインの改良。
    * 自分で新しいデザインにしましたが、今見るとダサいです。
    * これは今後修正するかもしれません。

手間を掛けず、簡単にできる修正だけしたという感じです。
ruby自体まず書かないですが、読みやすく破壊的変更も大してないので楽でした。
またこのCGI自体テーマがあったり拡張しやすい設計です。素晴らしい
CGIは良いものですね。シンプルで分かりやすくて。

# 年表
| 日時 | イベント |
| :----  | :---- |
| 1987年 | [EPWING](https://ja.wikipedia.org/wiki/EPWING)の前身となるWINGフォーマット制定。  |
| 1988年 | EPWING規約制定。 |
| 1993年 | [CGI登場](https://www.unisys.co.jp/tec_info/tr110/11001.pdf) |
| 1995年 | [Ruby](https://ja.wikipedia.org/wiki/Ruby)登場 |
| 1997年6月8日  | [EB Library](http://www.mistys-internet.website/eb/) 初版 |
| 2000年4月10日 | [ruby EB](http://rubyeb.sourceforge.net/) v1.0 |
| 2002年3月31日 | ruby EB v1.7 (最新版) |
| 2003年2月29日 | [let me see...](http://openlab.ring.gr.jp/edict/letmesee/) v1.0 |
| 2003年11月9日 | let me see... v1.1 (最新版) |
| 2010年3月8日  | EB Library v4.4.3 (最新版) |
| 2013年3月13日 | [Docker](https://ja.wikipedia.org/wiki/Docker) 初版 |
| 2014年1月30日 | ruby EB 久保健洋氏によるruby1.9対応[非公式フォーク](https://github.com/kubo/rubyeb19) 初版 |
| 2014年1月30日 | ruby EB 同フォーク 最終更新 |
| 2017年7月11日 | let me see... kuremaによる[フォーク](https://github.com/kurema/forkedLetMeSee) v1.2 (微修正) |
| 2020年11月10日 | let me see... 同フォーク v1.3 (Docker対応) |

しかしすごい歴史ですね。壮大。
まぁEPWINGの世界ってのはこんな感じです。Unicode対応以外は現在でもそれなりに「使える」フォーマットなのは感心します。
違う世代の辞書ファンたちがちょっとずつ色々何かやってるのがEPWINGの魅力です。
今でもずっと辞書アプリを開発してくださってる方もいらっしゃいます。感謝。

ちなみにこの記事では細かく触れませんでしたが、2017年にフォークし小改良、最近Docker対応のついでに中改良といった感じです。
さらに言えば記事の順番が前後しますが、これは私のDockerでCGI二つ目です。
一つ目は[ニコニコチャンネルキャッシュサーバー](https://github.com/kurema/nicochcgi_docker)。そのうち記事にします。

その他、EPWING関係リンク：

* [EBStuio](http://ebstudio.info/) ([EBWin4](http://ebstudio.info/manual/EBWin4/EBWin4.html)のような使いやすいソフト。スマホ版も。)
* [更に極めよ「EPWING/PDIC辞書」](http://kazuo.fc2web.com/dic/ddwin2.htm) (変換ツールリンク集)。
* [MacDicConversion](https://github.com/kurema/MacDicConversion/) (macOS 辞書.app用変換スクリプト)。