🐈

LaTeX環境をDockerで構築

2021/02/18に公開

https://github.com/overleaf/overleaf

LaTeX環境をDockerで構築

先人たちが作ってくれた環境を組み合わせたり若干修正したりまとめたりしたものです。

https://github.com/wsuzume/docker-alpine-texlive

本記事では Docker についてまったく知らない、という方のために Docker についての基本的な解説や簡単なチュートリアルも行っています。というかそれがほぼすべてなので Docker わかる人は本記事を読む必要はありません。GitHub のリポジトリの README や Dockerfile を読んだほうが早いです。

tl;dr

以下のコマンドで arXiv 風のサンプルがコンパイルされます。

$ git clone https://github.com/wsuzume/docker-alpine-texlive
$ cd docker-alpine-texlive

$ make pull
# or
$ make build

$ make sample

他のテンプレートは templates ディレクトリに入っているので、使いたいものを workdir にコピーして Makefile を編集してください。

あとはご自分で好きなように書き換えてください。書き換え方がわからない人は諦めて真面目に続きを読んでください。

参考

Docker とは?

Docker とはコンテナ型仮想環境の構築ツールです。ライブラリやパスといった依存関係を切り離してコンテナという単位に閉じ込めてくれるタイプの仮想環境で、カーネルはホスト OS と共有なので非常に高速に動作します。コンテナの元となるイメージの構築はスクリプトで管理されているので、ちょっとした変更を加えて、気に入らなかったら修正して、といったトライ&エラーが容易です。

LaTeX はローカルにインストールすると環境を汚しがちなので、仮想環境に閉じ込めて利用したい、というわけです。

Dockerのインストール

Docker がサポートしているプラットフォームは公式ドキュメント Docker docs の Supported platforms を参照してください。

インストール方法は公式ドキュメントを読んでもいいですが、わかりやすい記事を書いてくださっている方もたくさんいらっしゃいます。以下は、手元の環境でインストールするときに参考にした記事です。

Linux(Ubuntu 20.04 LTS)

Mac

Windows

Windows の場合は GitMake をインストールして PowerShell から呼び出せるようにパスを通す必要があります。

Docker の仕組み概説

OS によって細かい違いはあるのですが、通常、すべてのプログラムはプロセスと呼ばれる単位で管理されています(OS 自体もひとつ以上のプロセスから成ります)。プロセスには OS によって CPU, メモリ, ディスク, ネットワークといったリソースが割り当てられています。

Docker はプロセスに割り当てられたリソースを OS から分離してコンテナ化します。プロセスである以上、OS のカーネルは共有していますが、分離したディスクリソース上に他の OS のファイルシステムを構築することが可能です。Docker コンテナ内からはコンテナにインストールされたライブラリやソフトウェアにアクセスしにいくので、コンテナではあたかも別の OS が動作しているかのように見えます。これが Docker による仮想化です。CPU や OS の挙動をエミュレートするわけではなく、ホスト OS の環境と干渉しないようにソフトやライブラリをインストールしただけなので、とても高速に動作します。

通常、Docker コンテナ内からホスト OS 上のリソースにアクセスすることはできませんが、コンテナ起動時に適切なオプションを指定し、ホストのリソースの一部をマウントすることでアクセス可能になります。

今回は、Docker コンテナに Alpine という軽量 Linux ディストリビューションをインストールし、その上に LaTeX をインストールします。LaTeX のコンパイルを行うときは、このコンテナのシェルにアクセスしてコンパイルを実行するか、コンテナ起動時にコンパイルコマンドを実行させるかのどちらかになります。

Docker の基本概説

Docker コンテナは Docker イメージを元に作成(docker createコマンド)されます。Docker イメージは Dockerfile というインストールスクリプトを元にビルド(docker buildコマンド)するか、Docker Hub からプルします(docker pullコマンド)。

Docker イメージに対する操作はdocker imageコマンド、Docker コンテナに対する操作はdocker containerコマンドにまとめられています(たとえばdocker image lsdocker container lsはそれぞれイメージの一覧、コンテナの一覧を表示します)。

Docker コンテナには起動状態と停止状態があり、docker createコマンドで作成したコンテナは停止しています。docker startコマンドでコンテナを起動でき、docker stopコマンドはコンテナを停止させます。docker runコマンドは新しいコンテナを作成した上で起動します(create + startの操作)。

起動状態の Docker コンテナにはdocker attachまたはdocker execコマンドによりアクセスできます。docker attachコマンドは起動中のコンテナの PID が 1 のプロセスの標準出力にアクセスし、docker execコマンドは起動中のコンテナで新たにプロセスを起動して指定したコマンドを実行します。

