skip to content
camaleon's log Adapting's Blog
Image of a bottle pouring water into a fish tank shaped like a monitor, containing a blue whale

Using Docker to Compile LaTeX Documents

/ 8 min read

Docker: The LaTeX Lover’s Best Friend

Ever found yourself going down the rabbit hole of installing a multitude of dependencies for a tool you’re uncertain is the right one? Or even worse, struggling to ensure all the dependencies are correct for a tool you certainly need?

Well, fret not! We’ve been through that arduous journey and discovered the ultimate solution: Docker.

Context

Imagine you want to compile some PDFs using LaTeX locally. You’d typically follow the instructions for the tool (TeX Live) based on your operating system.

But what if you hit a snag? Like, say, with the new ARM architecture on macOS making some programs a headache to compile. What then?

Enter Docker, our knight in shining armor. The game plan is to build a Docker image with all the necessary dependencies and run our program inside a container using a volume and a few other tricks. This way, it works on any OS, consistently.

We started by scouting for existing solutions and stumbled upon a few:

The first one was a bit heavy and outdated. The second looked promising but wasn’t popular, focusing on CI/CD. The last one was just right—lightweight and maintained by pandoc, which adds a touch of class.

Here’s how I gave it a whirl:

$ docker run --rm -t pandoc/latex
^C%

NOTE: Including --rm is crucial to avoid unexpected errors. Once you’re comfortable with the command, you can consider removing --rm to reuse the same container.

I found myself hitting C-c to break off the process, just like an emergency stop button! Why, you ask? A program had sprung into action and was eagerly waiting for a stdin input - like a dog sits, wagging its tail, waiting for a “Great!“. The next step is to find the default command. For this, I used help as an argument.

$ docker run --rm -t pandoc/latex help
[WARNING] Could not deduce format from file extension
 Defaulting to markdown
pandoc: help: openBinaryFile: does not exist (No such file or directory)

Here’s a surprise - drumroll - guess which command pandoc was using as the default? Yep, you guessed it. It was using the pandoc command. I know, mind-blowing!

So, I’m going to remove the entrypoint next. And hey, bonus points if we already have pdflatex installed.

$ docker run --rm --entrypoint "" -t pandoc/latex /bin/sh -c "pdflatex --help"
Usage: pdftex [OPTION]... [TEXNAME[.tex]] [COMMANDS]
  or: pdftex [OPTION]... \FIRST-LINE
  or: pdftex [OPTION]... &FMT ARGS
 Run pdfTeX on TEXNAME, usually creating TEXNAME.pdf.
 ...

Great! Everything is running smoothly, similar to a well-maintained machine. We need to tighten a few screws before we can fully use it.

Cooking our Dockerfile

The tweaks were simple: create a Docker image without an entrypoint to freely use any binary inside the container, and install a LaTeX package called texliveonfly, which installs any missing packages on-the-fly during .tex file compilation.

After a few iterations, we ended up with this Docker image:

# Alpine
FROM pandoc/latex
ENV DEBIAN_FRONTEND noninteractive
ENV TERM=xterm-256color
WORKDIR /data

# texlive needs python
ENV PYTHONUNBUFFERED=1
RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
RUN python3 -m ensurepip && pip3 install --no-cache --upgrade pip setuptools

# extra packages
RUN tlmgr init-usertree && tlmgr install texliveonfly

# avoid zombie processes
# https://linuxconcept.com/reaping-a-zombie-inside-a-docker-container/
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

Here’s how I used it. First, I created a volume to the current directory ($PWD), but there’s a catch: files created will be owned by the container’s user, which is root by default:

$ docker run --rm -v $PWD:/data -t mmngreco/dex pdflatex test.tex > /dev/null
$ ls -lah
total 132K
drwxrwxr-x 2 mgreco mgreco 4.0K Jan 20 19:02 .
drwxrwxr-x 5 mgreco mgreco 4.0K Jan 18 20:43 ..
-rw-r--r-- 1 mgreco mgreco 1.0K Jan 20 19:04 test.aux
-rw-r--r-- 1 mgreco mgreco  16K Jan 20 19:04 test.log
-rw-r--r-- 1 mgreco mgreco  395 Jan 20 19:04 test.out
-rw-r--r-- 1 root   root    94K Jan 20 19:04 test.pdf
-rw-rw-r-- 1 mgreco mgreco 1.3K Jan 18 12:01 test.tex

To resolve the permission problem, we can provide our user and group id to Docker by using the --user parameter:

$ docker run --rm -t --user="$(id -u):$(id -g)" -v $PWD:/data mmngreco/dex
pdflatex test.tex > /dev/null
$ ls -lah
total 132K
drwxrwxr-x 2 mgreco mgreco 4.0K Jan 20 19:13 .
drwxrwxr-x 5 mgreco mgreco 4.0K Jan 18 20:43 ..
-rw-r--r-- 1 mgreco mgreco 1.0K Jan 20 19:13 test.aux
-rw-r--r-- 1 mgreco mgreco  16K Jan 20 19:13 test.log
-rw-r--r-- 1 mgreco mgreco  395 Jan 20 19:13 test.out
-rw-r--r-- 1 mgreco mgreco  94K Jan 20 19:13 test.pdf
-rw-rw-r-- 1 mgreco mgreco 1.3K Jan 18 12:01 test.tex

