Docker / CI 设置

我手边磁盘上展开的 TeX Live 2024 有 8.7 GB。而 Docker Hub 上的 texlive/texlive:latest 压缩后约 2.5 GB,最小的 latest-minimal335 MB(均为 2026 年 8 月列出的 amd64 数值)。选择用 Docker 跑 LaTeX,首先就是关于这几个数字的讨论。本页讲的是 texlive/texlive 镜像本身——谁在构建它、里面装了什么、该选哪个标签、如何固定版本——以及容器特有的那个坑:产出的 PDF 归 root 所有。GitHub Actions 工作流怎么写,归 CI 那一页。

为什么要在容器里跑 TeX

理由只有一个:可重现性。同一份 .tex 能否产出同一份 PDF,并不只由稿件决定。它取决于装的是哪一年的 TeX Live、每个宏包是什么修订版、系统注册了哪些字体。Docker 镜像就是把那套环境连同操作系统一起冻起来、装箱发出去。箱子里是某个快照的 TeX Live 及其引擎、宏包、字体,所以无论你的主机是 Windows、macOS,还是 CI 的 runner,打开同一个箱子就得到同一个环境。「我这儿能编译,合作者和 CI 就不行」也就基本消失了。

容器和本地安装并不互斥。现实的分工是:日常写作用编辑器配本地 TeX Live,只把最终构建和发给合作者的那一份交给容器。另一个好处是可以不弄脏自己的环境就随便试:想弄清某个 \usepackage{...} 为何失败时,一个全新的 TeX Live 几分钟就有了。代价也有。至少要拉一次好几 GB 的镜像,而且容器里没有图形界面的阅读器,也没有 SyncTeX 反向搜索。诚实的说法是:容器是构建环境,不是编辑环境

texlive/texlive 镜像是谁做的,里面装了什么

做它的不是 TeX 用户组,而是一个叫 Island of TeX 的社区——它并不是 TeX Live 的官方镜像。同一个镜像既可以从 Docker Hub 以 texlive/texlive 获取,也可以从 GitLab 以 registry.gitlab.com/islandoftex/images/texlive 获取。底座是 Debian testing 的 slim 镜像,Dockerfile 里写明了为什么没选 Alpine:编写当时,biber 之类的二进制并未针对 Linux/musl 平台发布。里面装的也不只是 TeX Live 本体:为 arara 备了 Java,为 biberxindy 备了 Perl,为 minted 备了 Python 和 Pygments,为 EPS 转换备了 Ghostscript,甚至为 pgfplots 备了 gnuplot。

terminal
# One-off build of ./main.tex. The image's own WORKDIR is /workdir.
docker run --rm -v "$PWD":/workdir -w /workdir texlive/texlive \
  latexmk -pdf main.tex

# Poke around inside instead of building:
docker run --rm -it -v "$PWD":/workdir -w /workdir texlive/texlive bash

--rm 在退出后丢弃容器,-v "$PWD":/workdir 把当前位置绑到容器内的 /workdir-w 让它成为工作目录。输入与输出都在你自己的文件夹里,PDF 就落在原地。中文与日文用 LuaLaTeX 最省事,把命令换成 latexmk -lualatex main.tex 即可。若要走 upLaTeX 加 dvipdfmx 的路线,就把后文的 .latexmkrc 放在稿件旁边再调用 latexmk main.tex。另有一点要记住:可以在镜像内执行 tlmgr update --self --all 更新,但只要丢掉容器,一切又回到原样。

该拉哪个标签:scheme 与它们的实际大小

标签是两个维度的乘积:scheme(规模)是否包含文档和源码。scheme 有 minimalbasicsmallmediumfull 五档(算上给 ConTeXt 用的 context 则是六种),不带后缀的 latestlatest-full 的别名。在此之上再叠加 -doc(手册)、-src.dtx 源码)或 -doc-src(两者)。差别相当悬殊:Docker Hub 上 latest(即 full)约 2.5 GB,而 latest-doc-src 约 6.7 GB——仅仅加上文档就翻了一倍不止。考虑到光是 texmf-dist/doc 就有 3.7 GB,这也在情理之中。

