The TeX Live 2024 unpacked on the disk beside me is 8.7 GB. The texlive/texlive:latest image on Docker Hub is about 2.5 GB compressed, and the latest-minimal tag about 335 MB (amd64 figures as listed in August 2026). Choosing to run LaTeX in Docker is, before anything else, a conversation about those numbers. This page is about the texlive/texlive image itself — who builds it, what is inside, which tag to take, and how to pin a version — plus the trap that is unique to containers: the PDF that comes out owned by root. How to write the GitHub Actions workflow belongs to the CI page.
Why run TeX in a container at all
There is one reason: reproducibility. Whether the same .tex yields the same PDF is not decided by the manuscript alone. It depends on which year of TeX Live is installed, on the revision of each individual package, and on which fonts are registered. A Docker image is that environment frozen together with its operating system and shipped as a box. Inside sits one snapshot of TeX Live with its engines, packages and fonts, so whether your host is Windows or macOS or a CI runner, opening the same box gives you the same environment. That is why “builds on my machine, fails on my co-author's and in CI” largely stops happening.
Containers and a local install are not mutually exclusive. The realistic split is to write day to day in your editor against a local TeX Live and hand only the final build and the copy you send to co-authors to a container. Another benefit is being able to experiment without dirtying your own setup: when you want to isolate why a \usepackage{...} fails, a pristine TeX Live is a few minutes away. There is a price, though. You have to pull several gigabytes at least once, and inside a container there is no GUI viewer and no SyncTeX reverse search. The honest framing is that a container is a build environment, not an editing environment.
Who builds the texlive/texlive image, and what is inside it
It is not built by the TeX Users Group but by a community called Island of TeX — it is not an official TeX Live image. The same image is available both from Docker Hub as texlive/texlive and from GitLab as registry.gitlab.com/islandoftex/images/texlive. The base is a slim Debian testing image, and the Dockerfile records why Alpine was not chosen: at the time it was written, binaries such as biber were not distributed for the Linux/musl platform. What is inside is more than TeX Live itself. The image bundles Java for arara, Perl for biber and xindy, Python with Pygments for minted, Ghostscript for EPS conversion, and even gnuplot for pgfplots.
# 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 throws the container away when it exits, -v "$PWD":/workdir binds your current location to /workdir inside the container, and -w makes that the working directory. Input and output both live in your own folder, so the PDF simply appears there. For Japanese, LuaLaTeX is the shortest route — just swap in latexmk -lualatex main.tex. For the upLaTeX and dvipdfmx route, put the .latexmkrc shown later beside the manuscript and call latexmk main.tex. One thing to remember: you can run tlmgr update --self --all inside the image, but the moment you discard the container it reverts.
Which tag to pull: schemes and their real sizes
Tags are the product of two axes: the scheme (how much TeX Live) and whether docs and sources are included. The scheme has five steps — minimal, basic, small, medium, full — or six if you count context for ConTeXt users, and the bare latest is an alias for latest-full. On top of that you add -doc (manuals), -src (the .dtx sources) or -doc-src (both). The effect is dramatic: Docker Hub lists latest (= full) at about 2.5 GB and latest-doc-src at about 6.7 GB — adding the documentation more than doubles the image. Given that texmf-dist/doc alone runs to 3.7 GB, that is only to be expected.
| Tag | What you get | Size as listed on Docker Hub (amd64, Aug 2026) |
|---|---|---|
latest | Alias for latest-full: every package, no docs or sources | about 2.53 GB — the sane default |
latest-medium | The medium scheme; enough for most everyday documents | about 898 MB |
latest-small | The small scheme; when you want a light CI job | about 590 MB |
latest-basic | The basic scheme: plain LaTeX plus a little | about 367 MB |
latest-minimal | The minimal scheme: essentially plain TeX, the smallest there is | about 335 MB |
latest-doc-src | full plus manuals plus sources; texdoc works here | about 6.69 GB — for looking things up locally |
TL2018-historic | A past release; images exist from 2013 onwards | For rebuilding an old manuscript — see the caveat below |
The practical choice is simple. In CI, take the bare latest, or latest-medium if it happens to carry what you need. Reach for a -doc tag only when you want texdoc locally. One caveat comes with the smaller schemes: according to the Island of TeX README, in any image other than full, a new executable installed with tlmgr install is not added to PATH automatically. You have to follow up, as in tlmgr install <pkg> && tlmgr path add. ARMv8 (arm64) builds are published as well, but the README explicitly calls them experimental, so if something behaves oddly on Apple Silicon, --platform linux/amd64 is worth a try.
A historic tag is not immutable: pin the digest as well
Even a past-release tag points at an image that changes. The Island of TeX README says plainly of the historic images that they are rebuilt every month and updated whenever the underlying operating-system image has updates. TeX Live may be frozen, but the Debian libraries underneath are not. On top of that, latest moves to a new CTAN snapshot every week, and each of those weekly builds carries a tag of the form TL{release}-{year}-{month}-{day} (before TL2021 the format also included the hour and minute). So if you truly want to freeze a paper's final build, write a digest rather than a tag — that is the only reliable way.
# 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.gzA digest is a hash of the image content itself, so whatever happens upstream, you can only ever pull the same bits. For a thesis, or for work a reviewer might ask you to reproduce years later, committing that one line to the repository is a favour to your future self. For everyday builds where that rigour is unnecessary, latest is fine. The criterion here is not “either will do” but what the build is for.
The PDF comes out owned by root
On Linux this is the first thing you hit. The image's default user is root — the Island of TeX README states outright that root is the default so that you can easily add packages with apt — so main.pdf and main.aux written into a bind-mounted directory come out owned by root, after which your editor cannot save over them and git clean cannot remove them. Docker Desktop on macOS and Windows hides this because its file-sharing layer rewrites ownership, but on Linux and in CI you meet it constantly. The fix is to pass your own UID and GID with --user.
# 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.texPassing --user then makes you a user with no home directory, and LuaTeX or fontconfig may complain when they try to write a cache. Pointing HOME at a writable directory, as above, is enough. Since February 2025 the image also ships a non-root user named texlive, and the README recommends it for downstream images — while adding, in the same breath, that bind-mount permissions may need adjusting. Fundamentally, using the same UID on the host and in the container is the only real fix.
Building a project-specific image on top
Rather than running tlmgr install on every build, it is faster — and much easier to debug — to build one image containing what you need and pin it. Start from FROM texlive/texlive:latest-medium and add the missing packages or fonts. There is a nice trick hidden here. The Island of TeX image builds a fictitious Debian package called texlive-local with equivs, at version 9999.99999999, and marks it installed. Because of that, installing a Debian package that depends on TeX — apt-get install pandoc, say — does not drag in a second, Debian-packaged TeX Live.
# 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 /workdirBuild it with docker build -t myproject-tex . and from then on you only use that tag. Baking in the CJK fonts means the LuaLaTeX plus luatexja-fontspec combination is self-contained inside the box. To hand the image to co-authors or a team, push it to an internal registry or freeze it with docker save. At this point the real payoff is not the megabytes: it is that the instruction to a collaborator is one line long instead of “install TeX Live yourself”.
Using the image from CI: GitHub Actions and GitLab CI
On GitLab CI you name the image in the job's image:; on GitHub Actions you either specify a container directly or use xu-cheng/latex-action. One assumption is worth killing here: xu-cheng/latex-action does not use texlive/texlive. The action pulls ghcr.io/xu-cheng/texlive-alpine or ghcr.io/xu-cheng/texlive-debian, published by the same author's xu-cheng/latex-docker project. That is why its os input defaults to alpine; its texlive_version input accepts 2020 through 2026, or latest. In short, two things both described as “a container with TeX Live” can be quite different inside.
# .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.texWriting the image into container: has the advantage that your local docker run and CI are literally the same environment. Using xu-cheng/latex-action instead is shorter and gives you fine-grained inputs such as latexmk_use_lualatex and latexmk_shell_escape (its default latexmk arguments are -pdf -file-line-error -halt-on-error -interaction=nonstopmode). The choice is a trade-off between identity with your local setup and brevity; if reproducibility comes first, take the former. Assembling the workflow, caching, and handling artifacts are the CI page's territory.
Making a manuscript container-ready
Manuscripts that fail inside a container have one thing in common: the build procedure exists only in an editor setting. Root file, engine, helper commands, output location — once those four exist as text inside the repository, the same single line latexmk main.tex works locally, in Docker and in CI. If you use upLaTeX and dvipdfmx for Japanese, put a .latexmkrc beside main.tex and state the route explicitly. $pdf_mode = 3 is the instruction “produce a DVI, then turn it into PDF with dvipdfmx”.
# .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;Commit that .latexmkrc and the PDF is built under the same rules whether it is called from docker run, GitLab CI or GitHub Actions. Alongside it, form the habit of revisiting the image tag whenever you actually add a \usepackage. Add tikz-cd while the scheme is still pinned to medium and one day only CI falls over, with ! LaTeX Error: File 'tikz-cd.sty' not found. Version the manuscript and the environment together — in the end, that is what typesetting in a container is a promise to do.