Docker イメージを作成するときに行われた変更はそのイメージを元に作成されたすべての Docker コンテナに引き継がれますが、Docker コンテナ上で行われた変更が Docker イメージに影響を与えることはありません。また個々の Docker コンテナは独立しており、ひとつの Docker コンテナ上で行われた変更が他の Docker コンテナに影響を与えることは基本的にはありません(例外は設定ファイル等をコンテナ間で共有していた場合などです)。

Docker チュートリアル

ここでは Dockerfile を書いて Docker イメージをビルドし、コンテナを作成して色々試すチュートリアルを行います。LaTeX 環境を自分なりにカスタマイズしたいときに必要となる知識ですので、一度はやり通しておくことをオススメします。所要時間は 1 時間くらいです。

1. 親ディレクトリを作成する

今回は docker_tutorial という名前にします。

$ mkdir docker_tutorial

2. Dockerfileを作成する

親ディレクトリの中に Dockerfile を作成してください。

$ cd docker_tutorial
$ touch Dockerfile

Dockerfile の中身は以下のように記述してください。

Dockerfile
FROM ubuntu:18.04

3. Docker イメージをビルドする

親ディレクトリ内で以下のコマンドを実行してください。

$ docker image build -f Dockerfile -t tutorial:latest .

コマンドの構造としては

$ docker image build -f [イメージの元になる Dockerfile の名前] -t [イメージの名前:イメージのバージョン] [実行するコンテキストのパス]

です。

このコマンドによって Dockerfile が読み取られ、FROM が指定した通り、ubuntu:18.04のイメージがダウンロードされ、tutorial:latest としてビルドされます。以下のコマンドを実行することで作成されたイメージを一覧できます。

$ docker image ls
output
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
tutorial     latest    c090eaba6b94   3 weeks ago   63.3MB
ubuntu       18.04     c090eaba6b94   3 weeks ago   63.3MB

Docker はイメージを効率よく作成するためにイメージを段階的に構築して各ステージをキャッシュしているので、tutorial:latest のもととなった ubuntu:18.04 のイメージも表示されるはずです。

4. Docker コンテナを作成してアクセスする

以下のコマンドを実行することで、先ほどビルドした tutorial:latest イメージのコンテナを作成し、そのシェルにアクセスすることができます(exitコマンドで抜けることができます)。

$ docker container run -it --rm tutorial:latest bash

コマンドの構造は

$ docker container run [オプション] [イメージ名] [実行するコマンド]

です。-itオプションは-i(コンテナの標準入力へのアタッチ)と-t(疑似ターミナルの割当)の組み合わせで、あたかもターミナルに接続したかのように振る舞わせるおまじないです。--rmはコンテナ停止時に自動的にそのコンテナを削除するオプションです。コンテナを使い捨てる場合はつけておくとよいでしょう。他のオプションについてはドキュメントを参照してください。

実行するコマンドは bash などシェルを指定しておけばシェルにアクセスできますし、ls のようにコンテナから使えるコマンドであればなんでも実行できます。最終的にはここに platexdvipdfmx を指定すれば、LaTeX のコンパイルなどを実行してくれるというわけです。

docker runコマンドはひとつのコマンドしか実行できません。複数のコマンドを実行したい場合は、シェルスクリプトを書いてそれを呼び出させるか、以下のように bash にワンライナーを実行させてください。

$ docker container run -it --rm tutorial:latest bash -c "ls | grep lib"

シェルスクリプトを書いて呼び出させる方法は、このチュートリアルの最後にできるようになります。

コンテナのシェルにアクセスしているときと、ホスト OS のシェルにいるときで区別がつきにくいので、今後は

host:% docker image ls
docker:% ls | grep lib

のように、どちらで実行しているのかを先頭に明記します。

コンテナに python をインストールしてみる

コンテナのシェルで python3 を実行すると、command not found になると思います。

docker:% python3 -V
bash: python3: command not found

これは ubuntu のイメージに python がインストールされていないからです。以下のコマンドを入力して、python をインストールしてみてください。

docker:% apt-get update
docker:% apt-get install --no-install-recommends -y python3 python3-pip

再び python3 を実行すると今度は python が使えるはずです。

docker:% python3 -V
Python 3.6.9

