Skip to content

The runtime image

In core the runtime image is built from source in your project, so the whole toolchain — Pandoc, TeX Live, the fonts — is yours to read and change. make image builds it; make publish runs your book against it. In core-slim the same image arrives prebuilt and signed; there, this page is background.

How it's assembled

The image starts from Ubuntu and Pandoc's static binary, with no TeX Live. Keystone then installs its own TeX Live: a pinned install-tl lays down the infrastructure, and tlmgr adds exactly the packages Keystone needs. The package set is small, and each TeX Live stage installs from a declaration in .docker/packages/tlmgr/.

The Dockerfile builds in stages so the build toolchain never reaches the finished image:

  • base — the general LaTeX / XeLaTeX packages (the engine, math, fonts support, hyperref, and the babel/hyphenation languages an author can select with lang:).
  • keystone-tex — the packages Keystone's own features pull in (callout boxes, drop caps, watermarks, page furniture, and so on).
  • fonts — installs the font packages and extracts only the files Keystone embeds, with each family's license alongside.
  • pandoc — fetches the pinned Pandoc release and checks it against its checksum.
  • pandoc-lint — fetches the pinned lint release and checks it against its checksum.
  • the final image copies Pandoc, the finished TeX Live tree, fonts and pandoc-lint onto a clean Ubuntu with only the packages the engine runs. The build toolchain it needed (gnupg, wget, the full Perl) is left behind, so the image is smaller and tlmgr no longer runs in it.

Pandoc's binary is statically linked, so a Lua filter can load only pure-Lua modules; one that requires a C module stops the build.

Attribution

The image carries its own NOTICE.md under /usr/share/doc/keystone/, naming what it bundles and where to read each license. Redistributing the image carries that attribution with it, independent of any project — which is what the copy in your own project covers.

Adding a LaTeX package

When a custom handler's macros.tex needs a LaTeX package the image doesn't ship, add a row to .docker/packages/tlmgr/keystone-tex.packages — the package name, and an optional description after it — then rebuild:

make image      # reinstall TeX Live with the new package
make publish    # build your book against it

The image redistributes the package under its own license, so it needs an entry in NOTICE.md.

A name that isn't a real package fails the build with package <name> not present in repository, so a typo surfaces immediately.

Inspecting a TeX Live package

tlmgr runs in the base stage rather than the finished image, pinned to the same snapshot. From the directory holding .docker/, open a container off it, then ask tlmgr inside:

docker build --target base -t keystone-base:local -f .docker/Dockerfile .
docker run --rm -it keystone-base:local bash
tlmgr install --with-doc <package>
tlmgr info --list <package>

--with-doc because the stage installs without doc files, and a license is one; without it, tlmgr info --list stops naming the license. Nothing ships from that container.

Reproducibility

TeX Live is installed from a frozen daily snapshot, pinned by the TL_SNAPSHOT value in the Dockerfile, so the same source rebuilds the same package versions over time. Pandoc and pandoc-lint are pinned to a release by each tarball's checksum, and a tarball that no longer matches fails make image. The handful of system packages underneath still install from Ubuntu's current archive. This is the build half of the reproducibility the sandbox gives the run.

A snapshot can serve an incomplete set, and tlmgr reports success when it does. The build stops instead, naming the packages that did not arrive: retry once the snapshot is whole, or move TL_SNAPSHOT to a date that carries them.

The install-tl download is verified against a vendored TeX Live signing key; a signature failure after a snapshot move usually means the key rotated.

The packages are verified against that same key, and tlmgr reports the check without acting on it. The build stops instead: a snapshot whose keyring tlmgr cannot use is another reason to move TL_SNAPSHOT to a newer date.

Moving the pins

A pin stays where it is until you decide to move it, since a move changes output that has to be checked again. After one, run make image and make publish, then compare the book with the last build.

Images

BASE_IMAGE and the yq stage's image are pinned by digest; the tag beside each is for the reader. Canonical rebuilds Ubuntu with security fixes, and taking one means setting the digest the tag resolves to now:

docker buildx imagetools inspect <image>:<tag> --format '{{.Manifest.Digest}}'

TeX Live

TL_SNAPSHOT fixes the TeX Live year as well. Within a year the packages gain only small fixes, so move it for a newer year or for a fix you need:

  1. Pick the latest snapshot for that year from https://texlive.info/tlnet-archive/<year>/.
  2. Set TL_SNAPSHOT=<year>/<month>/<day> in the Dockerfile.
  3. Rebuild and compare.
  4. Re-read the font licenses. A font or license file the snapshot renamed or moved stops the build, naming the path. Inspecting a TeX Live package finds where it went; correct the line naming it in .docker/fonts/licenses.copy.txt or .pandoc/filters/lib/font-registry.lua. Terms that changed under the same filename are not caught.

The signing key

TUG renews its signing subkey, and an installer signed with the new one fails to verify. gpg --show-keys .docker/texlive/texlive-pubkey.asc shows the vendored subkey's expiry, so the refresh can come first. make refresh-texlive-key re-fetches the key by its pinned fingerprint and rewrites that file; commit the result.

Pandoc and pandoc-lint

Each architecture has its own pin — PANDOC_AMD64 and PANDOC_ARM64, PANDOC_LINT_AMD64 and PANDOC_LINT_ARM64 — in the form <tag>@sha256:<hex>: the release's tag and the checksum of that architecture's tarball. Both pins of a release name the same tag, and the # renovate: comment above each names the release's repository. A # provenance: build line above that comment marks a release whose upstream also attests how each tarball was built.

With gh signed in:

  1. Set both pins. gh release view <tag> --repo <repository> --json assets lists each tarball's name and digest.
  2. Verify each tarball came from the release:

    gh release download <tag> --repo <repository> --pattern <tarball>
    gh release verify-asset <tag> <tarball> --repo <repository>
    

    Where the pin declares build provenance, gh attestation verify <tarball> --repo <repository> checks that too.

  3. Rebuild and compare.

Languages

The image bundles the standard European babel/hyphenation languages, so lang: selects any of them with hyphenation. Cyrillic text (Ukrainian, Russian) renders as long as the book uses a font that covers it — the built-in DejaVu and Linux Libertine families do (see Typography & fonts). Those languages aren't in the bundled set, so they typeset without language-specific hyphenation; adding it is a deeper change (the pattern packages must load in the base stage before the format is built).