1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DockerでCGI #2: ニコニコチャンネル キャッシュサーバー 「nicochcgi」

Last updated at Posted at 2020-12-01

はじめに

DockerでCGI、その2。
順番としてはこっちが最初。苦労しました。

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

nicochcgi

nicochcgiはニコニコチャンネルをスクレイピング・キャッシュしてコメント付きで見るCGIサーバーです。

GitHub : nicochcgi_docker
Docker : ghcr.io/kurema/nicochcgi/nicochcgi

image.png

Microsoft Storeに専用アプリもあります。
Japanese badge

サーバーサイドは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: GitHub, Current

Dockerfile
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を先にインストールします。
サーバーの性質上、タイムゾーンは日本のはず。

Dockerfile
RUN apt-get update -y && \
    apt-get install -y --no-install-recommends tzdata
ENV TZ=Asia/Tokyo

CGI有効化

ベースコンテナによってCGI有効化の手順が違います。今回は以下の条件です。

  • トップページがindex.htmlDirectoryIndexは変更不要
  • CGIの拡張子は全て.cgi
  • Ubuntuなのでa2enmodが普通に使える。

したがって、Dockerfileでは以下になりました。

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つです。

cgid.conf
# Socket for cgid communication
ScriptSock ${APACHE_RUN_DIR}/cgisock
cgid.load
LoadModule cgid_module /usr/lib/apache2/modules/mod_cgid.so

a2enmodと同じことをhttpdのようなa2enmodがないベースコンテナでやりたい場合は、上の二行を追記すればいいわけですね。
(#1ではScriptSockを忘れてちょっと困りました。)

``a2enmod cgi``
記事を書いている間に気づきましたが、誤って``a2emod cgi.load``になっていました。 実際にはマルチスレッドなので``a2enmod cgid``にする必要があります。 それに対しa2enmodは勝手に判断して``cgid``を有効にしてくれていました。 ``mods-enabled``に``mods-available``からのリンクを作ってくれるだけのお手軽ツールだと思ってましたが、なかなか賢いですね。
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するだけ…のはずでした。

Dockerfile
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時間応答なしだったので処理時間の問題でもなさそうですが。メモリ不足でスワップ?

image.png
image.png

参考までに成功時の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のバグ?
ちなみに記事投稿時点でタグの付いていないイメージが一覧に見えますが何でしょうねこれ。 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/07530199-f8fa-7348-e6bd-799287dc8d35.png)

GitHub Actions

GitHub Actionsは前回同様GitHub Communityの例文を参照しました。というかこっちが先です。

.github/workflows/docker.yml: GitHub, Current

.github/workflows/docker.yml
.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時間後に強制停止されました。

image.png

ざっと計算したところ合計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

若干自信がないですが、これで良さそう。
注意点。

  • DockerfileRUNで使われるシェルは標準でbashではなくsh
    • bashだと複数行の出力はecho -e "1\n2"になるが、shだとecho "1\n2"
  • CMD ["/startup.sh"]では正常に起動しない。
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年のクックパッドの記事が出たりしますから普通にアリっぽいですし、スクリプトを変更するのも嫌なので仕方ないですが、微妙な感じはします。

サイズを縮小するなら用途を絞って自前コンパイルでもするのが選択肢かもしれませんが、色々とあまりやりたくはないです。

参考記事

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の方が良いのかも。
  • Apache
    • shinoda (2017)「Apache2 で、CGI ソースが表示されちゃう件の対応(Debian APT パッケージ編)」電気ウナギ的○○
      • よく読めばa2enmod cgiでcgidがロードされると書いてありますね。

おまけ

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?