【Docker超入門】DockerでつくるLaTeX環境

本稿に関して動作がおかしいところや不明な点，ツッコミなどがあれば適宜コメントいただけるとありがたいです．

## はじめに
大学の研究会で講義を1コマ持つことになったため，弊研究会では必須なLaTeX環境をDockerで作る方法を解説することにしました．

読者としてはDockerの知識が一切ない人を想定しています．

なお，本稿ではLaTeX自体の使い方に関してはあまり触れません．

## LaTeXとは?
LateXとは，Donald E. Knuth氏が開発したレイアウトシステムのTexをLeslie Lamport氏が文書作成のために改良したものだそうです．主に学術論文を書く際に使用されており，数式を綺麗にレイアウトしてくれます．

例えば，世界一美しいとされるオイラーの等式でさえ，テキストで表現すると美しくないですが，

`e^{iπ} + 1 = 0`

TeXを用いてコンパイルすれば，以下のように美しくレイアウトされます．

$e^{iπ} + 1 = 0$


## Dockerとは?
Dockerとはコンテナ仮想化技術のプラットフォームです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420158/4c3bb0de-b0a2-0a0e-36ce-8bc4e8d37669.png)

従来の仮想化技術では，ホストOS上の仮想ハードウェアや，Hypervisorを利用してゲストOSを動かすことで環境を隔離していたのに対し，DockerではDocker EngineがホストOSのカーネルの利用を上手く管理することでプロセスとユーザを隔離しているため，VMに比べて軽量な仮想化環境を実現しています．

## 事前準備
本稿では，このDockerを用いてLaTeXのディストリビューションであるTeXLiveの環境を行うため，まずはDockerと生成されたPDFの閲覧用にSkimをインストールします，
### Dockerのインストール
下記ページからDockerHubのアカウントを作成しdmgをダウンロードしてインストールします．
MAC=> https://docs.docker.com/docker-for-mac/install/
Windows=> https://docs.docker.com/docker-for-mac/install/

### Skim(軽量PDFビューワー)のインストール
下記ページよりダウンロードしてインストール
https://skim-app.sourceforge.io/

## 全体像を把握する
今回は下記のような構成でDockerを用いてLaTeX環境を構築します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420158/7bff4299-6432-56b6-268c-1aa6aff6f77e.png)


## とりあえずDockerでLaTex環境を建ててみる
DockerとSkimのインストールが完了したら，筆者が事前に用意したTeXLiveのDockerイメージ(nontan18/texlive)を用いて以下のコマンドでLaTeX環境を実際に構築してみましょう．

```shell
// 作業用のworkディレクトリを作成し移動
$ mkdir work && cd work
```

`work`ディレクトリに入ったら下記の内容のファイルを作成します．

```sample.tex
\documentclass[twocolumn, a4j]{article}
\usepackage{multirow}
\usepackage{amsmath,amssymb}
\usepackage[T1]{fontenc}

\title{サンプル用のTexファイル}
\renewcommand{\thefootnote}{\fnsymbol{footnote}}
\author{Nozomu Miyamoto\footnotemark[2]　nontan@sfc.wide.ad.jp}
\renewcommand{\thefootnote}{\arabic{footnote}}
\date{\today}

\begin{document}

\twocolumn[
\begin{@twocolumnfalse}
  \maketitle
  \vspace{-6mm}
  \begin{abstract}
    ここに概要を書きましょう．
  \end{abstract}
  \vspace{2mm}
\end{@twocolumnfalse}
]

\renewcommand{\thefootnote}{\fnsymbol{footnote}}
\footnotetext[2]{慶應義塾大学 村井研}
\renewcommand{\thefootnote}{\arabic{footnote}}

\section{はじめに}

  　hoge hoge hoge hoge

\renewcommand{\refname}{参考文献}
\begin{thebibliography}{数字}
  \bibitem[opt]{key} 文献情報
\end{thebibliography}

\end{document}

```


```shell
// 生成したdocument.texをdocument.pdfにコンパイル
$ docker run -v $(pwd):/root/work -it nontan18/texlive:stable latexmk --pvc ./sample.tex
```