このコンテナを抜けて再びシェルにアクセスすると、また python が使えなくなっているはずです。docker run --rm は起動時に新たにコンテナを作成し、停止時にそのコンテナを削除しているからです。環境はコンテナごとに独立で、もととなったイメージや他のコンテナに影響を与えることはありません。

docker:% exit
host:% docker container run -it --rm tutorial:latest bash
docker:% python3 -V
bash: python3: command not found

コンテナ上で行った変更を残しておきたければ、--rm オプションを指定せずにコンテナを残しておき、再び docker container start などでそのコンテナを起動すればよいです。

host:% docker container run -it tutorial:latest bash
docker:% [諸々の作業]
docker:% exit
host:% docker container ls -a
CONTAINER ID   IMAGE             COMMAND   CREATED          STATUS                     PORTS     NAMES
79768b140ebb   tutorial:latest   "bash"    13 seconds ago   Exited (0) 9 seconds ago             nervous_mccarthy
host:% docker container start -i 79768
docker:% python3 -V
Python 3.6.9

docker container ls は起動中のコンテナを一覧します。docker container ls -a は停止中のコンテナも含めて一覧します。今回、停止していたコンテナのIDは 79768b140ebb だったので、そのコンテナIDを指定して起動しています(IDは途中まで入力すれば先頭が合致するコンテナを選択してくれます)。

いらなくなったコンテナは docker container rm で削除できます。docker container prune は停止中のコンテナをすべて削除します。

host:% docker container rm 79768
# または
host:% docker container prune

host:% docker container ls -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

コンテナ上で行った作業は他のコンテナやホスト OS に対して影響を与えないので、好きなだけいじくって遊んで破壊しまくってください。

5. python がインストールされたイメージを作成する

Docker コンテナは基本的に使い捨てが想定されているので、一度作ったものを後生大事に取っておくことは基本的にありません。コンテナの中で python を使いたければ、まずはイメージにインストールしておいて、そのイメージからコンテナを作ります。

Dockerfile を以下のように編集してください。

Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install --no-install-recommends -y \
         python3 python3-pip \
    && apt-get clean

RUN はイメージのデフォルトのシェル上で指定されたコマンドを実行します。いまは python をインストールするスクリプトを指定しています。編集したら再びイメージをビルドしてください。

host:% docker image build -f Dockerfile -t tutorial:latest .

そしてコンテナのシェルに入ると、今度は最初から python が使えるはずです。

host:% docker container run -it --rm tutorial:latest bash
docker:% python3 -V
Python 3.6.9

6. ホスト OS のディレクトリを Docker コンテナにマウントする

Docker コンテナはホスト OS のリソースから隔離されているため、コンテナの中からホスト OS のファイルやディレクトリにアクセスすることは基本的にはできません。

コンテナからホスト OS のファイルやディレクトリにアクセスするには主に次の2つの方法があります。

  1. イメージのビルド時にホスト OS のファイルやディレクトリをイメージ内にコピーする
  2. コンテナの起動時にホスト OS のディレクトリをコンテナにマウントする

ひとつ目の方法はコピーしたファイルを変更したい場合、再びイメージをビルドしなければならないので、今回の用途には適しません。よってふたつ目の方法を紹介します。

まずは共有したいディレクトリを作成して、その中にディレクトリが共有されているか確かめるためのファイルを入れておきます。

host:% mkdir workdir
host:% touch hello.py

hello.py は以下のように編集してください。

hello.py
print('Hello from Docker!!')

コンテナの起動時にホスト OS のディレクトリをコンテナにマウントするには、docker container run コマンドに -v オプションで指定します。書式は

-v [ホストOSのディレクトリ]:[コンテナのマウント先のディレクトリ]

です。

host:% docker container run -it --rm -v ${PWD}/workdir:/workdir tutorial:latest bash
docker:% ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  workdir

ls の出力に workdir が新たに作成されていることが確認できます(-v オプションをつけなかった場合と比較してみてください)。workdir に入ると hello.py が確認でき、これはコンテナの python で実行可能です。

docker:% cd workdir
docker:% ls
hello.py
docker:% python3 hello.py
Hello from Docker!!

!!注意!!

現在、コンテナは ubuntu の root ユーザーで起動しているはずなので、コンテナがホスト OS に対して行う操作はすべて root 権限になっています。workdir 内で touch コマンド等で作成したファイルやディレクトリはすべて root ユーザーの所有となります。

docker:% touch hoge
docker:% exit
host:% cd workdir
host:% ls -l
-rw-rw-r-- 1 host host 29  218 00:02 hello.py
-rw-r--r-- 1 root root  0  218 00:11 hoge

