docker-composeでMediaWikiを動かす

ローカルネットワークで wiki を運用してるんだけど、Docker 上に移行すべく、今日はそのテスト。

環境

  • Ubuntu 20.04
  • Docker 19.03.8
  • docker-compose 1.25.0

Dockerイメージ

MediaWiki も MariaDB も Docker Hub に公式イメージが有るのでそれを使わせてもらう。データベースは、はじめは MySQL を試したんだけどうまく動かなかった(原因不明)ので MariaDB に変えた。

  • mediawiki:1.35.0
  • mariadb:10.5.6-focal

ディレクトリ構成とファイル

用意した構成はこんなの:

takatoh@apostrophe:testwiki$ tree .
.
├── docker-compose.yml
├── mysql
│   └── db
└── wiki
    └── images

docker-compose.yml はこう:

version: '3'

services:
  testwiki:
    container_name: testwiki
    image: mediawiki:1.35.0
    restart: always
    ports:
      - 8888:80
    volumes:
      - ./wiki/images:/var/www/html/images
#      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

  mysql:
    container_name: db
    image: mariadb:10.5.6-focal
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpasswd
      MYSQL_DATABASE: testwiki
      MYSQL_USER: mysqluser
      MYSQL_PASSWORD: mypassword
    ports:
      - 3306:3306
    volumes:
      - ./mysql/db:/var/lib/mysql

コメントアウトしてある行は MediaWiki の設定ファイル。これはインストールが済んでから使う。

コンテナの起動とMediaWikiのインストール

docker-compose コマンドでコンテナを起動する。

takatoh@apostrophe:testwiki$ docker-compose up -d

ブラウザで http://localhost:8888/ にアクセスして MediaWiki のインストール(というかセットアップというか)をする。その際、データベース関係は docker-compose.yml の記述に合わせる。

  • データベースのホスト:db
  • データベース名:testwiki
  • インストールで使用する利用者アカウント→データベースのユーザ名:mysqluser
  • インストールで使用する利用者アカウント→データベースのパスワード:mypassword

その他はそれなりに設定すればいい。最後に LocalSettings.php ファイル(設定ファイル)をダウンロードしてインストールは終わり。

設定の反映

いったんコンテナを止める。

takatoh@apostrophe:testwiki$ docker-compose down

ダウンロードした設定ファイルを配置。

takatoh@apostrophe:testwiki$ cp ~/Downloads/LocalSettings.php wiki

docker-compose.yml のコメントをはずす(該当行だけ示す)。

      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

コンテナを再起動。

takatoh@apostrophe:testwiki$ docker-compose up -d

これで無事起動した。

参考にしたページ

[追記:2020/11/5] データベースの移行

旧い wiki からデータを移行する手順。

  • データは wiki.sql ファイルにダンプしてあるものとする
  • 旧いほうの MediaWiki のバージョンは 1.27.1

wiki.sql ファイルを Docker コンテナと共有しているディレクトリにコピーする。

takatoh@apostrophe:testwiki$ cp wiki.sql ./mysql/db

./mysql/db ディレクトリは、データベースの Docker コンテナ(コンテナ名は db)からは /var/lib/mysql として認識されている(前述の docker-compose.yml ファイルを参照)。なのでデータベースのコンテナに接続して、データを流し込む。

takatoh@apostrophe:testwiki$ docker exec -it db bash
root@007d71dfcb37:/# cd /var/lib/mysql
root@007d71dfcb37:/var/lib/mysql# ls *.sql
wiki.sql
root@007d71dfcb37:/var/lib/mysql# mysql -u mysqluser -p testwiki < wiki.sql
Enter password:
root@007d71dfcb37:/var/lib/mysql# exit
exit

これでデータベース側での作業は終了。ただ、このままだと MediaWiki でエラーになる。バージョンが上がっているので MediaWiki の使用するデータベーススキーマとかも変わっているからだ。

そこで、MediaWiki のコンテナに接続して更新スクリプトを実行する。更新スクリプトは /var/www/html/maintenance/update.php だ。

takatoh@apostrophe:testwiki$ docker exec -it testwiki bash
root@f92dd50471e5:/var/www/html# cd maintenance
root@f92dd50471e5:/var/www/html/maintenance# php update.php

これで完了。

[追記]

11/7、本番環境も無事 Docker 上に移行した。