これでworkディレクトリ内にsample.pdfが出力されますので，SkimなどのPDFビューワーを使って表示してみましょう．


表示できたら，お好きなエディタを用いて，sample.texを編集してみます．

```shell
$ vim sample.tex
```
編集が完了すると自動でコンパルが始まり，document.pdfが更新されるかと思います．

## Dockerの基本的な使い方
このセクションでは上記のDockerコマンドで一体何が起きているのかを説明します．
### docker run
`docker run`はDockerイメージを元にDockerコンテナを起動するコマンドです．
`run`コマンドの第1引数で指定したコマンドが起動したコンテナ内で実行されます．

上記の

```shell
$ docker run -v $(pwd):/root/work -it nontan18/texlive:1.0.0 latexmk --pvc ./sample.tex
```

においては`latexmk --pvc ./sample.tex`コマンドが実行されています．
(`latexmk --pvc ./sample.tex`コマンドは任意のTexファイルを監視して更新されるたびに自動コンパイルするコマンドです．)

`-t`オプションでDockerイメージを参照することができます．
上記の例ではDockerHubに登録されている`nontan18/texlive`というイメージ(より詳細には，その中でも1.0.0タグがついているイメージ)を参照して，それを元にDockerコンテナを作成しています．
(ちなみに，このDockerHubのようなDockerImageが登録されている場所をDockerRegistryと呼びます．)

`-v`オプションはボリュームのマウントオプションで，DockerホストのボリュームをDokcerコンテナ内の任意の場所にマウントすることができます．上記の例では，texの作業用に作った
(-v オプションでは相対リンクでボリュームを指定できないので，`pwd`コマンドで現在のディレクトリを取得してそれを引数に取っています．)

### docker ps
`docker run`コマンドで動いているDockerコンテナは`docker ps`コマンドで確認することができます．


```shell
$ docker ps
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                               NAMES
1349e83ba710        nontan18/texlive:1.0.0            "latexmk --pvc ./sam…"   5 seconds ago       Up 3 seconds                                            gallant_galileo
```

### docker exec
また，`docker exec -it [CONTAINER ID] [COMMAND]`コマンドを使うと，起動中のコンテナ内でコマンドを実行することができます．

```shell
// $CONTAINER_IDはdocker psで取得したコンテナのIDに変更(example:1349e83ba710)
$ docker exec -it $CONTEAINER_ID sh
// ここからコンテナ内
~/work# ls
sample.aux  sample.dvi  sample.fdb_latexmk  sample.fls  sample.log  sample.pdf  sample.synctex.gz  sample.tex
// TeXLiveの絵文字パッケージが入っているか調べる
~/work# tlmgr search fontawesome
// コンテナ内にTeXLiveの絵文字パッケージをインストール
~/work# tlmgr install fontawesome
//...省略
tlmgr: package repository http://mirror.utexas.edu/ctan/systems/texlive/tlnet (verified)
[1/1, ??:??/??:??] install: fontawesome [498k]
running mktexlsr ...
done running mktexlsr.
running updmap-sys ...
done running updmap-sys.
tlmgr: package log updated: /usr/local/texlive/2019/texmf-var/web2c/tlmgr.log
// コンテナから出る
~/work# exit
exit
// ホストのシェルに戻った
$
```

上記の例では先程建てたコンテナ内でshコマンドを実行して，ターミナルからコンテナ内に入り，TeXLiveの絵文字パッケージ(fontawsome)が入っていないことを確認して，TeXLiveパッケージマネージャー(tlmgr)を用いて絵文字パッケージをインストールしました．

### docker commit(※あんま使わない)
ただ，これだけではDockerコンテナ内での変更でしか無いため，一度Dockerコンテナを削除し，`docker run`コマンドで再度コンテナを建てると，絵文字パッケージはインストールされていない状態に戻ってしまいます．

このDockerコンテナの変更をDockerイメージに反映させるためには`docker commit [CONTAINER ID] [IMAGE NAME]`コマンドを用います．