同様にして、マウントしたディレクトリに対してはあらゆる操作を root 権限で実行できてしまうので、不用意に重要なディレクトリをマウントしないよう注意してください。通常はイメージをビルドするときに、必要な作業が終わったら root 権限を剥奪したユーザーに移行し、この問題を回避します。

7. コンテナ起動時のワーキングディレクトリを指定する

先ほどは cd workdir を実行してマウントした workdir へと移動していましたが、起動時のカレントディレクトリは -w オプションで指定することができます。書式は

-w [起動時のカレントディレクトリ]

です。

host:% docker container run -it --rm -v ${PWD}/workdir:/workdir -w /workdir tutorial:latest bash
docker:% pwd
/workdir
docker:% ls
hello.py

ここまで解説した内容の集大成として、コンテナの起動と同時に hello.py のスクリプトを実行するコマンドを以下に示します。

host:% docker container run -it --rm -v ${PWD}/workdir:/workdir -w /workdir tutorial:latest bash -c "python3 hello.py"
Hello from Docker!!

練習問題として、何らかのシェルスクリプト(たとえば "Marvelous Shell Script!!" と表示する、など)を作成し、コンテナの起動時にそれを実行してみてください。

以上でチュートリアルは終了です。あとは python を LaTeX に、hello.pymain.tex に置き換えれば Docker 上に LaTeX 環境が構築できるという寸法です。

CMake で作業を自動化する

チュートリアルをやっているうちに、怠惰な人は Makefile を書いてコマンドを簡略化していたかもしれません。

Makefile
SOURCE=Dockerfile
IMAGE=tutorial:latest

# build container image
.PHONY: build
build: Dockerfile
	docker image build -f ${SOURCE} -t ${IMAGE} .

# create new container and login to the shell
.PHONY: shell
shell:
	docker container run -it --rm \
		-v ${PWD}/workdir:/workdir \
		-w /workdir \
		${IMAGE} \
		bash

# create new container and execute hello.py
.PHONY: hello
hello:
	docker container run -it --rm \
		-v ${PWD}/workdir:/workdir \
		-w /workdir \
		${IMAGE} \
		bash -c "python3 hello.py"

# clean up all stopped containers
.PHONY: clean
clean:
	docker container prune
  • make build: イメージのビルド
  • make shell: コンテナのシェルへのログイン
  • make hello: hello.py の実行
  • make clean: 停止したコンテナをすべて削除

LaTeX 環境の構築

ここまで理解できていれば、直接リポジトリのDockerfileMakefile を読みに行けると思います。

https://github.com/wsuzume/docker-alpine-texlive

Ubuntu の代わりに軽量の Alpine Linux に glibc がインストールされたものをベースイメージに用い、texlive 2020 をインストールしています。LaTeX のパッケージを追加する例として疑似コードを書くための jlisting, algorithms, algorithmicxの3つを追加しています。

root ユーザーのままだといろいろ気持ち悪い(作成されたファイルの所有者が root になったりする)ので、最後に一般ユーザーに降格しています。

パッケージを追加したあとに mktexlsr を実行すべきなのですが、イメージのビルド時にどう実行しても、コンテナ内からは package not found と言われてしまうので、仕方なしにコンテナ起動時に最初に mktexlsr を実行させることで問題を回避しています。make shell でコンテナに入った場合は、platex などの実行前に mktexlsr を実行することを忘れないでください。ビルド時に mktexlsr をやる方法がありましたら、どなたかご教示いただけますと幸いです。

ともかく、

$ git clone https://github.com/wsuzume/docker-alpine-texlive
$ cd docker-alpine-texlive
$ make build

で LaTeX 環境をビルドすることができます。ときどき遅いミラーサーバーが選択されると、接続がタイムアウトしてビルドがコケることがありますので、そのときは Ctrl+C で強制終了して再びビルドしてください。

手元の環境でビルドに成功したものが Docker Hub に push してあるので、make build の代わりに

$ make pull

を実行しても大丈夫です。

LaTeX 環境の使用方法

templates ディレクトリにいくつかテンプレートが用意してありますので、その中の main.pdf を見て気に入ったものを workdir にコピーしてください。

workdir 内には最初から sample というディレクトリがあって、これは templates/preprint_en_single_column という arXiv 風のテンプレートをコピーしたものです。samplemake sample コマンドによってコンパイルすることができますので、自分で新しくテンプレートをコピーした場合は、Makefile 内の sample コマンドを参考にご自身で必要な設定を書き換えてください。

おしまい!!

Discussion