标签内容Docker Hub 显示的大小(amd64,2026 年 8 月)
latestlatest-full 的别名:全部宏包,不含文档与源码约 2.53 GB —— 拿不定主意就用它
latest-mediummedium scheme;日常文档大多够用约 898 MB
latest-smallsmall scheme;想让 CI 轻一点时约 590 MB
latest-basicbasic scheme:基本的 LaTeX 再加一点点约 367 MB
latest-minimalminimal scheme:相当于 plain TeX,最小的一档约 335 MB
latest-doc-srcfull 加手册加源码;这里 texdoc 可用约 6.69 GB —— 适合在本地查资料
TL2018-historic过去的发行版;2013 年以后的都有用于重建旧稿件——参见下面的注意事项

实用的选法很简单:CI 里用不带后缀的 latest;如果 latest-medium 正好够用就用它。只有想在本地查 texdoc 时才取 -doc 系列。选小 scheme 时有一处要注意:按 Island of TeX 的 README,在 full 以外的镜像里,用 tlmgr install 装进来的新可执行文件不会自动加入 PATH,需要接着执行,例如 tlmgr install <pkg> && tlmgr path add。ARMv8(arm64)构建也有提供,但 README 明确说明它属于实验性质,所以在 Apple Silicon 上遇到怪现象时,值得试试 --platform linux/amd64

「historic 标签就不会变」是错的——连 digest 一起固定

即便是过去发行版的标签,它指向的镜像也会变。 Island of TeX 的 README 明说 historic 镜像「每月重建一次,底层操作系统镜像有更新时也会更新」。TeX Live 那边冻结了,Debian 那边的库却在动。此外 latest 每周迁移到新的 CTAN 快照,每次周构建都会带上 TL{发行版}-{年}-{月}-{日} 形式的标签(TL2021 之前的格式还含时和分)。所以要真正固定论文的最终构建,就写 digest 而不是标签——这是唯一可靠的办法。

terminal
# A tag can be re-pointed later; a digest cannot.
docker pull texlive/texlive:latest
docker image inspect --format '{{index .RepoDigests 0}}' texlive/texlive:latest
# -> texlive/texlive@sha256:0123abcd...

# Record that string in the repository and build against it forever:
docker run --rm -v "$PWD":/workdir -w /workdir \
  texlive/texlive@sha256:0123abcd... latexmk -pdf main.tex

# Belt and braces for archival work: keep your own copy of the image.
docker save texlive/texlive@sha256:0123abcd... | gzip > texlive-frozen.tar.gz

digest 是镜像内容本身的哈希,所以无论上游发生什么,你拉到的永远是同一份字节。对学位论文,或几年后可能被审稿人要求复现的工作,把这一行提交进仓库就是给未来的自己帮了大忙。而对不需要这种严格性的日常构建,latest 就够了。这里的判断标准不是「哪个都行」,而是这次构建是为了什么

产出的 PDF 归 root 所有的问题

在 Linux 上这是你最先撞上的问题。镜像的默认用户是 root——Island of TeX 的 README 直言默认用 root 是为了方便用 apt 补装软件包——于是写进绑定挂载目录的 main.pdfmain.aux 都归 root 所有,之后编辑器无法覆盖保存,git clean 也删不掉。macOS 和 Windows 上的 Docker Desktop 因为文件共享层会改写属主而看不出来,但在 Linux 和 CI 上这是家常便饭。解决办法是用 --user 把自己的 UID 和 GID 传进去。

terminal
# Run as the calling user, so the PDF belongs to you.
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD":/workdir -w /workdir \
  texlive/texlive latexmk -pdf main.tex

# If a package writes to $HOME (luaotfload caches, fontconfig), give it one:
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/workdir \
  -v "$PWD":/workdir -w /workdir \
  texlive/texlive latexmk -lualatex main.tex

传了 --user 之后,你会以一个没有主目录的用户身份运行,LuaTeX 或 fontconfig 想写缓存时可能会抱怨。这时像上面那样把 HOME 指向一个可写目录就行。另外,自 2025 年 2 月起镜像里还准备了一个名为 texlive 的非 root 用户,README 建议在自建的下游镜像中使用它——同时也补了一句:绑定挂载的权限可能需要调整。从根本上说,让宿主与容器使用同一个 UID 才是唯一的解。

在上面构建项目专用镜像