ひとそろいの画像ファイルからEPUB(電子書籍)ファイルを作る

Qiita の↓の記事を読んで、EPUB って結構簡単(もちろん単純なものなら)なんだな、と思ったので Python でスクリプトを作ってみた。

作ったもの

フォルダに入った画像ファイル一式から EPUB ファイルを生成する。

  • 1ページ1画像のファイル一式
  • とりあえず PNG にだけ対応
  • 画像ファイルはファイル名でソートするので連番でなくても構わない
  • 目次とかそういうのはなし
  • 元データのフォルダ名が EPUB のタイトル、ファイル名になる

ファイル構成は次の通り:

takatoh@montana: img2epub > tree /f .
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB
│  .gitignore
│  img2epub.py
│
├─data
│      book.opf.template
│      chap1.xhtml.template
│      container.xml
│      nav.xhtml
│
└─sample
        sample-000.png
        sample-001.png
        sample-002.png
        sample-003.png
        sample-004.png
        sample-005.png
        sample-006.png
        sample-007.png

img2epub.py が Python で書いたスクリプト本体。data フォルダ以下のファイルは EPUB を構成するファイルあるいはそのテンプレート。スクリプトは次の通り:

#!/usr/bin/env python
# encoding: utf-8


import sys
import os
import shutil
import subprocess
from datetime import datetime, timezone
import uuid
import glob
from jinja2 import Template, Environment, FileSystemLoader


def main():
    src_dir = sys.argv[1]
    now = datetime.now(timezone.utc)
    tmp_dir_name = "tmp.epub.{time}".format(time=now.strftime("%Y%m%d%H%M%S"))

    make_dirs(tmp_dir_name)
    images = copy_images(src_dir, tmp_dir_name)
    images = [s.replace("\\","/") for s in sorted(images)]
    gen_mimetype(tmp_dir_name)
    copy_container(tmp_dir_name)
    book_opf_context = {
        "title": src_dir,
        "time": now.isoformat(),
        "images": images
    }
    gen_book_opf(tmp_dir_name, book_opf_context)
    copy_nav(tmp_dir_name)
    gen_chap1_xhtml(tmp_dir_name, book_opf_context)
    zip_epub(tmp_dir_name, src_dir)


def make_dirs(tmp_dir_name):
    os.makedirs(os.path.join(tmp_dir_name, "META-INF"))
    os.makedirs(os.path.join(tmp_dir_name, "EPUB"))


def copy_images(src_dir, tmp_dir_name):
    images_dir = os.path.join(tmp_dir_name, "EPUB/images")
    shutil.copytree(src_dir, images_dir)
    return glob.glob("{dir}/*".format(dir=images_dir))


def gen_mimetype(tmp_dir_name):
    with open(os.path.join(tmp_dir_name, "mimetype"), "w") as f:
        f.write("application/epub+zip")


def copy_container(tmp_dir_name):
    shutil.copyfile("data/container.xml", os.path.join(tmp_dir_name, "META-INF/container.xml"))


