はじめに
DockerでCGI、その2。
順番としてはこっちが最初。苦労しました。
前:DockerでCGI #1: EPWING電子辞書サーバー「let me see...」 (2003年)
nicochcgi
nicochcgiはニコニコチャンネルをスクレイピング・キャッシュしてコメント付きで見るCGIサーバーです。
GitHub : nicochcgi_docker
Docker : ghcr.io/kurema/nicochcgi/nicochcgi
サーバーサイドはPerl+CGI、クライアントは素のHTML5+JavaScriptという(一昔前の)普通の構成です。Ajax時代のLAMP。
定期実行はcron。サムネイル作成はシェルスクリプト。
今なら基本ASP.NET Coreで作りますが、当時サーバーにしていたx86版Ubuntuでは使えなかったので手軽に作ってこうなりました。
2017年ごろから気が向いたときに開発を続けています。
導入方法
導入方法は以下です。
あらかじめニコニコ動画のアカウントが必要です。
$ git pull https://github.com/kurema/nicochcgi_docker.git
$ cd nicochcgi_docker
$ nano docker-compose.yml # キャッシュ場所の変更・移動先にはmkthumb.shもコピー
$ sudo docker-compose up -d
$ chmod 666 config/*
$ chmod 777 videos/*.sh
# 管理用パスワードの作成・各種設定
$ sudo docker-compose exec nicochcgi perl /var/www/html/get_password.pl
$ nano config/nicoch.conf
# cronの設定
$ sudo crontab -e
0 3 * * * cd docker-compose.ymlの場所 && docker-compose exec -T nicochcgi perl /var/www/html/nico-anime.pl >> ログファイル 2>&1 && docker-compose exec -T nicochcgi perl /media/niconico/mkthumb.sh >> ログファイル 2>&1
この後、http://server:50001/
でログインし、キャッシュするチャンネルを登録してください。
開発過程
初めてのDocker
だったので入門記事やらを読みながら試行錯誤しました。
nicochcgi自体の開発は以前行ったものなので省略します。
ベースイメージはubuntu
です。
ffmpegが数百MBを消費するので多少節約したところで誤差でしょう。httpd
よりトラブルが少なそうだと判断しました。
開発は基本的にGitHub Actionsをぐるぐる回す形でやりました。無料なので。
ローカルで試さないままリモートでやってみるという形です。
マルチプラットフォームビルドは重いのでビルド中は別の作業ができるのは楽でした。
Docker対応
Dockerfile
FROM ubuntu
MAINTAINER kurema
# https://qiita.com/Kashiwara/items/07e154bb5e859445eac6
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2
#You need to install tzdata first to prevent 'Please select the geographic area...' message.
#https://sleepless-se.net/2018/07/31/docker-build-tzdata-ubuntu/
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
#Install dependencies.
RUN apt-get install -y --no-install-recommends ffmpeg \
apache2 \
cpanminus \
build-essential \
libexpat1-dev \
#fix cpanm error...
liblwp-protocol-https-perl \
libnet-ssleay-perl \
libcrypt-ssleay-perl \
openssl \
libssl-dev && \
apt-get clean && \
rm -rf /var/cache/apt/archives/*
#Enable cgi. This is cgi in 2020.
RUN sed -ri 's/Options Indexes FollowSymLinks/Options Indexes FollowSymLinks ExecCGI/g;' /etc/apache2/apache2.conf && \
echo "AddHandler cgi-script .cgi" >> /etc/apache2/apache2.conf && \
a2enmod cgid && \
echo "-- Apache2.conf --" && \
cat /etc/apache2/apache2.conf
#Copy cpanfile first for better cache management.
RUN touch /var/www/html/cpanfile
COPY nicoch/cpanfile /var/www/html/cpanfile
RUN cpanm --installdeps --no-man-pages /var/www/html/ && \
rm -rf /root/.cpanm/work/*
COPY nicoch/ /var/www/html/
RUN chmod 755 /var/www/html/*.cgi /var/www/html/*.pl
#/etc/nicochcgi/ should be overwritten using -v
COPY config/ /etc/nicochcgi/
RUN chown www-data:www-data /etc/nicochcgi/*
RUN chmod 666 /etc/nicochcgi/*
#I think document should be included.
COPY README.md /
EXPOSE 80
CMD ["apachectl", "-D", "FOREGROUND"]
Apacheのインストール
これは検索でヒットした以下の記事を参照しました。
環境変数の用意が必要なようです。
apt-get
するだけだろうと調べなければトラブルでした。良かった。
@Kashiwara (2018) 「Dockerでapache2起動」Qiita
タイムゾーン設定
前回の記事に書きましたが、Please select the geographic area in which you live.
のエラー対策にtzdataを先にインストールします。
サーバーの性質上、タイムゾーンは日本のはず。
RUN apt-get update -y && \
apt-get install -y --no-install-recommends tzdata
ENV TZ=Asia/Tokyo
CGI有効化
ベースコンテナによってCGI有効化の手順が違います。今回は以下の条件です。
- トップページが
index.html
→DirectoryIndex
は変更不要 - CGIの拡張子は全て
.cgi
- Ubuntuなので
a2enmod
が普通に使える。
したがって、Dockerfileでは以下になりました。
RUN sed -ri 's/Options Indexes FollowSymLinks/Options Indexes FollowSymLinks ExecCGI/g;' /etc/apache2/apache2.conf && \
echo "AddHandler cgi-script .cgi" >> /etc/apache2/apache2.conf && \
a2enmod cgid
a2enmod cgid
で有効化されるのは以下の2つです。
# Socket for cgid communication
ScriptSock ${APACHE_RUN_DIR}/cgisock
LoadModule cgid_module /usr/lib/apache2/modules/mod_cgid.so
a2enmod
と同じことをhttpd
のようなa2enmod
がないベースコンテナでやりたい場合は、上の二行を追記すればいいわけですね。
(#1ではScriptSock
を忘れてちょっと困りました。)
``a2enmod cgi``
2020-10-24T05:47:59.1426858Z #21 0.362 Your MPM seems to be threaded. Selecting cgid instead of cgi.
2020-10-24T05:47:59.1427458Z #21 0.362 Enabling module cgid.
2020-10-24T05:47:59.1427912Z #21 0.367 To activate the new configuration, you need to run:
2020-10-24T05:47:59.1428339Z #21 0.367 service apache2 restart
cpanm
以前は自分のサーバーを主眼において、開発過程で必要なライブラリを都度CPANからインストールする形にしていました。
第三者がnicochcgi
をインストールする際は同様に一々確認し追加していく想定でしたが、大変面倒で時間が掛かります。
まずcpanfile
を作成しました。最初からそうするべきでしたね。
Docker側としては、キャッシュ管理のためにcpanfileのみをコピーしてcpanm --installdeps --no-man-pages
するだけ…のはずでした。
RUN touch /var/www/html/cpanfile
COPY nicoch/cpanfile /var/www/html/cpanfile
RUN cpanm --installdeps --no-man-pages /var/www/html/ && \
rm -rf /root/.cpanm/work/*
cpanmの苦労 #1: 重い
そもそもCPANのインストールは重いです。本当に重い。
一々テストやらビルドやらするからのようです。
apt install
と違って実際にCPUを使って処理するからか、マルチプラットフォーム対応時には特に大変です。
ある時はGitHub Actionsで6時間処理をしたあげく自動キャンセルされました。そんなのあるんですね。
4時間応答なしだったので処理時間の問題でもなさそうですが。メモリ不足でスワップ?
参考までに成功時のQEMUでの実行時間を表にします。
同時処理なので参考値ですが、エミュレーション時は目安で10倍程度時間が掛かるようですね。
arch | ellapsed |
---|---|
amd64 (Native) | 449.1s |
arm64 | 3921.1s |
ppc64le | 4014.9s |
arm/v7 | 3919.5s |
cpanmの苦労 #2: 依存関係
CPANはPerlモジュールだけ面倒を見るだけで、依存するバイナリパッケージを予め導入する必要があります。
今回は以下のエラーに悩まされました。
要するにLWP::Protocol::https
インストール関係の問題です。
2020-10-23T19:00:01.5141020Z #24 2933. Building and testing IO-Socket-SSL-2.068 ... ! Installing IO::Socket::SSL failed. See /root/.cpanm/work/1603476565.7/build.log for details. Retry with --force to force install it.
2020-10-23T19:00:01.6639377Z #24 3037. ! Installing the dependencies failed: Module 'IO::Socket::SSL' is not installed, Module 'IO::Socket::SSL::Utils' is not installed
2020-10-23T19:00:01.6640902Z #24 3037. ! Bailing out the installation for LWP-Protocol-https-6.09.
2020-10-23T19:00:01.7953327Z #24 3037. ! Installing the dependencies failed: Module 'LWP::Protocol::https' is not installed
2020-10-23T19:00:01.7954896Z #24 3037. ! Bailing out the installation for /var/www/html/.
2020-10-23T19:00:01.7955267Z #24 3037. FAIL
最終的にはずらずらと以下全てをapt-get
で導入し解決しました。cpanm
も早くなりました。
要するにliblwp-protocol-https-perl
他を導入すればcpanのテストをスキップし諸々導入してくれるようです。
実際には不要なものもある気がします。
検索してもドンピシャの答えがなかったので試行錯誤しました。
RUN apt-get install -y --no-install-recommends libexpat1-dev \
#fix cpanm error...
liblwp-protocol-https-perl \
libnet-ssleay-perl \
libcrypt-ssleay-perl \
openssl \
libssl-dev
cpanmの苦労 #3: ログが面倒
上のログをよく見るとSee /root/.cpanm/work/1603476565.7/build.log
との記述があります。
ローカルでDocker build
をしているなら中に入ってファイルを確認できますが、GitHub Actions+マルチプラットフォームビルドとなると正直お手上げです。
思いつく対策は3つです。
- ローカルで試してみる。
- 今回は
cpanfile
に追加後ビルドに失敗するようになったので以前のバージョンがありました。
- 今回は
-
cpanm --installdeps -v
(verboseのv) - Artifactを使うQiita記事。
- QEMUを使ってマルチプラットフォームでビルドしていて大変そうなので辞めました。
まずはローカルで試しました。docker-compose exec nicochcgi bash
で入って、cpanfile
を編集後cpanm --installdeps
するだけの作業です。
試行錯誤をするには最適ですが、ローカルで動くはずがビルドに失敗したりしました。
なので-v
で原因追及をしました。
-v
はGitHub Actionsのログが長くなるので正常動作するならお勧めしません。
普通に動くようになれば削除しました。
本来はArtifactを使うべきだと思います。
公開
#1と同じく最終的にはGitHub Container Registryに保存しました。
しかし、最初なので以下のミスをしてしまいました。
- 最初にGitHub Package Registryを使ってしまう。
- GitHub Container Registryと異なりGitHub Package Registryではパブリックイメージは削除できません。ダウンロード数が0でも。
- 古いバージョンが残ってしまいました。仕方ない。
- 名前が似ているのでよくわかってなかった。
- GitHub Container Registryでは「レポジトリ名/イメージ名」みたいなルールだと勘違いする。
- その結果イメージ名が
nicochcgi/nicochcgi
という冗長な感じに。 - 現時点の利用者はおそらく自分だけですが、名前変更は避けました。変えるのは簡単ですが履歴も消えますし。
- その結果イメージ名が
記事時点でGitHub Package RegistryのDockerの奴とGitHub Container Registryがあるので皆さん気を付けましょう。
GitHub Container Registryのバグ?
GitHub Actions
GitHub Actionsは前回同様GitHub Communityの例文を参照しました。というかこっちが先です。
.github/workflows/docker.yml: GitHub, Current
.github/workflows/docker.yml
name: Docker Container Build Workflow
#https://github.community/t/github-package-registry-does-not-support-multi-cpu-architecture-image/14339/11
on:
schedule:
- cron: '0 8 13 * *' # once a month
push:
branches:
- main
tags:
- 'v*.*.*'
pull_request:
branches:
- main
workflow_dispatch:
env:
IMAGE_NAME: nicochcgi
REPO_NAME: nicochcgi
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=ghcr.io/${{ github.repository_owner }}/${REPO_NAME}/${IMAGE_NAME}
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=monthly
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION=test
fi
TAGS="${DOCKER_IMAGE}:${VERSION}"
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=tags::${TAGS}
echo "::set-output name=latest::${DOCKER_IMAGE}:latest"
# lowercase the branch name
BRANCH=$(echo ${GITHUB_REF##*/} | tr '[A-Z]' '[a-z]')
LABELS="org.opencontainers.image.revision=$GITHUB_SHA"
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
LABELS="$LABELS,org.opencontainers.image.version=$VERSION"
LABELS="$LABELS,com.github.repo.branch=$BRANCH"
LABELS="$LABELS,com.github.repo.dockerfile=Dockerfile"
echo ::set-output name=labels::${LABELS}
BUILD_ARGS="BRANCH=$BRANCH"
echo ::set-output name=args::${BUILD_ARGS}
- name: Tag names
run: echo ${{ steps.prep.outputs.tags }}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Builder instance name
run: echo ${{ steps.buildx.outputs.name }}
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PAT }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/arm/v7
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
build-args: ${{ steps.prep.outputs.args }}
labels: ${{ steps.prep.outputs.labels }}
cache-from: type=registry,ref=${{ steps.prep.outputs.latest }}
cache-to: type=inline
時間
ビルド時間はキャッシュありで1~2分、フルのマルチプラットフォームビルドには1時間強程度です。
amd64,arm64,ppc64le,arm/v7
の同時ビルドで1時間19分、s390x
単独で45分です。
前述のとおり最悪ケースで6時間後に強制停止されました。
ざっと計算したところ合計13.5時間程度回しているようです。
パブリックレポジトリなので全部無料。お得感ヤバいです。
無料だから過剰に回してる感はありますが、一応キャッシュは使ってます。
cron
このスクリプトはcronで定期実行する設計になっています。
Docker内でcronを動かすのは以下の理由で望ましくないので、下の記事を参考にホスト側のcronから設定するようにしました。
そのせいでセットアップの手間が増えています。
-
docker pull
などをする度に設定が消える。 - コンテナで動かすプロセスが増える。
- ログ管理が面倒。
@YuukiMiyoshi(2019) 「Docker + Cron環境を実現する3つの方法」 Qiita
懸念点
1コンテナ1プロセス
Dockerは1コンテナ1プロセスが推奨されているようです(Stack Exchange)。
cgidは一応デーモンですから、これを逸脱しています。
セットアップの簡便さのためにDockerを利用している感覚なので別にどっちでもいいですし、現実問題Apacheとcgidを分けろと言われても無理ですが、マシン再起動時にcgidの起動が失敗する例を確認しました。
Address already in use: AH01243: Couldn't bind unix domain socket
※cgidの起動が失敗する理由は「1コンテナ1プロセス」と無関係なようなので別の節にしました。
うっかりdocker-compose restart
してしまったのでログが消えてしまいましたが、ブラウザ側からは503 Service Unavailable
が、ログにはcouldn't bind unix domain socket
などが記録されていたはずです。
1コンテナ1プロセスにしておけば発生しなかった問題のようにも見えました。タイミング的な何かでしょうか。謎です。
${APACHE_RUN_DIR}
にダミーファイルを作成すれば良いらしいです。
不正なシャットダウン時(停電とか)にcgisock.*
ファイルが生き残るのが原因みたいです。普通にrm
できるので、コンテナ起動時に削除すれば良いのでしょうか。
apache2.pid
はスルーして起動してくれるんで、cgid
も無視して起動して欲しいです。
[Sun Dec 20 22:38:39.951792 2020] [core:warn] [pid 7:tid 140237580024896] AH00098: pid file /var/run/apache2/apache2.pid overwritten -- Unclean shutdown of previous Apache run?
[Sun Dec 20 22:38:39.951398 2020] [cgid:error] [pid 8:tid 140237580024896] (98)Address already in use: AH01243: Couldn't bind unix domain socket /var/run/apache2/cgisock.7
若干自信がないですが、これで良さそう。
注意点。
-
Dockerfile
のRUN
で使われるシェルは標準でbash
ではなくsh
-
bash
だと複数行の出力はecho -e "1\n2"
になるが、sh
だとecho "1\n2"
。
-
-
CMD ["/startup.sh"]
では正常に起動しない。-
CMD [""]
はシェルで実行するわけではなく直接呼び出す。 - Dockerfile リファレンス
-
RUN echo "#/bin/sh\nrm -f /var/run/apache2/cgisock.*\napachectl -D FOREGROUND" > /startup.sh && \
chmod 777 /startup.sh
EXPOSE 80
CMD ["sh", "-c", "/startup.sh"]
容量
nicochcgiのイメージは654MBにもなります。
ubuntuにffmpegをインストールすると557MB要求されるのでほとんどがffmpegです。
多少軽量化しても無駄なのが良く分かりますね。
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ghcr.io/kurema/nicochcgi/nicochcgi latest 14fdd99f2dc5 22 hours ago 654MB
ghcr.io/kurema/letmesee latest 869e7d478ba0 4 days ago 414MB
httpd latest 3dd970e6b110 7 weeks ago 138MB
ubuntu latest 9140108b62dc 2 months ago 72.9MB
$ apt install --no-install-recommends ffmpeg
...
Need to get 90.1 MB of archives.
After this operation, 557 MB of additional disk space will be used.
正直、654MBと言えば仮想マシンみたいな数字で「これをコンテナ内で実行するのはちょっと…」という気になります。
どうせサーバーには普通にffmpeg入れますし、エンコード支援とかが関係すれば厄介そうです(今回はサムネ作成とフォーマット変換程度ですが)。
「Docker ffmpeg」で検索すれば2014年のクックパッドの記事が出たりしますから普通にアリっぽいですし、スクリプトを変更するのも嫌なので仕方ないですが、微妙な感じはします。
サイズを縮小するなら用途を絞って自前コンパイルでもするのが選択肢かもしれませんが、色々とあまりやりたくはないです。
- shin1ohno (2014)「Dockerでffmpegもimagemagickも怖くないという話」 クックパッド開発者ブログ
参考記事
Docker入門にあたって参考にした記事を下に挙げます。
- Docker
- 武井 (2020) 「【連載】世界一わかりみが深いコンテナ & Docker入門」SIOS TECH.LAB
- Docker全般。入門で一番役に立ちました。
- kooohei (2015) 「Dockerコンテナの作成、起動〜停止まで」 Qiita
- Dockerのコマンド集です。基本。
- 最近はビルドはGitHub Actionsで、サーバー上では
docker-compose
メインで使う数種類程度は覚えました。
- senyoltw (2015)「Docker上でApacheコンテナを作成しCGIのコンテンツを走らせるまで」ワタナベ書店
- #1でも参照したCGIの記事です。
-
#Scriptsock cgisock
のコメントインしていない点だけは注意が必要です。
- @yohm (2019) 「dockerでvolumeをマウントしたときのファイルのowner問題」 Qiita
- dd_fort (2020) 「Dockerのvolumeでpermission deniedが発生した場合の解決法」 ラクス エンジニアブログ
-
docker -v
のownerの話はめんどくさいですね。chmod
で権限を緩くすることで強引に解決してます。 - 自分の環境ではとりあえず問題は発生していません。
-
-
@YuukiMiyoshi (2019) 「Docker + Cron環境を実現する3つの方法」 Qiita
- Dockerでcronを実行するには。「ホストのcronからコンテナ内のプログラムを実行する」の方法を選択しました。
-
@homines22 (2020) 「【GitHub Actions】GitHub Package Registryを利用して同一Dockerイメージをjobで共有する」 Qiita
- GitHub Package Registryなのでそろそろ非推奨です。
- ただしGitHub Container Registryは現在ベータなのでまだGitHub Package Registryの方が良いのかも。
- 武井 (2020) 「【連載】世界一わかりみが深いコンテナ & Docker入門」SIOS TECH.LAB
- Apache
- shinoda (2017)「Apache2 で、CGI ソースが表示されちゃう件の対応(Debian APT パッケージ編)」電気ウナギ的○○
- よく読めば
a2enmod cgi
でcgidがロードされると書いてありますね。
- よく読めば
- shinoda (2017)「Apache2 で、CGI ソースが表示されちゃう件の対応(Debian APT パッケージ編)」電気ウナギ的○○
おまけ
Dockerの初体験的な感じだったので「Docker未経験から2週間で」みたいなタイトルにしようかと思いましたが辞めました。
既に流行ってないみたいだし、「確かにDockerは未経験だけど…」って感じなので。
補足
似たようなことをやっている人を見てみると、以下の設定をhttpd.conf
に追記するのも良さそうです。
httpd-security-fswiki-local.conf
### Security Headers for docker httpd:2.4.46 and httpd:2.4.46-alpine
## Against Signature
ServerTokens Prod
ServerSignature Off
## Against Clickjacking
# Header set X-Frame-Options SAMEORIGIN
Header always append X-Frame-Options DENY
## Against XSS
Header always set X-Content-Type-Options nosniff
## Against XST
TraceEnable Off
## CSP
# Header always set Content-Security-Policy "default-src 'self'; img-src *;"
Header always set Content-Security-Policy "default-src 'self';"
dockerfile_fswiki_local
MIT License
Copyright (c) 2020 Kaz KOBARA