与其每次构建都跑 tlmgr install,不如一次性做好一个装齐所需内容的镜像并固定它,既快,也更容易查错。从 FROM texlive/texlive:latest-medium 开始,补上缺的宏包或字体即可。这里藏着一个有意思的机关:Island of TeX 的镜像用 equivs 造了一个名为 texlive-local虚构 Debian 包(版本号 9999.99999999)并标记为已安装。于是当你安装依赖 TeX 的 Debian 包时——比如 apt-get install pandoc——不会再被拖下来一整套 Debian 版的 TeX Live

terminal
# Dockerfile
FROM texlive/texlive:latest-medium

# Extra TeX packages. On a non-full scheme, "path add" is required
# for any package that installs a new executable.
RUN tlmgr install latexmk siunitx biblatex biber \
 && tlmgr path add

# Extra system packages: the image already claims texlive is installed,
# so this will not pull in Debian's own TeX Live.
RUN apt-get update \
 && apt-get install -y --no-install-recommends fonts-noto-cjk \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /workdir

docker build -t myproject-tex . 烤出来之后,往后只用这个标签即可。把 CJK 字体一起装进去,LuaLaTeX 加 luatexja-fontspec 的组合就能在箱子里自成一体。要把镜像交给合作者或团队,就推到内部 registry,或用 docker save 打包送出。走到这一步,最大的好处已不是省下多少兆字节,而是给合作者的说明只有一行,而不是「请自行安装 TeX Live」。

在 CI 里使用镜像:GitHub Actions 与 GitLab CI

在 GitLab CI 里只需写进作业的 image:;在 GitHub Actions 里则是直接指定容器,或使用 xu-cheng/latex-action。这里要先破除一个容易想当然的地方:xu-cheng/latex-action 用的并不是 texlive/texlive这个 action 拉的是同一作者的 xu-cheng/latex-docker 项目发布的 ghcr.io/xu-cheng/texlive-alpineghcr.io/xu-cheng/texlive-debian。这正是它的 os 输入默认为 alpine 的原因;texlive_version 可取 20202026,或 latest。简言之,同样叫「装了 TeX Live 的容器」,来路不同,里面可能完全是两回事

terminal
# .gitlab-ci.yml - the image goes straight into the job.
build:
  image: texlive/texlive:latest
  script:
    - latexmk -pdf main.tex
  artifacts:
    paths:
      - main.pdf

# GitHub Actions, run inside the same image instead of an action:
jobs:
  build:
    runs-on: ubuntu-latest
    container: texlive/texlive@sha256:0123abcd...
    steps:
      - uses: actions/checkout@v7
      - run: latexmk -pdf main.tex

把镜像写进 container: 的好处是,本地的 docker run 与 CI 字面意义上是同一个环境。改用 xu-cheng/latex-action 的好处则是写法更短,并能通过 latexmk_use_lualatexlatexmk_shell_escape 等输入做细致控制(其默认 latexmk 参数为 -pdf -file-line-error -halt-on-error -interaction=nonstopmode)。这是「与本地环境完全一致」和「写法简短」之间的取舍;若把可重现性放在首位,选前者。工作流的组织、缓存、产物的处理,属于 CI 那一页的地盘。

让稿件适合交给容器

在容器里翻车的稿件有一个共同点:构建步骤只存在于编辑器的设置里。根文件、引擎、辅助命令、输出位置——这四样一旦以文字形式存在于仓库中,本地、Docker、CI 都只需同一行 latexmk main.tex。若用 upLaTeX 加 dvipdfmx 排日文,请把 .latexmkrc 放在 main.tex 旁边,把路线写明。$pdf_mode = 3 的含义是「先生成 DVI,再用 dvipdfmx 转成 PDF」。

latex
# .latexmkrc: upLaTeX + dvipdfmx
$latex = "uplatex -interaction=nonstopmode -halt-on-error %O %S";
$bibtex = "upbibtex %O %B";
$dvipdf = "dvipdfmx %O -o %D %S";
$pdf_mode = 3;

把这份 .latexmkrc 提交上去,无论从 docker run、GitLab CI 还是 GitHub Actions 调用,都会按同一套规则生成 PDF。与此同时,请养成每当真的新增了 \usepackage 就回头检查镜像标签的习惯。scheme 还锁在 medium 却加了 tikz-cd,某天就会只有 CI 挂掉,报 ! LaTeX Error: File 'tikz-cd.sty' not found.。把稿件和环境一起纳入版本管理——用容器排版,说到底就是这样一个约定。