def gen_book_opf(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("book.opf.template")
    context["images"] = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    context["cover"] = context["images"][0]
    context["uuid"] = str(uuid.uuid4())
    with open(os.path.join(tmp_dir_name, "EPUB/book.opf"), "w") as f:
        f.write(template.render(context))


def copy_nav(tmp_dir_name):
    shutil.copyfile("data/nav.xhtml", os.path.join(tmp_dir_name, "EPUB/nav.xhtml"))


def gen_chap1_xhtml(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("chap1.xhtml.template")
    images = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    with open(os.path.join(tmp_dir_name, "EPUB/chap1.xhtml"), "w") as f:
        f.write(template.render(images=images))


def zip_epub(tmp_dir_name, title):
    epub_file_name = "../{title}.epub".format(title=title)
    os.chdir(tmp_dir_name)
    subprocess.run(["zip", "-X0", epub_file_name, "mimetype"], stdout=subprocess.DEVNULL)
    subprocess.run(["zip", "-r9", epub_file_name, "*", "-x", "mimetype"], stdout=subprocess.DEVNULL)
    os.chdir("..")



main()

で、sample フォルダ以下が元になる画像ファイル一式。

使い方

スクリプトの引数に画像一式が入っているフォルダを指定するだけ。

takatoh@montana: img2epub > python img2epub.py sample

そうすると、EPUB ファイル(今回は sample.epub)と、EPUB にまとめる前のファイル一式の入ったフォルダ(同じく tmp.epub.20201027130853)ができる。このフォルダはテンポラリなものなので消しちゃってもいいんだけど、今の段階ではまだ残している。

takatoh@montana: img2epub > ls


    Directory: C:\Users\takatoh\Documents\w\img2epub

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/27    20:07                data
d----          2020/10/27    20:09                sample
d----          2020/10/27    22:08                tmp.epub.20201027130853
-a---          2020/10/27    20:07              9 .gitignore
-a---          2020/10/27    20:07           2832 img2epub.py
-a---          2020/10/27    22:08        8011218 sample.epub

あとは出来上がった sample.epub ファイルを好きな EPUB ビューワで見ればいい。

EPUB のファイルの構成

EPUB のファイルっていうのは、基本的には使用で決められたファイルとコンテンツを zip で一つにまとめて、拡張子を .epub にしただけのファイルだ。単純に zip にしただけではないんだけど、そのへんはこの記事では触れない。冒頭の Qiita の記事か、EPUB 3.2 の仕様を参照のこと。

今回作ったスクリプト img2epub.py では、tmp.epub.* フォルダにその一式が入っている(つまりこれを zip 圧縮して .epub ファイルを作る)。フォルダの中身は次の通り:

takatoh@montana: img2epub > tree /f tmp.epub.20201027130853
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB\TMP.EPUB.20201027130853
│  mimetype
│
├─EPUB
│  │  book.opf
│  │  chap1.xhtml
│  │  nav.xhtml
│  │
│  └─images
│          sample-000.png
│          sample-001.png
│          sample-002.png
│          sample-003.png
│          sample-004.png
│          sample-005.png
│          sample-006.png
│          sample-007.png
│
└─META-INF
        container.xml

EPUB/images 以下の画像ファイルは、元のデータをコピーしたもの。そのほかのファイルはスクリプトが生成したファイルだ。詳しくは略。

余談

EPUB を構成するファイルの一部(book.opf や chap1.xhtml)を生成するためにテンプレートエンジンを使ってるんだけど、Python には string.Template というテンプレートエンジンが標準でついている。これ、今回調べてて初めて知った。

ところがこの string.Template、単純な値の挿入はできるけど繰り返しや条件分岐の機能がない。今回、条件分岐は使ってないけど繰り返すは必要だったので、結局 Jinja2 を使った。標準添付されてるのはいいけど、変数を値に置き換えるだけしかできないんじゃ、用途は限られるよなぁ。

Poppler for WindowsでPDFをPNGに変換する

PDF をページごとの画像ファイルに変換したくて、はじめは Python でできないか調べてた。そしたら↓のページで pdf2image という(Pythonの)ライブラリを紹介しているのを見つけた。

ところが記事を読んでみるとこう書いてある:

pdf2imageは「Poppler」というフリーのPDFコマンドラインツールを背後で用います。そのため、Popplerをダウンロードしておく必要があります。

それなら Poppler をそのまま使えばいいじゃん。

というわけで、Poppler for Windows をダウンロードした。

バージョンは 0.68.0 (poppler-0.68.0_x86.7z)。7zip なので 7z コマンドをインストールしてから展開した。

展開したファイルを眺めてみると、bin フォルダの中に pdfimages.exe という実行ファイルがある。これが使えそうだ。PATH を通してとりあえずヘルプを見てみた。

takatoh@montana: tmp > pdfimages -h
pdfimages version 0.68.0
Copyright 2005-2018 The Poppler Developers - http://poppler.freedesktop.org
Copyright 1996-2011 Glyph & Cog, LLC
Usage: pdfimages [options] <PDF-file> <image-root>
  -f <int>       : first page to convert
  -l <int>       : last page to convert
  -png           : change the default output format to PNG
  -tiff          : change the default output format to TIFF
  -j             : write JPEG images as JPEG files
  -jp2           : write JPEG2000 images as JP2 files
  -jbig2         : write JBIG2 images as JBIG2 files
  -ccitt         : write CCITT images as CCITT files
  -all           : equivalent to -png -tiff -j -jp2 -jbig2 -ccitt
  -list          : print list of images instead of saving
  -opw <string>  : owner password (for encrypted files)
  -upw <string>  : user password (for encrypted files)
  -p             : include page numbers in output file names
  -q             : don't print any messages or errors
  -v             : print copyright and version info
  -h             : print usage information
  -help          : print usage information
  --help         : print usage information
  -?             : print usage information

出力は JPEG が欲しかったので -j オプションを指定。<image-root> が何を指すのかよくわからないけどテキトーに。そしたらこうなった。

takatoh@montana: tmp > pdfimages -j sample.pdf foo
takatoh@montana: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24     8:53        5364971 foo-000.ppm
-a---          2020/10/24     8:53        5364971 foo-001.ppm
-a---          2020/10/24     8:53        5364971 foo-002.ppm
-a---          2020/10/24     8:53        5364971 foo-003.ppm
-a---          2020/10/24     8:53        5364971 foo-004.ppm
-a---          2020/10/24     8:53        5364971 foo-005.ppm
-a---          2020/10/24     8:53        5364971 foo-006.ppm
-a---          2020/10/24     8:53        5364971 foo-007.ppm
-a---          2020/03/28     9:12        4082441 sample.pdf

.ppm って!いまどき .ppm ファイルなんて何で見ればいいんだ。なら PNG でいいや。あと、<image-root> は出力ファイルのプレフィックスみたいだな。なので .ppm ファイルは削除してやり直した。

takatoh@montana: tmp > pdfimages -png sample.pdf sample
takatoh@montana: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/03/28     9:12        4082441 sample.pdf
-a---          2020/10/24     9:01        1269290 sample-000.png
-a---          2020/10/24     9:01         446405 sample-001.png
-a---          2020/10/24     9:01         893712 sample-002.png
-a---          2020/10/24     9:01        1258104 sample-003.png
-a---          2020/10/24     9:01        1301072 sample-004.png
-a---          2020/10/24     9:01        1344592 sample-005.png
-a---          2020/10/24     9:01        1157016 sample-006.png
-a---          2020/10/24     9:01         755768 sample-007.png

これで OK。だけど、欲を言えば出力されるファイルをひとつのフォルダに入れたい。それらしいオプションは見当たらないけど、プレフィックスにパスを含めてやればできた。フォルダは先に作っておくこと。

takatoh@montana: tmp > mkdir out


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/24    11:29                out

takatoh@montana: tmp > pdfimages -png sample.pdf out/sample
takatoh@montana: tmp > ls out


    Directory: C:\Users\takatoh\Documents\tmp\out

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24    11:29        1269290 sample-000.png
-a---          2020/10/24    11:29         446405 sample-001.png
-a---          2020/10/24    11:29         893712 sample-002.png
-a---          2020/10/24    11:29        1258104 sample-003.png
-a---          2020/10/24    11:29        1301072 sample-004.png
-a---          2020/10/24    11:29        1344592 sample-005.png
-a---          2020/10/24    11:29        1157016 sample-006.png
-a---          2020/10/24    11:29         755768 sample-007.png

できた。

補足。

-j オプションの説明は write JPEG images as JPEG files となっている。PDFの中身が JPEG なら JPEG で出力するってことのようだ。

Windows10のiTunesからMacのミュージックアプリへ音楽の移行は出来ない

出来ない、というか少なくともウチの環境では出来なかった。

移行アシスタント

Windows10 の iTunes にある音楽を新しい iMac のミュージックアプリにどうやって移行するのか、ググってみたところで見つかったのが「移行アシスタント」だ。これは同じネットワーク内にある Windows 側と iMac 側の両方で移行アシスタントを実行すると簡単にデータ(今回の目的は音楽だけだけど、これに限らない)を移行できるというもの。少なくとも Apple のページにはそう書いてある。

ところが、実際にページに書いてある手順でやろうとしたところ、そもそも Windows 版の移行アシスタントがインストールできない。ファイルをダウンロードし直したり、PC を再起動したりと色々やってみたけど、どうにもならない。移行アシスタントアプリのバージョンのせいなのかそれとも Windows の環境のせいなのか、何度やってもダメだった。

USBメモリでファイルを移動

仕方がないので、Windows 側の iTunes のデータフォルダを丸ごと USB メモリにコピーして iMac に挿した。で、ミュージックアプリにインポート。時間はかかったけど、とりあえず曲の移行はできた。

が、だ。

このやり方はつまり、ミュージックアプリに新しい曲をファイルから追加しただけなので、再生回数みたいなメタデータやプレイリストは移行されてない。

これらをどうやったら Windows から持ってくれるか、調べてみたけどいい方法が見つからなかった。もしかしたらやり方があるのかもしれないけど。

で、いつまでもこれだけに構っていられないので、もう諦めることにした。プレイリストもいくつか程度しか作ってないし、作り直せばいいだろ。

というわけで、何の役にも立たない記事だな。

あ、あとこれだけは書いておく。USB メモリを Windows と Mac の両方で使うには exFAT でフォーマットしておく必要がある。

自宅ネットワーク内のコンピュータのIPアドレス割当てを固定にした

昨日、今日と2日続けて停電が発生した。午後に降った雷雨のせいだ(たぶん)。停電は短時間(1分もない)だったけど断続的に発生して、おかげで普段つけっぱなしの PC の電源が落ちた。サーバにしている PC も、だ。

雨がおさまってきたころ、PC の電源を入れて立ち上げたけど、サーバ PC で動かしている web アプリにアクセスしても繋がらなくなっていた。どうも IP アドレスが変わってしまったようだ。停電でルータの電源も落ちたからだな(たぶん)。

たいていの家庭のネットワークも同じだと思うけど、ウチのネットワークもルータの DHCP 機能を利用して各 PC に自動で IP アドレスを割り当てている。今回、ルータの電源も落ちたことで、PC を立ち上げたときに割り当てられたアドレスが変わってしまったらしい。

で、リモートではつながらないのでローカルにログインして、割り当てられている IP アドレスを確認して、hosts ファイルを修正したら繋がるようになった。

さて、もしかすると明日も雷雨で停電するかもしれないし、それでまた IP アドレスが変わってしまうようなことになればまた今日と同じ作業をしなくちゃいけなくなる。そんなのは面倒なので、DHCP で割り当てられる IP アドレスを固定にすることにした。

ルータは web インターフェイスで設定できるので、ログインしたら DHCP の設定のページで固定割り当ての設定をする。もう少し詳しく書くと、PC の NIC の MAC アドレスと、割り当てたい IP アドレスの組を登録しておくってことだ。PC が何十台もあるわけじゃないから大した作業ではない。

ともあれ、これで電源が落ちても今日みたいな作業をしなくても済むはずだ。もっと早くやっときゃよかった。

Arctic Code Vault Contributor

今日、久しぶりに GitHub にアクセスしたら、自分のプロファイルの下のところに「Arctic Code Vault Contributor」なる文字があるに気がついた。

何かと思って調べてみると、GitHub のリポジトリのソースコードを北極圏の島にアーカイブして1000年後まで保管するとかいう計画に選ばれたらしい。

「GitHub Arctic Code Vault」というプロジェクトに一環で、北極圏にあるノルウェーのスヴァールバル諸島の永久凍土の地下(廃坑)のなかにマイクロフィルムに保存されたソースコード(2020年2月2日に取得したスナップショット)を保存するというものだ。今回は第1回目で、頻度は明らかにされていないものの数年に一度くらいの割合でスナップショットを保存するらしい。

それにしても、オレの書いたソースコードが1000年先まで残るのか。スゲー。

……と思いながらよく読むと、アクティブなパブリックリポジトリはすべて対象になったようだ。オレの書いたコードなんて自分でしか使わないようなのばっかなんだけど、そういうことね。いや、それにしてもすごいけど。

ところで、今回保存されたソースコードの総量は約21TBとのこと。圧縮してるのかもしれないけど、以外に少ないと感じた。まぁ、ソースコードってほとんどはテキストファイルだからそんなものなのかな。

Docker Compose

前回の記事では、Docker のネットワークを使って通信しながら、複数のコンテナを協働させた。

基本的には前回の記事のやり方でいいんだけど、サービスの数が増えたり、規模が大きくなってコンテナの数が増えたりすると、コンテナごとにコマンドを打って起動するのは面倒だ。コンテナによって指定するオプションも異なるので、それも覚えておく必要がある。

というわけで、そういう煩雑な部分を楽にしてくれるツールがある。それが Docker Compose だ。メインマシンである apostrophe をセットアップしたときに apt でインストールしておいた。

akatoh@apostrophe:docker$ docker-compose --version
docker-compose version 1.25.0, build unknown

題材と構成

前回記事と同じ、書籍管理 web アプリと HTTP サーバ(Nginx)という構成にする。詳しくは前回記事を参照。

ビルド済みのイメージを利用する

前回ビルドした Docker イメージが残っているので、まずはそれを使ってコンテナを起動する設定を書いてみる。

Docker Compose は docker-compose.yml という名前のファイルを読み込んで、その設定どおりにコンテナを起動してくれる。今回書いた docker-compose.yml ファイルは次の通り。

version: '3'
services:

  bruschetta-back:
    image: bruschetta:1
    container_name: bruschetta-back
    restart: always

  nginx-front:
    image: nginx-c:1
    container_name: nginx-front
    restart: always
    ports:
      - 8080:80

service: の下に連なっているのが、Docker Compose で起動するコンテナだ。各設定項目は見ればだいたいわかるだろうからここでは説明は省略。ただ、docker コマンドで起動するときとは次の点が違う。

  • Docker ネットワークは自動で作ってくれるので設定不要
  • コンテナ間通信は、コンテナ名ではなく service 名で行われる

特に2つ目の挙動は docker コマンドで起動したときと違うので注意が必要だ。最初、ここでハマった。

で、これを起動するには次のようにする。

takatoh@apostrophe:docker$ docker-compose up -d

起動中のコンテナの確認。

takatoh@apostrophe:docker$ docker-compose ps

そして停止。

takatoh@apostrophe:docker$ docker-compose stop

最後にコンテナの削除。

takatoh@apostrophe:docker$ docker-compose rm

Dockerイメージをビルドして利用する

さて、今度は Docker イメージをビルドするところからやってみる。そのためには、docker-compose.yml ファイルと同じディレクトリに、イメージ作成用のファイル一式を収めたディレクトリを置いておく。こんな感じ。

akatoh@apostrophe:docker$ ls -l
合計 12
drwxrwxr-x 3 takatoh takatoh 4096 5月 9 03:15 bruschetta
-rw-rw-r-- 1 takatoh takatoh 239 6月 14 13:26 docker-compose.yml
drwxrwxr-x 3 takatoh takatoh 4096 5月 14 06:19 nginx-c

そして、docker-compose.yml ファイルを次のように書き変える。

version: '3'
services:

  bruschetta-back:
    build: ./bruschetta
    image: bruschetta:1
    container_name: bruschetta-back
    restart: always

  nginx-front:
    build: ./nginx-c
    image: nginx-c:1
    container_name: nginx-front
    restart: always
    ports:
      - 8080:80

書き変えた、というか書き足したのは、build: の行だ。この行でイメージをビルドするための Dockerfile (とその他に必要なフィアル)を置いてあるディレクトリを指定している。

ビルドするには次のようにする。

takatoh@apostrophe:docker$ docker-compose build

イメージができたか、確認。

takatoh@apostrophe:docker$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
nginx-c             1                   c8b3966bbc31        54 seconds ago       127MB
bruschetta          1                   7d6607e796c2        About a minute ago   468MB
nginx               1.17.10             9beeba249f3e        4 weeks ago          127MB
ubuntu              20.04               1d622ef86b13        7 weeks ago          73.9MB

nginx-c:1 と bruschetta:1 がビルドされたイメージ。下の2つは元になったイメージ(Docker Hub から取ってきたもの)だ。

じゃあ、これを起動してみよう。

takatoh@apostrophe:docker$ docker-compose up -d

OK。ブラウザからアクセスしてみて、期待通りに動いているのを確認できた。

Dockerコンテナ間通信

Docker を使ったサービスでは、サービスを構成する各サーバプログラムをそれぞれ別のコンテナで立ち上げるのが一般的なようだ。例えばデータベース(MySQLなど)を利用する web アプリケーションであれば、つぎの3つのコンテナを立ち上げることになる:

  • データベースサーバ
  • web アプリケーションサーバ
  • HTTP サーバ

当然、これらを連携するにはコンテナ間をまたいだ通信をする必要がある。Docker にはコンテナ間通信を実現するネットワーク機能が備わっていて、各コンテナは同じ(Docker の)ネットワークに接続していれば、コンテナ名とポート番号を使って通信することができる。

今日はそのコンテナ間通信を試してみる。

題材と構成

先日作った書籍管理 web アプリと、HTTP サーバとして Nginx を前に立てる構成とする。上にはデータベースサーバが書いてあるけど、この書籍管理アプリはデータベースに Sqlite を使っているので、今回データベースサーバはなし。アプリケーションサーバと HTTP サーバ(Nginx)の2つだ。

コンテナ名は、アプリケーションサーバを bruschetta-back、HTTP サーバを nginx-c とする。この2つが Docker ネットワークで通信するわけだ。nginx-c は80番ポートをホスト側の8080ポートに接続する。ホスト側からは bruschetta-d というホスト名で nginx-c にアクセスできるようにしておく。

アプリケーションサーバ

先日作ったものなので詳細は省略。ただ、あいだに PC のリプレイスをはさんだので、イメージ名が bruschetta:1 に変わっている。内容は変更なし。

HTTPサーバ

全面に立てる HTTP サーバには、Docker Hub で公開されている Nginx こイメージをメースにして、アプリケーションサーバにつなぐための設定ファイルをコピーしたものを用意する。

Dockerfile はこう:

ROM nginx:1.17.10

COPY ./files/bruschetta-d /etc/nginx/conf.d/bruschetta-d.conf
RUN mkdir /var/log/nginx/bruschetta

CMD [ "nginx", "-g", "daemon off;" ]

この中でコピーしている Nginx の仮想ホストの設定ファイルはこうだ:

upstream uwsgi-bruschetta {
    server bruschetta-back:5000;
}

server {
    # port
    listen      80;

    # server name
    server_name bruschetta-d;

    # log files
    access_log /var/log/nginx/bruschetta/access.log combined;
    error_log  /var/log/nginx/bruschetta/error.log  warn;

    keepalive_timeout     60;
    proxy_connect_timeout 60;
    proxy_read_timeout    60;
    proxy_send_timeout    60;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://uwsgi-bruschetta;
    }

}

これで docker build した。

できたイメージの状況。

akatoh@apostrophe:docker$ docker image ls
REPOSITORY          TAG                IMAGE ID            CREATED              SIZE
nginx-c             1                  6f5be529ea6d        About a minute ago   127MB
bruschetta          1                  11afb91367ae        2 hours ago          451MB
nginx               1.17.10            9beeba249f3e        8 hours ago          127MB
ubuntu              20.04              1d622ef86b13        3 weeks ago          73.9MB

Docker ネットワークの作成

Docker ネットワークの作成は docker network create コマンドを利用する。

takatoh@apostrophe:docker$ docker network create nginx-network

これで nginx-network という名前の Docker ネットワークができた。つぎのようにすると様子が見える。

takatoh@apostrophe:docker$ docker network ls
NETWORK ID           NAME               DRIVER              SCOPE
d0c23099f6be         bridge             bridge              local
492e64e5d79d         host               host                local
7f38bac647a0         nginx-network      bridge              local
b42cbd843632         none               null                local

nginx-network 当名前が見える。ほかにもあるけど、これらはデフォルトで用意されているものらしい。詳細は調べてない。

コンテナの起動

2つのコンテナを起動する。その際、--netowork オプションで接続する Docker ネットワークの名前(今回は nginx-network)を指定する。また、HTTP サーバのコンテナの方でアプリケーションサーバの名前を bruschetta-back と指定しているので、これを間違えないようにする。

まずはアプリケーションサーバのコンテナから。

takatoh@apostrophe:docker$ docker run --name bruschetta-back --network nginx-network -d bruschetta:1

HTTP サーバ。ポートの指定も忘れずに。

akatoh@apostrophe:docker$ docker run --name nginx-front --network nginx-network -p 8080:80 -d nginx-c:1

ホスト側から確認

起動したコンテナは bruschetta-d というホスト名で待ち受けているので、/etc/hosts ファイルに 127.0.0.1 を指すように記述を追加する。それができたら準備は完了だ。http://bruschetta-d:8080/ にブラウザでアクセスしてみる。

結果、期待通りにアプリケーションにアクセス、使用できることが確認できた。

参考ページ

新しいPCにUbuntu 20.04 LTSをインストール

メインに使っている PC を買い替えた。

先日ダウンロードしておいた Ubuntu 20.04 LTS 日本語 Remix を DVD に焼いて(この作業は別の Windows マシンでやった)、プリインストールされていた Windows 10 は起動すらせずに上書きインストールした。ホスト名は替える前と同じ apostrophe。

OS のインストールが終わったら、とりあえずすぐに使いそうなソフトウェアだけインストールした。

  • rbenv と Ruby (2.7.1)
  • pyenv と Python (3.8.2)
  • Git
  • Sublime text
  • Dropbox
  • Tweaks
  • Docker

Python はデフォルトで入っていた(コマンドとしては python3)けど、今後を考えて pyenv を使った。Tweaks というのは、参考にした web ページにあった設定ツール。デスクトップまわりの設定ができる。

Docker についてはインストール方法を書いておこう。最初は先日 16.04 にインストールしたのを参考に作業をしたんだけどダメだった。えぇっ?と思いながらググってみると、apt コマンドでインストールできるようになっていた。

takatoh@apostrophe:~$ sudo apt install -y docker.io
takatoh@apostrophe:~$ sudo systemctl start docker
takatoh@apostrophe:~$ sudo systemctl enable docker

ついでに docker-compose もインストール(今後使う予定なので)。

takatoh@apostrophe:~$ sudo apt install -y docker-compose

あとはいくつか設定をカスタマイズして、データをバックアップサーバからコピーしてきて、ひとまずは完了。のこりは必要に応じてやっていこう。

参考にしたページ:

Dockerfileを書いてDockerイメージを作る練習→解決編

先日、Dockerfile を書いてイメージをビルドしたものの、コンテナが期待通りに動いてくれない、という記事を書いた。今日はその解決編。

結論を先に書くと、問題は Dockerfile じゃなくて Python のアプリケーションサーバである uWSGI の設定ファイルにあるつぎの行だった。

daemonize = file:/usr/bruschetta/bruschetta.log

この行をつぎのように書き換えたところ、期待通りに動いてくれて web アプリにアクセス、使用できるようになった。

logger = file:/usr/bruschetta/bruschetta.log

これは、なんとか原因を見つけようとして試行錯誤しながら「docker uwsgi」で検索に引っかかったいくつかのページを見ている時に気づいた。それらのページに載っている uWSGI の設定ファイルには daemonize = ... という記述がない。この記述は uWSGI にデーモンモードで動作するように指示する記述だ。そして、Dockerfile の CMD で指定したコマンドはコンテナの中でプロセス ID 1 で動作する、というのをどこかで読んだ。

つまり、こういうことだ。

  1. コンテナが起動すると、CMD で指定されている uWSGI がプロセス ID 1 で実行される。
  2. uWSGI はデーモンモードで動作するように設定ファイルで指定されているので、デーモンをフォークして自分自身は死ぬ。
  3. プロセス ID 1 のプロセスが死ぬと、コンテナも終了する。

実際に試してみよう。これが書きなおした uWSGI の設定ファイル。

[uwsgi]
http = :5000
chdir = /usr/bruschetta
wsgi-file = /usr/bruschetta/manage.py
callable = app
master = true
pidfile = /usr/bruschetta/bruschetta.pid
logger = file:/usr/bruschetta/bruschetta.log

上述したように最後の daemonize の行を logger に書き換えてある。Dockerfile も載せておく。

FROM ubuntu:20.04

RUN apt update && apt install -y python3 python3-pip git
RUN pip3 install uwsgi
RUN git clone https://github.com/takatoh/Bruschetta.git /usr/bruschetta
RUN cd /usr/bruschetta && pip3 install -r requirements.txt
COPY files/bruschetta.ini /usr/bruschetta/
RUN cd /usr/bruschetta && python3 manage.py init_db

CMD [ "/usr/local/bin/uwsgi", "/usr/bruschetta/bruschetta.ini" ]

こちらは、書き方は先日とちょっと変わっているが、実質的に変更無し。

これでビルドしたイメージからコンテナを起動する。

takatoh@apostrophe $ docker run -it -d -p 8080:5000 --name bruschetta-5 bruschetta:5

ブラウザで localhost:8080 アクセスすると、期待どおり web アプリが動作しているのを確認できた。

ちなみに、起動中のコンテナの中に入ってプロセスを確認してみると:

takatoh@apostrophe $ docker exec -it bruschetta-5 bash
root@423460e65b10:/# ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/0    Ss+    0:01 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
    8 pts/0    S+     0:00 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
    9 pts/0    S+     0:00 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
   10 pts/1    Ss     0:00 bash
   21 pts/1    R+     0:00 ps ax

やっぱり、uWSGI がプロセス ID 1 で動いている。

教訓:コンテナで起動するプログラムはデーモンモードにするな