```shell
$ docker commit 1349 nontan18/texlive:stable
sha256:a64dcff68e35d56082c52817f9432b9a2c1142d32a631f461474188912207341
```

これでローカルのイメージに変更が反映され，新しいイメージのSHA256ハッシュ値が出力されます．

### docker images
ローカルのDockerイメージは`docker images`コマンドで確認することができ，以下の用に，変更したDockerイメージ（nontan18/texlive)の`IMAGE ID`が先程のハッシュ値になっていることが確認できます．

```shell
$ docker images
REPOSITORY                                  TAG                 IMAGE ID            CREATED             SIZE
nontan18/texlive                            stable              a64dcff68e35        3 minutes ago       1.48GB

```

実際に，`docker run -it [IMAGE_NAME] [COMMAND]`コマンドを用いて，更新したDockerイメージからDockerコンテナを作成すると，今度は絵文字パッケージ(fontawesome)がはじめからインストールされていることがわかります．

```shell
// コンテナを立てるとともにshを起動してコンテナ内に入る
$ docker run -it nontan18/texlive:stable sh
// fontawesomeがインストールされているか調べる
~/work# tlmgr search fontawesome
fontawesome - Font containing web-related icons
~/work# exit
exit
```

※ただ，上記の方法でイメージを更新すると，イメージの中身が不透明になりメンテナンスが難しくなるので`docker commit`コマンドの仕様はあまり推奨しません．

そこで次にDockerイメージの元となるDockerfileを変更する方法を解説します．

### Dockerfile
DockerfileはDockerイメージの作り方が書かれたファイルです．
今回使っているnontan18/texliveのDockerfileはgithubに公開されているので確認してみましょう．

```shell
// githubからローカルにレポジトリをクローン
$ git clone https://github.com/nontan18/texlive.git
// クローンしてきたディレクトリに移動
$ cd texlive
// ディレクトリ内のファイル一覧を出力
$ ls
Dockerfile  README.md  app  docker-compose.yml  sample
// Dockerfileの中身を出力
$ cat Dockerfile
```

今回使ったDockerイメージ(nontan18/texlive)の元となった以下のようなDockerfileが出力されるかと思います．

```Dockerfile
# Alpine Linux v3.9.4をベースのイメージとして指定
FROM alpine:3.9.4

# MAINTAINER nontan <nontan@sfc.wide.ad.jp>
LABEL maintainer="nontan@sfc.wide.ad.jp"

WORKDIR /root/work

# パッケージリストの更新
RUN apk update
# TeXLiveのインストールに必要なパッケージを取得
RUN apk add perl wget fontconfig-dev
# TeXLiveのインストーラーの解凍に必要なパッケージのインストール
RUN apk add xz tar

# 圧縮されたTeXLiveのインストーラーをダウンロードし，tmpディレクトリに保存
ADD http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz /tmp/install-tl-unx.tar.gz
# インストーラーを解凍した際に配置するディレクトリを作成
RUN mkdir /tmp/install-tl-unx
# ダウンロードしたinstall-tl-unx.tar.gzを解凍
RUN tar -xvf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl-unx --strip-components=1
# TeXLiveのインストール用の設定ファイルを作成
RUN echo "selected_scheme scheme-basic" >> /tmp/install-tl-unx/texlive.profile
# TeXLiveのインストール
RUN /tmp/install-tl-unx/install-tl -profile /tmp/install-tl-unx/texlive.profile
# TeXLiveのバージョンを取得しインストールディレクトリを特定し，latestの名称でシンボリックリンクを作成
RUN TEX_LIVE_VERSION=$(/tmp/install-tl-unx/install-tl --version | tail -n +2 | awk '{print $5}'); \
    ln -s "/usr/local/texlive/${TEX_LIVE_VERSION}" /usr/local/texlive/latest
# インストールしたTeXLiveへパスを通す
ENV PATH="/usr/local/texlive/latest/bin/x86_64-linuxmusl:${PATH}"

# TeXLive Package Managerを使用して必要なパッケージをインストール
# texファイルの自動コンパイルパッケージをインストール
RUN tlmgr install latexmk
# latexmkの設定ファイルをホストからイメージにコピー
COPY ./app/config/.latexmkrc /root/.latexmkrc
# 2カラムの設定に必要なパッケージのインストール
RUN tlmgr install multirow
# 日本語対応パッケージのインストール
RUN tlmgr install collection-langjapanese
# フォントパッケージのインストール
RUN tlmgr install collection-fontsrecommended
RUN tlmgr install collection-fontutils

# 不要なパッケージなどの削除(イメージの容量削減のため)
RUN apk del xz tar
RUN rm -rf /var/cache/apk/*
RUN rm -rf /tmp/*


# References
# - https://github.com/blang/latex-docker
# - https://github.com/Paperist/docker-alpine-texlive-ja/blob/master/Dockerfile
```