Now the PDF file has the correct permissions. We can also prevent Docker from creating a network by adding the --net none parameter:

$ docker run --rm -t --net none --user="$(id -u):$(id -g)" -v $PWD:/data
mmngreco/dex pdflatex test.tex > /dev/null
$ ls -lah
total 132K
drwxrwxr-x 2 mgreco mgreco 4.0K Jan 20 19:13 .
drwxrwxr-x 5 mgreco mgreco 4.0K Jan 18 20:43 ..
-rw-r--r-- 1 mgreco mgreco 1.0K Jan 20 19:13 test.aux
-rw-r--r-- 1 mgreco mgreco  16K Jan 20 19:13 test.log
-rw-r--r-- 1 mgreco mgreco  395 Jan 20 19:13 test.out
-rw-r--r-- 1 mgreco mgreco  94K Jan 20 19:13 test.pdf
-rw-rw-r-- 1 mgreco mgreco 1.3K Jan 18 12:01 test.tex

With our command ready, we need an easy way to use it without typing the whole thing every time we want to compile a LaTeX file. We can either create a shell alias or make an executable script that can be called like any other system command.

Creating an Alias

These days, bash and zsh are probably the most popular shells, but the process should be similar in others.

Depending on your shell, you’d modify .bashrc or .zshrc and add the following:

alias dex='docker run --rm -t --net none --user="$(id -u):$(id -g)" -v $PWD:/data mmngreco/dex'

After restarting the shell, you can use the alias:

$ dex pdflatex --help
...

The downside is that this alias is user-specific, not global. So, a program like VSCode won’t recognize dex unless it’s in the system path.

Creating an Executable

This is the most generic and recommended solution. Create a bash script with the command, add parameters, give it execution permissions, and move it to a system-accessible path like ~/.local/bin/.

Create a file named dex with the iterated command, appending $@ for user-provided parameters:

 #!/usr/bin/env bash
 docker run \
   -t \
   --rm \
   --net none \
   --user="$(id -u):$(id -g)" \
   -v $PWD:/data  \
   mmngreco/dex $@

Give it execution permissions and move it to the PATH:

# execution permissions
sudo chmod +x dex
# move to path
mv dex ~/.local/bin/

If done correctly, our script should work:

$ dex pdflatex --help
...

Live Preview

At this stage, we have a docker image that is fully equipped to compile .tex files. However, we can improve this by automating it from our preferred text editor (neovim).

The end goal is to modify the .tex file and have the editor compile it for us, displaying the resulting PDF.

In the following examples, we are using the dex executable only, as it’s general enough for the purpose of this section.

VSCode

In VSCode, we achieved this using the [LaTeX-Workshop extension][tex-ext]. Then, add the following user configuration to settings.json (Ctrl+Shift+P and search for “settings json”):

Include this in the JSON:

     "latex-workshop.latex.outDir": "./",
     "latex-workshop.view.pdf.viewer": "tab",
     "latex-workshop.synctex.afterBuild.enabled": true,
     "latex-workshop.latex.recipes": [
         {
           "name": "dex",
           "tools": [
             "pdflatex"
           ]
         },
       ],
     "latex-workshop.latex.tools": [
         {
           "name": "pdflatex",
           "command": "dex",
           "args": [
             "pdflatex",
             "%DOC%",
             "-output-directory=%OUTDIR%"

           ],
           "env": {"PWD": "%OUTDIR%"}
         },
         {
           "name": "bibtex",
           "command": "dex",
           "args": [
             "bibtex",
             "%DOCFILE%"
           ],
           "env": {"PWD": "%OUTDIR%"}
         }
     ],

After that, you should be able to use the extension and see a PDF preview in a new tab.

Vim/Neovim

For Neovim users such as myself, here’s a setup you can utilize. Feel free to adjust it according to your needs. Please note, this is just a small sample.

" vimscript
augroup Latex
 au!
 au BufWritePost *.tex silent !dex pdflatex % && firefox %:t:r.pdf
augroup end

Or in Lua:

-- lua
vim.cmd[[
augroup Latex
 au!
 au BufWritePost *.tex silent !dex pdflatex % && firefox %:t:r.pdf
augroup end
]]

Final Thoughts

Take note, this solution is like a swiss army knife, it can work for almost every Command Line Interface (CLI) program out there. It’s a versatile cross-platform solution you can rely on.

In this context, we are wrapping LaTeX, but truth be told, this approach can be applied to almost any python program too; be it an app, a service, or a CLI. The beauty of it all is that the underlying principles remain the same.

Based on your goals, you may need to invest more time into security or be a little more mindful of the final size. It’s like cooking, you always adjust the spices to your taste, right?