ここで，先程のように`fontawesome`をインストールしておくには，`RUN tlmgr install fontawesome`を追加します．

```
# フォントパッケージのインストール
RUN tlmgr install collection-fontsrecommended
RUN tlmgr install collection-fontutils
# 下の行を追加
RUN tlmgr install fontawesome
```


### docker build
DockerfileからDockerイメージをビルドするには，`docker build -t [IMAGE NAME] .`コマンドを用います．

```shell
$ docker build -t mylatexlive . 
```

Dockerイメージのビルドが終わったら，`docker images`コマンドでDockerイメージが追加されているのがわかります．

```shell
$ docker images
REPOSITORY                                  TAG                 IMAGE ID            CREATED             SIZE
mylatexlive                                 latest              0bbb5bed8ce1        7 minutes ago       1.45GB
```

当然，このDockerイメージからDockerコンテナを作成すると，fontawesomeのパッケージがインストールされていることがわかります．

```shell
// コンテナを立てるとともにshを起動してコンテナ内に入る
$ docker run -it mylatexlive sh
// fontawesomeがインストールされているか調べる
~/work# tlmgr search fontawesome
fontawesome - Font containing web-related icons
~/work# exit
exit
```

## docker-composeの基本的な使い方
ここまででDockerの基本的な使い方を学びましたが，実際にDockerを用いて環境構築を行おうと考えると，複数のコンテナで様々なDockerイメージやオプションを追加しなければならなく非効率です．

そこで，複数のコンテナを効率的に管理するためにdokcer-composeを紹介します．

### docker-compose.yml
docker-composeは`docker-compose.yml`に記述したDockerコンテナの構成を`docker-compose up`コマンドや`docker-compose down`コマンドなどで手軽に展開できるツールです．

ここでは，上の過程で`git clone`した`docker-compose.yml`を見てみましょう．

```docker-compose.yml
version: "3.1"
services:
  texlive:
    build: .
    # image: nontan18/texlive
    volumes:
      - ./sample:/root/work
    command: latexmk --pvc /root/work/sample.tex

  nginx:
    # /root/public以下のファイルをホスティングするnginxベースのイメージ
    image: nontan18/stable-file-host
    ports:
      # "ホストのポート:コンテナのポート"
      - "8080:80"
    volumes:
      # 生成されるsample.pdfを公開ディレクトリにマウント
      - ./sample:/root/public

```

上記の`docker-compose.yml`ではtexliveコンテナでコンパイルされたsample.pdfファイルが入っているsampleディレクトリがnginxコンテナの`/root/public/`にマウントされるようになっています．nginxコンテナのイメージである`nontan18/stable-file-host`は`/root/public/`以下のファイルをホスティングしているため，nginxコンテナの80番ポートにアクセスすることができれば，ブラウザ上で`sample.pdf`を閲覧することができます．

ここでは`ports`でホストの8080番ポートをnginxコンテナ内の80番ポートに転送しているので，このコンテナがたつと，ブラウザから`http://localhost:8080/sample.pdf`にアクセスすることで出力されたPDFを閲覧することが可能になります．


### docker-compose up
上記の`docker-compose.yml`のあるディレクトリで`docker-compose up`コマンドを実行すると下記のようにdocker-compose.ymlに記述されたすべてのコンテナが起動します．

```shell
$ docker-compose up
Starting texlive_nginx_1 ...
Starting texlive_nginx_1 ... done
Attaching to texlive_texlive_1, texlive_nginx_1
```

上記の通り，`http://localhost:8080/sample.pdf`にアクセスすると出力されたPDFが確認できるはずです．

### docker-compose ps
`docker-compose up`した状態で`docker-compose ps`コマンドを実行すると，docker-compose.ymlで定義された各コンテナが確認できます．

```shell
$ docker-compose ps
      Name                     Command               State          Ports
---------------------------------------------------------------------------------
texlive_nginx_1     nginx -g daemon off;             Up      0.0.0.0:8080->80/tcp
texlive_texlive_1   latexmk --pvc /root/work/s ...   Up

```

### docker-compose down
`docker-compose up`をした状態で`Ctrl + C`で各コンテナを停止させることもできますが，別のターミナルのセッションから`docker-compose down`を用いることで起動したすべてのDockerコンテナを停止することができます．

```shell
$ docker-compose down
```

`docker-compose down`には，マウントしたDockerボリューム(本稿では解説していませんが，ホストの特定ディレクトリをマウントする以外のコンテナのデータを永続化させる方法)をすべて削除する`-v`オプションや使用しているイメージを削除する`--rmi`オプションなどが存在します．

MySQL+Django+Angularのようなサーバークライアントモデルの開発環境を構築する際には，このdocker-composeは非常に便利です．

## おわりに
最後に，今回，無造作に建てたDockerコンテナとイメージを削除してクリーンな状態に戻しましょう．
`docker container rm [CONTAINER ID]`コマンドや`docker image rm [IMAGE NAME]`コマンドでもDockerコンテナやDockerイメージの削除は可能ですが，ここでは`docker container prune`と`docker image prune`を紹介します．

### docker container prune
`docker container prune`コマンドは停止しているDockerコンテナをすべて削除するコマンドです．
`docker ps`コマンドで起動中のコンテナを確認し，不必要なコンテナを`docker stop [CONTAINER ID]`で停止させたのち，`docker container prune`コマンドを実行しましょう．

```shell
$ docker container prune
```

### docker image prune
`docker container prune`と同様に`docker image prune`は起動中のコンテナに使用されていないDockerイメージを全て削除します．(なぜだかは知りませんが削除するイメージの数が多いとだいぶ長く時間がかかることがあります)

```shell
$ docker image prune
```

これで本稿で作成したコンテナもイメージも綺麗さっぱりホストから削除することができました．

従来の仮想化技術より軽量なコンテナ仮想化技術は，複雑な環境ごとパッケージ化したアプリケーションを気軽に展開したり削除したりできて非常に便利です．

本稿で紹介したのはDockerコマンドの一部に過ぎず，DockerはCIツールやクラスターにコンテナによるオーケストレーションを展開するkubernetes,アプリケーションコンテナの依存関係を整理しパッケージ管理するHelm,Docker環境にkubernetesクラスターを瞬時に展開しHelmを用いてアプリケーションを展開するRancherなどDockerの世界は非常に広大です．

本稿が皆様の`Hello Docker World`のきっかけになれば幸いです．

## 注意事項
上記では何の躊躇もせずに筆者がビルドしたDockerイメージを使用していますが，信頼できない他人の作ったDockerイメージを安易に使用するのには注意が必要です．
たとえDockerfileが公開されていたとしても，Dockerレジストリに公開されているDockerイメージが悪意の無いものとは限りません．
Dockerfileを精査したのち，自身で`docker build`を行って使用することを推奨します．
特にホストのボリュームをマウントする場合や，`--privileged`オプションを必要とするものには注意しましょう．

## 参考
- https://www.atmarkit.co.jp/ait/articles/0903/12/news120.html
- https://github.com/blang/latex-docker
- https://github.com/Paperist/docker-alpine-texlive-ja/blob/master/Dockerfile