├── .dockerignore ├── .editorconfig ├── .github ├── scripts │ ├── _shared_functions.sh │ ├── assemble-release-notes.sh │ ├── build-image.sh │ ├── build-installer-image.sh │ ├── make-release-tag.sh │ ├── run-example.sh │ └── update-dockerhub-info.sh ├── trigger-release.sh └── workflows │ ├── test-and-publish.yml │ ├── trigger-release.yml │ └── update-dockerhub-info.yml ├── .gitignore ├── .hadolint.yaml ├── .noai ├── Dockerfile ├── LICENSE ├── README.md ├── demo ├── .gitignore └── static-document-server │ ├── .dockerignore │ ├── Dockerfile │ ├── Texlivefile │ ├── create_many_documents.sh │ ├── hello_world.tex │ └── serve.sh ├── entrypoint.sh ├── examples ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── Texlivefile ├── _custom-build-script.sh ├── _example_setup.sh ├── complex-build.sh ├── custom-image.sh ├── hello_world.tex ├── interactive-build.sh ├── one-off-build.sh └── repeated-build.sh ├── profiles ├── base-luatex.profile ├── base.profile └── minimal.profile └── renovate.json5 /.dockerignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.idea/ 3 | /examples/ 4 | /LICENSE 5 | .editorconfig 6 | *.md 7 | *.log 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/scripts/_shared_functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function dockerhub_repo { 4 | prefix="${DOCKERHUB_USER_NAME:-}${DOCKERHUB_USER_NAME:+/}" 5 | echo "${prefix}texlive-${1:-minimal}" 6 | } 7 | 8 | # Log curl response and confirm success 9 | CURL_OUT="curl_out" 10 | function process_curl_response { 11 | cat "${CURL_OUT}" 12 | status_code=$(grep -e '^HTTP/' "${CURL_OUT}" | awk '{ print $2 }') 13 | [[ ${status_code} =~ 2[0-9]{2} ]] 14 | return $? 15 | } 16 | 17 | function print_curl_response_json { 18 | # We need to cut off headers; search for first opening brace 19 | sed '/^{/,$!d' "${CURL_OUT}" 20 | echo "" 21 | } 22 | -------------------------------------------------------------------------------- /.github/scripts/assemble-release-notes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | image_tag_list="${1}" 6 | 7 | # TODO: Include tag annotation? 8 | # TODO: include some form of changelog? 9 | 10 | echo -e "### Published images\n" 11 | for tag in $(grep -ve ':latest$' "${image_tag_list}"); do 12 | echo " - [${tag}](https://hub.docker.com/r/${tag%%:*})" 13 | done 14 | -------------------------------------------------------------------------------- /.github/scripts/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${0}")/_shared_functions.sh" 4 | 5 | set -eu 6 | 7 | installer_image=${TEXLIVE_INSTALLER_IMAGE:-'texlive-installer:latest'} 8 | image_tag_list="${IMAGE_TAG_LIST:-/dev/stdout}" 9 | 10 | # NB: Can't seem to resolve mirrors.ctan.org from within 'docker build', so do it here: 11 | ctan_mirror="$(curl -Ls -o /dev/null -w '%{url_effective}' https://mirrors.ctan.org)" 12 | echo "Will use CTAN mirror ${ctan_mirror}" 13 | 14 | ref="${1}" 15 | profile="${2:-minimal}" 16 | commit_time="$(date --date="@$(git show -s --format=%ct HEAD)" --rfc-3339=seconds)" 17 | # Use commit time for (more) reproducible builds; format is required by OCI annotation spec. 18 | 19 | tlversion="$(docker run --rm "${installer_image}" | head -n 1 | awk '{print $5 }')" 20 | 21 | function make_docker_tag { 22 | echo "$(dockerhub_repo "${1}"):${2}" 23 | } 24 | 25 | case "${ref}" in 26 | refs/tags/release-* ) 27 | version="${ref##*/release-}" 28 | 29 | # Confirm that release tag and to-be-installed version match 30 | if [[ ${tlversion} -ne ${version%.*} ]]; then 31 | echo "TeXlive version mismatch: Installer version ${tlversion} vs Git tag ${ref##*/}" 32 | exit 77 33 | fi 34 | 35 | docker build \ 36 | --cache-from "${installer_image}" \ 37 | --tag "$(make_docker_tag "${profile}" "${version}")" \ 38 | --tag "$(make_docker_tag "${profile}" latest)" \ 39 | --build-arg "ctan_mirror=${ctan_mirror}" \ 40 | --build-arg "profile=${profile}" \ 41 | --build-arg "label_created=${commit_time}" \ 42 | --build-arg "label_version=${version}" \ 43 | --build-arg "label_tlversion=${tlversion}" \ 44 | --build-arg "label_revision=$(git rev-parse --verify HEAD)" \ 45 | . 46 | make_docker_tag "${profile}" "${version}" >> "${image_tag_list}" 47 | make_docker_tag "${profile}" latest >> "${image_tag_list}" 48 | ;; 49 | refs/tags/pre-* ) 50 | version="${ref##*/pre-}" 51 | docker build \ 52 | --cache-from "${installer_image}" \ 53 | --tag "$(make_docker_tag "${profile}" "${version}")" \ 54 | --build-arg "ctan_mirror=${ctan_mirror}" \ 55 | --build-arg "profile=${profile}" \ 56 | --build-arg "label_created=${commit_time}" \ 57 | --build-arg "label_version=${version}" \ 58 | --build-arg "label_tlversion=${tlversion}" \ 59 | --build-arg "label_revision=$(git rev-parse --verify HEAD)" \ 60 | . 61 | # shellcheck disable=SC2086 62 | make_docker_tag "${profile}" "${version}" >> ${image_tag_list} 63 | ;; 64 | * ) 65 | version="${ref##*/}" 66 | # This is testing, just build the thing. 67 | docker build \ 68 | --cache-from "${installer_image}" \ 69 | --tag "$(make_docker_tag "${profile}" "${version}")" \ 70 | --build-arg "ctan_mirror=${ctan_mirror}" \ 71 | --build-arg "profile=${profile}" \ 72 | . 73 | make_docker_tag "${profile}" "${version}" >> "${image_tag_list}" 74 | ;; 75 | esac 76 | -------------------------------------------------------------------------------- /.github/scripts/build-installer-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | installer_image=${TEXLIVE_INSTALLER_IMAGE:-'texlive-installer:latest'} 6 | 7 | # NB: Can't seem to resolve mirrors.ctan.org from within 'docker build', so do it here: 8 | ctan_mirror="$(curl -Ls -o /dev/null -w '%{url_effective}' https://mirrors.ctan.org)" 9 | echo "Will use CTAN mirror ${ctan_mirror}" 10 | 11 | docker build --no-cache \ 12 | --build-arg "ctan_mirror=${ctan_mirror}" \ 13 | --target "texlive-installer" \ 14 | --tag "${installer_image}" \ 15 | . 16 | 17 | docker run --rm "${installer_image}" 18 | -------------------------------------------------------------------------------- /.github/scripts/make-release-tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${0}")/_shared_functions.sh" 4 | 5 | installer_image=${TEXLIVE_INSTALLER_IMAGE:-'texlive-installer:latest'} 6 | 7 | # Get the TeXlive installer and extract the major version 8 | tlversion="$(docker run --rm "${installer_image}" | head -n 1 | awk '{ print $5 }')" 9 | echo "Determined TeXlive version: ${tlversion}" 10 | 11 | # Check if this version was released before 12 | set -o pipefail 13 | last_minor_version="$(git tag | grep "release-${tlversion}." | sed -e "s/release-${tlversion}\.//" | sort -rn | head -n 1)" 14 | 15 | # shellcheck disable=SC2181 # we need the output 16 | if [[ $? -eq 0 ]]; then 17 | echo "Determined last minor version: ${last_minor_version}" 18 | # Increment "minor version" 19 | next_version="${tlversion}.$((last_minor_version + 1))" 20 | else 21 | # No tag for this TeXlive version yet, start over 22 | next_version="${tlversion}.1" 23 | fi 24 | echo "Will release version: ${next_version}" 25 | 26 | # POST a new tag via Github API 27 | current_commit="$(git show-ref master --hash | head -n 1)" 28 | new_tag="release-${next_version}" 29 | echo "Will try to create tag ${new_tag} on ${GITHUB_REPOSITORY}:${current_commit}" 30 | 31 | # First, create the tag _object_ (for the annotation) 32 | # cf. https://developer.github.com/v3/git/tags/#create-a-tag-object 33 | curl -siX POST "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/tags" \ 34 | -H "Authorization: token ${GITHUB_TOKEN}" \ 35 | -o "${CURL_OUT}" \ 36 | -d @- \ 37 | << PAYLOAD 38 | { 39 | "tag": "${new_tag}", 40 | "message": "Scheduled re-release.\n", 41 | "object": "${current_commit}", 42 | "type": "commit", 43 | "tagger": { 44 | "name": "Raphael Reitzig", 45 | "email": "4246780+reitzig@users.noreply.github.com", 46 | "date": "$(date --iso-8601=seconds)" 47 | } 48 | } 49 | 50 | PAYLOAD 51 | 52 | process_curl_response || exit 1 53 | 54 | # Now, create the tag _reference_ (to have an actual Git tag) 55 | # cf. https://developer.github.com/v3/git/refs/#create-a-reference 56 | curl -siX POST "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/refs" \ 57 | -H "Authorization: token ${GITHUB_TOKEN}" \ 58 | -o "${CURL_OUT}" \ 59 | -d @- \ 60 | << PAYLOAD 61 | { 62 | "ref": "refs/tags/${new_tag}", 63 | "sha": "${current_commit}" 64 | } 65 | PAYLOAD 66 | 67 | process_curl_response || exit 1 68 | -------------------------------------------------------------------------------- /.github/scripts/run-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | image=${1} 6 | example=${2} 7 | 8 | cd examples 9 | ./"${example}.sh" "${image}" 10 | [[ -f hello_world.log ]] && [[ -f hello_world.pdf ]] 11 | rm -f hello_world.log hello_world.pdf 12 | -------------------------------------------------------------------------------- /.github/scripts/update-dockerhub-info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${0}")/_shared_functions.sh" 4 | 5 | # TODO: It would be nicer to use the original action: 6 | # https://github.com/peter-evans/dockerhub-description/blob/master/entrypoint.sh 7 | # But since we can't yet iterate over steps, well... 8 | # Also, we want to set more than just the full description 9 | 10 | profile="${1:-minimal}" 11 | 12 | set -euo pipefail 13 | IFS=$'\n\t' 14 | 15 | # Prepare description values 16 | dh_repo=$(dockerhub_repo "${profile}") 17 | 18 | readme_filepath=${README_FILEPATH:="./README.md"} 19 | readme_profile_filepath="${readme_filepath}_${profile}" 20 | 21 | # Patch README so it 22 | # - links to Github project and license, 23 | # - mentions the profile, and 24 | # - resolves relative links to Github. 25 | cat > "${readme_profile_filepath}" << BADGES 26 | [![Dockerfile](https://img.shields.io/badge/-Dockerfile%20-blue)](https://github.com/reitzig/texlive-docker/blob/master/Dockerfile) 27 | [![GitHub issues](https://img.shields.io/github/issues/reitzig/texlive-docker)](https://github.com/reitzig/texlive-docker/issues) 28 | [![GitHub license](https://img.shields.io/github/license/reitzig/texlive-docker)](https://github.com/reitzig/texlive-docker/blob/master/LICENSE) 29 | 30 | BADGES 31 | 32 | sed \ 33 | -e "s/\(# TeXlive Docker Image\)s/\1 (${profile})/" \ 34 | -e '/^\[.\+\]:[[:space:]]\+https:\/\//! s#^\[\(.\+\)\]:[[:space:]]\+\([[:alnum:]]\)#[\1]: https://github.com/'"${GITHUB_REPOSITORY}"'/blob/master/\2#g' \ 35 | "${readme_filepath}" \ 36 | >> "${readme_profile_filepath}" 37 | 38 | # Retrieve Github repo information 39 | # TODO: make an action out of this? 40 | curl -siX GET "https://api.github.com/repos/${GITHUB_REPOSITORY}" \ 41 | -o "${CURL_OUT}" 42 | process_curl_response || exit 1 43 | gh_description=$(print_curl_response_json | jq -r .description) 44 | 45 | # Acquire a login token for the Docker Hub API 46 | echo "Acquire Docker Hub login token" 47 | curl -siX POST https://hub.docker.com/v2/users/login/ \ 48 | -H "Content-Type: application/json" \ 49 | -o "${CURL_OUT}" \ 50 | -d @- \ 51 | << PAYLOAD 52 | { 53 | "username": "${DOCKERHUB_USER_NAME}", 54 | "password": "${DOCKERHUB_ACCESS_TOKEN}" 55 | } 56 | 57 | PAYLOAD 58 | 59 | process_curl_response | grep -v token || exit 1 60 | dh_token=$(print_curl_response_json | jq -r .token) 61 | 62 | echo "Will try to update the description of ${dh_repo}" 63 | curl -siX PATCH "https://hub.docker.com/v2/repositories/${dh_repo}/" \ 64 | -H "Authorization: JWT ${dh_token}" \ 65 | -H "Content-Type: application/json" \ 66 | -o "${CURL_OUT}" \ 67 | --data @- << PAYLOAD 68 | { 69 | "description": "${gh_description}", 70 | "full_description": $(jq -Rsa . "${readme_profile_filepath}") 71 | } 72 | PAYLOAD 73 | # NB: jq outputs a JSON string, that is `"..."`. 74 | # Therefore, we don't add additional quotes, which break the outer JSON! 75 | 76 | # TODO: tags/categories/topics? << Github topics 77 | # TODO: icon? 78 | 79 | process_curl_response || exit 1 80 | print_curl_response_json | jq . 81 | -------------------------------------------------------------------------------- /.github/trigger-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: Obsolete? Can trigger Workflow from web UI these days. 4 | 5 | set -eu 6 | 7 | read -sp "Github API Token: " ght 8 | echo "" 9 | 10 | curl -siX POST https://api.github.com/repos/reitzig/texlive-docker/dispatches \ 11 | -H "Authorization: token ${ght}" \ 12 | -H "Accept: application/vnd.github.everest-preview+json" \ 13 | --data '{"event_type": "manual-release"}' 14 | -------------------------------------------------------------------------------- /.github/workflows/test-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: "Test & Publish" 2 | on: 3 | schedule: 4 | - cron: '0 16 28 * *' 5 | # Verify that (most) things work before running the monthly release. 6 | push: 7 | branches: 8 | - 'master' 9 | paths-ignore: 10 | - '**.md' 11 | - 'LICENSE' 12 | - 'demo/**' 13 | pull_request: 14 | create: 15 | workflow_dispatch: 16 | 17 | env: 18 | IMAGE_TAG_LIST: "texlive-image-tags" 19 | TEXLIVE_INSTALLER_IMAGE: "texlive-installer:latest" 20 | DOCKERHUB_USER_NAME: "reitzig" 21 | 22 | jobs: 23 | lint-docker: 24 | name: "Lint: Dockerfile" 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: hadolint/hadolint-action@v3.1.0 29 | 30 | test-create-minimal: 31 | name: "Test: Create Minimal Image" 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Obtain Installer 36 | run: .github/scripts/build-installer-image.sh 37 | # TODO: Can the separate jobs share this image? 38 | # TODO: build daily? and push to Github registry? (--> catch APK upgrades) 39 | - name: "Build Test Image" 40 | run: .github/scripts/build-image.sh ${GITHUB_REF} minimal 41 | - name: "Test: Print version" 42 | run: docker run --rm $(head -n 1 "${IMAGE_TAG_LIST}") version 43 | 44 | test-run-examples: 45 | name: "Test: Run Usage Examples" 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Obtain Installer 50 | run: .github/scripts/build-installer-image.sh 51 | - name: "Build Test Image" 52 | run: .github/scripts/build-image.sh ${GITHUB_REF} base-luatex 53 | - name: "Test: One-off build" 54 | run: .github/scripts/run-example.sh $(head -n 1 "${IMAGE_TAG_LIST}") one-off-build 55 | - name: "Test: Repeated build" 56 | run: .github/scripts/run-example.sh $(head -n 1 "${IMAGE_TAG_LIST}") repeated-build 57 | - name: "Test: Complex build" 58 | run: .github/scripts/run-example.sh $(head -n 1 "${IMAGE_TAG_LIST}") complex-build 59 | - name: "Test: Interactive build" 60 | run: .github/scripts/run-example.sh $(head -n 1 "${IMAGE_TAG_LIST}") interactive-build 61 | - name: "Test: Custom Image" 62 | run: .github/scripts/run-example.sh $(head -n 1 "${IMAGE_TAG_LIST}") custom-image 63 | # TODO: Can we repeat the same step for different inputs? 64 | # TODO: Split into separate jobs so they can be run in parallel? 65 | 66 | publish-release: 67 | name: Publish Images 68 | runs-on: ubuntu-latest 69 | needs: 70 | - test-create-minimal 71 | - test-run-examples 72 | if: startsWith(github.ref, 'refs/tags/pre-') || startsWith(github.ref, 'refs/tags/release-') 73 | steps: 74 | - uses: actions/checkout@v4 75 | - name: Obtain Installer 76 | run: .github/scripts/build-installer-image.sh 77 | - name: Build images 78 | run: | 79 | for p in profiles/*; do 80 | p=$(basename ${p}) 81 | .github/scripts/build-image.sh ${GITHUB_REF} ${p%.profile} 82 | done 83 | - name: Publish images 84 | # TODO: Use step output instead of a file? 85 | # --> https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-output-parameter-set-output 86 | run: | 87 | docker login --username ${DOCKERHUB_USER_NAME} --password ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} 88 | while IFS= read -r tag; do 89 | docker push "${tag}" 90 | done <"${IMAGE_TAG_LIST}" 91 | docker logout 92 | - name: Determine version 93 | id: release-version 94 | run: | 95 | tag="${{ github.ref }}" 96 | version="${tag##*/*-}" 97 | is_prerelease=false 98 | title="Release ${version}" 99 | if [[ "${tag}" == *"/pre-"* ]]; then 100 | is_prerelease=true 101 | title="Pre-${title}" 102 | fi 103 | 104 | echo "version=${version}" >> $GITHUB_OUTPUT 105 | echo "is_prerelease=${is_prerelease}" >> $GITHUB_OUTPUT 106 | echo "title=${title}" >> $GITHUB_OUTPUT 107 | - name: Assemble release notes 108 | id: release-notes 109 | run: | 110 | .github/scripts/assemble-release-notes.sh \ 111 | "${IMAGE_TAG_LIST}" \ 112 | > release-notes.md 113 | - name: Create Github release 114 | id: create-gh-release 115 | uses: softprops/action-gh-release@v2 116 | with: 117 | name: ${{ steps.release-version.outputs.title }} 118 | body_path: release-notes.md 119 | # TODO: mark as draft if created manually 120 | prerelease: ${{ steps.release-version.outputs.is_prerelease }} 121 | -------------------------------------------------------------------------------- /.github/workflows/trigger-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Monthly Release 2 | on: 3 | schedule: 4 | - cron: '0 6 1 * *' 5 | repository_dispatch: 6 | types: manual-release 7 | workflow_dispatch: 8 | 9 | env: 10 | TEXLIVE_INSTALLER_IMAGE: "texlive-installer:latest" 11 | 12 | jobs: 13 | make-tag: 14 | name: Make Release Tag 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 # need to get the tags, cf. https://github.com/actions/checkout/issues/701 20 | - name: Obtain Installer 21 | run: .github/scripts/build-installer-image.sh 22 | - name: Determine TeXlive Version 23 | run: .github/scripts/make-release-tag.sh 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REITZIG }} 26 | # Note: use "real" access token so workflow trigger isn't suppressed 27 | # cf https://github.community/t5/GitHub-Actions/Tag-created-by-API-does-not-trigger-create/m-p/36686/highlight/true#M2653 28 | # --> will trigger `test-and-publish.yml` with a `create` event 29 | -------------------------------------------------------------------------------- /.github/workflows/update-dockerhub-info.yml: -------------------------------------------------------------------------------- 1 | name: "Update DockerHub Info" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'README.md' 8 | - 'LICENSE' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-info: 13 | name: Update Info 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: "Push Info to Repos" 18 | env: 19 | DOCKERHUB_USER_NAME: "reitzig" 20 | DOCKERHUB_ACCESS_TOKEN: "${{ secrets.DOCKERHUB_ACCESS_TOKEN_FULL }}" 21 | run: | 22 | for p in profiles/*; do 23 | p=$(basename ${p}) 24 | .github/scripts/update-dockerhub-info.sh ${p%.profile} 25 | done 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | /.idea 4 | *.log 5 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | failure-threshold: warning 2 | override: 3 | info: 4 | # false positives; not an issue with modern busybox 5 | # cf. https://github.com/hadolint/hadolint/issues/1084 6 | - SC3037 7 | - DL4006 8 | -------------------------------------------------------------------------------- /.noai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reitzig/texlive-docker/ffb5538b3eacc2c74bb55b845fa8ab7eb0714fc9/.noai -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 AS texlive-installer 2 | 3 | SHELL ["/bin/sh", "-e", "-u", "-o", "pipefail", "-c"] 4 | 5 | # renovate: datasource=repology depName=alpine_3_22/bash versioning=loose 6 | ENV BASH_VERSION="5.2.37-r0" 7 | # renovate: datasource=repology depName=alpine_3_22/cairo versioning=loose 8 | ENV CAIRO_VERSION="1.18.4-r0" 9 | # renovate: datasource=repology depName=alpine_3_22/gpg versioning=loose 10 | ENV GPG_VERSION="2.4.7-r0" 11 | # renovate: datasource=repology depName=alpine_3_22/icu versioning=loose 12 | ENV ICU_LIBS_VERSION="76.1-r0" 13 | # renovate: datasource=repology depName=alpine_3_22/gcc versioning=loose 14 | ENV LIBGCC_VERSION="14.2.0-r6" 15 | # renovate: datasource=repology depName=alpine_3_22/libpaper versioning=loose 16 | ENV LIBPAPER_VERSION="2.2.5-r0" 17 | # renovate: datasource=repology depName=alpine_3_22/libpng versioning=loose 18 | ENV LIBPNG_VERSION="1.6.47-r0" 19 | # renovate: datasource=repology depName=alpine_3_22/gcc versioning=loose 20 | ENV LIBSTDCPP_VERSION="14.2.0-r6" 21 | # renovate: datasource=repology depName=alpine_3_22/libx11 versioning=loose 22 | ENV LIBX11_VERSION="1.8.11-r0" 23 | # renovate: datasource=repology depName=alpine_3_22/musl versioning=loose 24 | ENV MUSL_VERSION="1.2.5-r10" 25 | # renovate: datasource=repology depName=alpine_3_22/perl versioning=loose 26 | ENV PERL_VERSION="5.40.2-r0" 27 | # renovate: datasource=repology depName=alpine_3_22/pixman versioning=loose 28 | ENV PIXMAN_VERSION="0.46.0-r0" 29 | # renovate: datasource=repology depName=alpine_3_22/wget versioning=loose 30 | ENV WGET_VERSION="1.25.0-r1" 31 | # renovate: datasource=repology depName=alpine_3_22/xz versioning=loose 32 | ENV XZ_VERSION="5.8.1-r0" 33 | # renovate: datasource=repology depName=alpine_3_22/zlib versioning=loose 34 | ENV ZLIB_VERSION="1.3.1-r2" 35 | RUN apk --no-cache add \ 36 | bash=${BASH_VERSION} \ 37 | cairo=${CAIRO_VERSION} \ 38 | gpg=${GPG_VERSION} \ 39 | icu-libs=${ICU_LIBS_VERSION} \ 40 | libgcc=${LIBGCC_VERSION} \ 41 | libpaper=${LIBPAPER_VERSION} \ 42 | libpng=${LIBPNG_VERSION} \ 43 | libstdc++=${LIBSTDCPP_VERSION} \ 44 | libx11=${LIBX11_VERSION} \ 45 | musl=${MUSL_VERSION} \ 46 | perl=${PERL_VERSION} \ 47 | pixman=${PIXMAN_VERSION} \ 48 | wget=${WGET_VERSION} \ 49 | xz=${XZ_VERSION} \ 50 | zlib=${ZLIB_VERSION} 51 | 52 | ARG ctan_mirror="https://mirrors.ctan.org" 53 | ENV CTAN_MIRROR=$ctan_mirror 54 | 55 | RUN wget --quiet "${CTAN_MIRROR}/systems/texlive/tlnet/install-tl-unx.tar.gz" \ 56 | && tar -xzf install-tl-unx.tar.gz \ 57 | && rm install-tl-unx.tar.gz \ 58 | && mv install-tl-* install-tl 59 | 60 | ENTRYPOINT ["cat", "install-tl/release-texlive.txt"] 61 | # Only used in `make-release-tag.sh`, overwritten for final image below 62 | 63 | # # # # # # # # # # # # # # # 64 | # Re-use the installer image -- built only once during CI/CD! 65 | # cf. build-image.sh 66 | FROM texlive-installer AS texlive 67 | 68 | SHELL ["/bin/sh", "-e", "-u", "-o", "pipefail", "-c"] 69 | 70 | ARG profile=minimal 71 | COPY "profiles/${profile}.profile" /install-tl/${profile}.profile 72 | 73 | # Workaround: installer doesn't seem to handle linuxmusl(-only) install correctly 74 | RUN tlversion="$(head -n 1 install-tl/release-texlive.txt | awk '{ print $5 }')" \ 75 | && mkdir -p "/usr/local/texlive/${tlversion}/bin" \ 76 | && ln -s "/usr/local/texlive/${tlversion}/bin/x86_64-linuxmusl" "/usr/local/texlive/${tlversion}/bin/x86_64-linux" \ 77 | && ln -s "usr/local/texlive/${tlversion}/bin/x86_64-linuxmusl/mktexlsr" "/usr/local/bin/mktexlsr" 78 | 79 | ARG ctan_mirror="https://mirrors.ctan.org" 80 | ENV CTAN_MIRROR=$ctan_mirror 81 | 82 | # hadolint ignore=DL3003 83 | RUN ( cd install-tl \ 84 | && tlversion="$(head -n 1 release-texlive.txt | awk '{ print $5 }')" \ 85 | && sed -i "s/\${tlversion}/${tlversion}/g" "${profile}.profile" \ 86 | && ./install-tl -repository="${CTAN_MIRROR}/systems/texlive/tlnet" -profile "${profile}.profile" \ 87 | ) \ 88 | && rm -rf install-tl \ 89 | && tlmgr version | tail -n 1 > version \ 90 | && echo "Installed on $(date)" >> version 91 | # && tlmgr option repository "${CTAN_MIRROR}" 92 | # TODO: Determine if this is necessary -- shouldn't be, and 93 | # we don't want to hammer the same mirror whenever the image is used! 94 | 95 | ARG src_dir="/work/src" 96 | ARG tmp_dir="/work/tmp" 97 | ARG out_dir="/work/out" 98 | ENV SRC_DIR="${src_dir}" 99 | ENV TMP_DIR="${tmp_dir}" 100 | ENV OUT_DIR="${out_dir}" 101 | 102 | # Instead of VOLUME, which breaks multi-stage builds: 103 | RUN mkdir -p "${src_dir}" "${tmp_dir}" "${out_dir}" 104 | 105 | ENV TEXLIVE_REPOSITORY="" 106 | COPY entrypoint.sh /bin/entrypoint 107 | # Add "aliases" to align `docker run` and `docker exec` usage. 108 | RUN set -eo noclobber; \ 109 | for cmd in help version hold clean work; do \ 110 | echo -e "#!/bin/sh\n\nentrypoint ${cmd} \"\${@}\"" > /bin/${cmd}; \ 111 | chmod +x /bin/${cmd}; \ 112 | done 113 | 114 | WORKDIR /work 115 | ENV BUILDSCRIPT="build.sh" 116 | ENV TEXLIVEFILE="Texlivefile" 117 | ENV OUTPUT="*.pdf *.log" 118 | 119 | # Labels as per OCI annotation spec 120 | # cf. https://github.com/opencontainers/image-spec/blob/master/annotations.md (Oct 2019) 121 | ARG label_maintainer="Raphael Reitzig" 122 | ARG label_github="https://github.com/reitzig/texlive-docker" 123 | ARG label_created="nA" 124 | ARG label_version="nA" 125 | ARG label_tlversion="" 126 | ARG label_revision="nA" 127 | LABEL org.opencontainers.image.created="${label_created}" \ 128 | org.opencontainers.image.authors="${label_maintainer}" \ 129 | org.opencontainers.image.url="${label_github}" \ 130 | org.opencontainers.image.documentation="${label_github}" \ 131 | org.opencontainers.image.source="${label_github}" \ 132 | org.opencontainers.image.version="${label_version}" \ 133 | org.opencontainers.image.revision="${label_revision}" \ 134 | org.opencontainers.image.vendor="${label_maintainer}" \ 135 | org.opencontainers.image.licenses="Apache-2.0" \ 136 | # org.opencontainers.image.ref.name -- doesn't apply 137 | org.opencontainers.image.title="TeXlive ${label_tlversion} (${profile})" 138 | # org.opencontainers.image.description -- not much more to tell 139 | 140 | # TODO: ONBUILD to install additional packages? 141 | 142 | STOPSIGNAL SIGKILL 143 | ENTRYPOINT [ "/bin/entrypoint" ] 144 | CMD [ "help" ] 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TeXlive Docker Images 2 | 3 | Yet another attempt at coming up with working _and_ generally applicable 4 | Docker images for [TeXlive][texlive]. 5 | 6 | The basic concept is to provide small-ish base images which 7 | install additional packages from CTAN if and when needed. 8 | 9 | These images attempt to cover the following use cases: 10 | 11 | - Replace local TeXlive installations. 12 | - Build LaTeX documents in CI/CD pipelines. 13 | - Build legacy documents with old package versions. 14 | 15 | We currently publish the following images based on different selections 16 | from the TeXlive collections suggested by [the installer][install-tl]; 17 | from smaller to larger: 18 | 19 | - [reitzig/texlive-minimal][minimal-dockerhub] ([profile][minimal-profile]) 20 | - [reitzig/texlive-base][base-dockerhub] ([profile][base-profile]) 21 | - [reitzig/texlive-base-luatex][base-luatex-dockerhub] ([profile][base-luatex-profile]) 22 | 23 | We also provide PoCs to demonstrate that more involved applications can 24 | be built on top of the base images provided here: 25 | 26 | - [Serve a static set of pre-built documents.][demo-static-serve] 27 | 28 | 29 | 30 | 31 | 32 | ## Usage 33 | 34 | The fastest way to build a document at hand (once) is this: 35 | 36 | ```bash 37 | docker run --rm \ 38 | --volume `pwd`:/work/src:ro \ 39 | --volume `pwd`/out:/work/out \ 40 | reitzig/texlive-base-luatex \ 41 | work lualatex hello_world.tex 42 | ``` 43 | 44 | Note: 45 | 46 | - This assumes that all TeXlive packages beyond what is contained in the 47 | `texlive-base-luatex` image are listed in `Texlivefile`. 48 | You can also use image `reitzig/texlive-full` instead if you are happy 49 | with downloading a (way) larger image. 50 | - This may overwrite files in `out`. Chose a folder name that you currently 51 | do not use. 52 | 53 | See the scripts in [`examples`][examples] for other ways to use the images. 54 | 55 | ### Dependencies 56 | 57 | Place a file called `Texlivefile` with a list of required CTAN packages, 58 | one name per line, in the source directory. 59 | The container will install all packages on that list before running the work command. 60 | 61 | --- 62 | ⚠️ Installing dependencies in this way will stop working once a new version of TeXlive is released, 63 | with an error like this: 64 | 65 | > tlmgr: Local TeX Live (2023) is older than remote repository (2024). 66 | 67 | If you need to keep using an older image for a little while, 68 | you can override the repository by setting environment variable 69 | `TEXLIVE_REPOSITORY` to a value like 70 | ``` 71 | https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2023/tlnet-final 72 | ``` 73 | This feature has been available since 2024.2; 74 | see 75 | [reitzig/texlive-docker#18.2022953222](https://github.com/reitzig/texlive-docker/issues/18#issuecomment-2022953222) 76 | for hints on how to backport it to older images. 77 | 78 | ⚠️ Note that most CTAN mirrors do not maintain historic versions 79 | (cf. [tex.SE#460132](https://tex.stackexchange.com/questions/460132/historic-tex-live-distributions-https-sftp-mirror)), 80 | so keep in mind that widespread use of this workaround _will_ stress those few mirrors that do. 81 | We strongly recommend upgrading to the latest TeXlive version as soon as possible! 82 | 83 | 84 | ℹ️ That said, an alternative is to maintain custom Docker images with historic package versions. 86 | This approach completely avoids the need for running `tlmgr` at runtime, 87 | so repository freeze does not impact your builds. 88 | 89 | --- 90 | 91 | ### Parameters 92 | 93 | You can adjust some defaults of the 94 | [main container script][entrypoint] 95 | by 96 | [setting environment variables][docker-set-env] 97 | 98 | - `BUILDSCRIPT` (default: `build.sh`) 99 | If present, the given script will be executed unless a work command is specified. 100 | - `TEXLIVEFILE` (default: `Texlivefile`) 101 | The file to read dependencies from. 102 | - `OUTPUT` (default: `*.pdf *.log`) 103 | Shell pattern for all files that should be copied from the working to the output directory. 104 | 105 | ### Debugging 106 | 107 | All output of the work command is collected in a single folder; extract it with: 108 | 109 | ```bash 110 | docker cp $container:/work/tmp ./ 111 | ``` 112 | 113 | ## Build 114 | 115 | Run 116 | 117 | ```bash 118 | docker build -t texlive-base-luatex --build-arg "profile=base-luatex" . 119 | ``` 120 | 121 | to build an image locally. Exchange `base-luatex` for any of the profile names in 122 | [`profiles`][profiles] to start from another baseline. 123 | 124 | ### Customization 125 | 126 | If you repeatedly need the same exact set of dependencies or even sources, it 127 | might make sense to create your own TeXlive Docker image. 128 | There are two ways to go about that: 129 | 130 | - Extend one of the existing images using your own Dockerfile (see [example][custom-dockerfile]); 131 | install additional TeXlive (or even Alpine) packages, copy source files 132 | or additional scripts into the appropriate folders, fix the work command, or ... 133 | - Use [install-tl][install-tl] to create your own TeXlive installation profile. Make sure to 134 | 135 | 1. select platform `x86_64-linuxmusl` and 136 | 2. manually change line `binary_x86_64-linux 1` in the resulting profile file 137 | to `binary_x86_64-linux 0`. 138 | 140 | 141 | If you want to use your profile across different TeXlive versions, 142 | replace all occurrences of the TeXlive version (e.g. `2019`) with `${tlversion}`. 143 | 144 | Copy the final file to [`profiles`][profiles] and run the regular build command. 145 | 146 | 147 | [examples]: examples 148 | [profiles]: profiles 149 | [entrypoint]: entrypoint.sh 150 | [custom-dockerfile]: examples/Dockerfile 151 | [demo-static-serve]: demo/static-document-server 152 | 153 | [minimal-dockerhub]: https://hub.docker.com/r/reitzig/texlive-minimal 154 | [minimal-profile]: profiles/minimal.profile 155 | [base-dockerhub]: https://hub.docker.com/r/reitzig/texlive-base 156 | [base-profile]: profiles/base.profile 157 | [base-luatex-dockerhub]: https://hub.docker.com/r/reitzig/texlive-base-luatex 158 | [base-luatex-profile]: profiles/base-luatex.profile 159 | 160 | [docker-set-env]: https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file 161 | [install-tl]: https://www.tug.org/texlive/acquire-netinstall.html 162 | [texlive]: https://www.tug.org/texlive/ 163 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pdf 3 | -------------------------------------------------------------------------------- /demo/static-document-server/.dockerignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /*.pdf 3 | /serve.sh 4 | -------------------------------------------------------------------------------- /demo/static-document-server/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG out_dir="/work/out" 2 | # This is needed so all stages agree about the output directory 3 | 4 | FROM reitzig/texlive-base-luatex AS build 5 | 6 | COPY . "${SRC_DIR}/" 7 | ENV BUILDSCRIPT="create_many_documents.sh" 8 | RUN work 9 | 10 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 11 | 12 | FROM nginx:alpine AS serve 13 | ARG out_dir 14 | 15 | # Enable the auto index page 16 | RUN sed -i -e '/location.*\/.*{/a autoindex on\;' /etc/nginx/conf.d/default.conf \ 17 | && rm /usr/share/nginx/html/* 18 | 19 | COPY --from=build \ 20 | ${out_dir}/*.pdf \ 21 | /usr/share/nginx/html/ 22 | 23 | RUN ls -R /usr/share/nginx/html 24 | -------------------------------------------------------------------------------- /demo/static-document-server/Texlivefile: -------------------------------------------------------------------------------- 1 | qrcode 2 | -------------------------------------------------------------------------------- /demo/static-document-server/create_many_documents.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for name in World Alice "Dear Visitor"; do \ 4 | sed -e "s/<% name %>/${name}/" \ 5 | hello_world.tex \ 6 | > "hello_${name}.tex" 7 | lualatex "hello_${name}.tex" 8 | done 9 | -------------------------------------------------------------------------------- /demo/static-document-server/hello_world.tex: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper]{article} 2 | 3 | \usepackage{qrcode} 4 | 5 | \begin{document} 6 | Hello, <% name %>! 7 | 8 | \centering 9 | \qrcode{https://github.com/reitzig/texlive-docker} 10 | \end{document} 11 | -------------------------------------------------------------------------------- /demo/static-document-server/serve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # What we want to run is just 4 | # 5 | # docker build --tag tld-demo-serve-static . 6 | # 7 | # While this _works_, it doesn't lend itself well to 8 | # re-building the server image: in multi-stage builds, 9 | # caching of layers doesn't "just work" as it does for 10 | # regular builds. 11 | # 12 | # Hence, this more convoluted construct to tell Docker 13 | # explicitly to use its usual smarts: 14 | 15 | docker build \ 16 | --target build \ 17 | --cache-from tld-demo-serve-static:build \ 18 | --tag tld-demo-serve-static:build \ 19 | . 20 | docker build \ 21 | --cache-from tld-demo-serve-static:build \ 22 | --cache-from tld-demo-serve-static:latest \ 23 | --tag tld-demo-serve-static \ 24 | . 25 | 26 | echo "" 27 | echo "Try and access in a browser: http://localhost:8080/" 28 | echo "Stop with CTRL+C" 29 | 30 | docker run --publish 8080:80 --rm tld-demo-serve-static 31 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | command="${1:-}" 6 | 7 | documentation="$(cat <<-HELP 8 | 9 | Usage: docker exec CONTAINER COMMAND [ARG] 10 | 11 | Entrypoint script of TeXlive Docker containers. 12 | 13 | Commands: 14 | 15 | clean Remove all files in TMP_DIR. 16 | help Print this message. 17 | hold Halt execution and wait for interrupt, keeping container 18 | alive. 19 | version Print information about the TeXlive version installed in 20 | CONTAINER. 21 | work [STRING] If given, interprets STRING as Bash command. Otherwise, 22 | BUILDSCRIPT is run as work command. 23 | Execution proceeds as follows. 24 | 25 | 1. Install dependencies listed in TEXLIVEFILE. 26 | 2. Copy content of SRC_DIR to TMP_DIR. 27 | 3. Delete files matching OUTPUT from TMP_DIR. 28 | 4. Execute work command in TMP_DIR. 29 | 5. Copy files matching OUTPUT from TMP_DIR to OUT_DIR. 30 | 31 | Note how the command is not per se idempotent since TMP_DIR 32 | is not cleaned after each run. 33 | 34 | 35 | Environment Variables: 36 | 37 | OUT_DIR Directory for the relevant output of work commands. 38 | Default: /work/out 39 | SRC_DIR Directory with project sources. Can be read-only. 40 | Default: /work/src 41 | TMP_DIR The working directory for work commands. 42 | Default: /work/tmp 43 | BUILDSCRIPT Script in SRC_DIR that can be run by the work command. 44 | Default: build.sh 45 | TEXLIVE_REPOSITORY Direct URL to a TeXlive repository; 46 | bypasses use of mirrors.ctan.org and gives access to history versions. 47 | Default: empty (default behaviour of tlmgr) 48 | TEXLIVEFILE A file in SRC_DIR that contains the TeXlive packages the 49 | project requires, one package name per line. 50 | Default: Texlivefile 51 | OUTPUT Bash glob pattern that defines the relevant output of 52 | work commands. 53 | Default: '*.pdf *.log' 54 | HELP 55 | )" 56 | 57 | # Make sure the main folders exist 58 | mkdir -p "${SRC_DIR}" "${TMP_DIR}" "${OUT_DIR}" 59 | 60 | case "${command}" in 61 | "" | "help" ) 62 | echo "${documentation}" 63 | ;; 64 | "version" ) 65 | cat /version 66 | ;; 67 | "hold" ) 68 | echo "Blocking to keep container alive" 69 | tail -f /dev/null 70 | ;; 71 | "clean" ) 72 | rm -rf "${TMP_DIR:?}"/* 73 | ;; 74 | "work" ) # Just because 'run' and 'exec' are Docker keywords 75 | case "${2:-}" in 76 | "" ) 77 | if [[ -f "${SRC_DIR}/${BUILDSCRIPT}" ]]; then 78 | work_command="${SRC_DIR}/${BUILDSCRIPT}" 79 | else 80 | echo 'Neither work command or build script given.' 81 | exit 1 82 | fi 83 | ;; 84 | *) 85 | shift 86 | work_command="$@" 87 | 88 | if [[ -f "${SRC_DIR}/${BUILDSCRIPT}" ]]; then 89 | echo "Work command overrides build script ${BUILDSCRIPT}." 90 | fi 91 | ;; 92 | esac 93 | 94 | # Install dependencies 95 | hashfile="${TMP_DIR}/${TEXLIVEFILE}.sha" 96 | mkdir -p "$(dirname "${hashfile}")" 97 | 98 | if [[ -f "${SRC_DIR}/${TEXLIVEFILE}" ]]; then 99 | if ! sha256sum -c "${hashfile}" > /dev/null 2>&1; then 100 | tlrepo="" 101 | if [ -n "${TEXLIVE_REPOSITORY}" ]; then 102 | echo "Will use TeXlive repository ${TEXLIVE_REPOSITORY}" 103 | tlrepo="--repository ${TEXLIVE_REPOSITORY}" 104 | fi 105 | 106 | echo "Installing dependencies ..." 107 | tlmgr update ${tlrepo} --self 108 | xargs tlmgr install ${tlrepo} < "${SRC_DIR}/${TEXLIVEFILE}" 109 | tlmgr path add 110 | sha256sum "${SRC_DIR}/${TEXLIVEFILE}" > "${hashfile}" 111 | else 112 | echo "${TEXLIVEFILE} has not changed; nothing new to install." 113 | fi 114 | else 115 | echo "${TEXLIVEFILE} not found; continuing without installing additional packages." 116 | fi 117 | echo "" 118 | 119 | # Execute command on a copy of the sources 120 | set +e # continue even if globs don't match 121 | cp -rf "${SRC_DIR}"/* "${TMP_DIR}"/ 122 | cd "${TMP_DIR}" 123 | rm -f ${OUTPUT} 124 | bash -c "${work_command}"; work_status=$? 125 | cp ${OUTPUT} "${OUT_DIR}"/ 126 | 127 | exit ${work_status} 128 | ;; 129 | * ) 130 | echo "Unknown command '${command}'" 131 | exit 1 132 | ;; 133 | esac 134 | -------------------------------------------------------------------------------- /examples/.dockerignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /*.pdf 3 | /*.sh 4 | /.gitignore 5 | /Dockerfile* 6 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | *.log 3 | *.pdf -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG base_image="reitzig/texlive-base-luatex" 2 | FROM ${base_image} 3 | 4 | COPY hello_world.tex "${SRC_DIR}/" 5 | 6 | RUN tlmgr install amsmath qrcode 7 | # --> faster run! 8 | 9 | ENTRYPOINT entrypoint work 'lualatex hello_world.tex' > /dev/null && cat "${OUT_DIR}/hello_world.pdf" 10 | # TODO: If this is a frequent/attractive use case, 11 | # introduce a silent mode so the default entrypoint 12 | # can be piped to a file directly 13 | # ( `... work 'lualatex hello_world.tex && cat ...'`) 14 | # Or an option that prints the result file? Which? 15 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | All examples share the same basic task: 4 | 5 | - compile document [`hello_world.tex`](hello_world.tex) with 6 | - dependencies given in [`Texlivefile`](Texlivefile). 7 | 8 | They go about it in different ways in order to showcase the flexibility 9 | of using TeXlive in this way -- and to _specify_ how they should work 10 | for testing. 11 | 12 | - [One-off build](one-off-build.sh) -- a single command that builds 13 | a document from scratch. 14 | - [Complex build](complex-build.sh) -- shows how to use a custom build 15 | script 16 | - [Repeated build](repeated-build.sh) -- shows how to rebuild a 17 | document in the same container, potentially making use of temporary 18 | files (e.g. TikZ externalization). 19 | - [Interactive build](interactive-build.sh) -- shows how to keep a 20 | TeXlive container alive and interact freely with it using `docker cp` 21 | and `docker exec`. 22 | - [Custom image](custom-image.sh) -- shows how do use a custom Docker 23 | image to cover more specialized requirements such as pinning versions 24 | of TeXlive packages. If you need reproducible and/or archive builds, 25 | look here. 26 | 27 | These are just a few examples -- you have to full power of Docker and 28 | the Linux running _in_ the container at your disposal. Go nuts! 29 | You may find some ideas in our [demos](../demo). 30 | 31 | ## How to run the examples 32 | 33 | The examples can be run as shell scripts: 34 | 35 | ```bash 36 | ./one-off-build.sh 37 | ``` 38 | 39 | ### Note 40 | 41 | - The examples are tested to run with 42 | [`base-luatex`](../profiles/base-luatex.profile) 43 | images. "Larger" images will also work. 44 | - If you want to try out a custom image, pass its tag to the example: 45 | 46 | ```bash 47 | ./one-off-build.sh texlive-base-luatex:local-testing 48 | ``` 49 | 50 | ## How to read the examples 51 | 52 | The examples are executable for the purpose of testing. 53 | Some hoop-jumping is necessary to make them run both locally _and_ 54 | in CI/CD jobs which, unfortunately, has made them less readable. 55 | 56 | Here are some remarks that should help. 57 | 58 | - Script [_example_setup.sh](_example_setup.sh) is `source`-ed from the top of each 59 | script. If defines variables that are then used in the example 60 | scripts. 61 | 62 | - The first parameter is the name (and tag) of the image used to run 63 | the example; it is optional with default `reitzig/texlive-base-luatex`. 64 | It sets variable `$image` to this value. 65 | - It sets variable `$tty_params` so that `docker run` et al. run 66 | in both interactive and non-interactive shells. 67 | -------------------------------------------------------------------------------- /examples/Texlivefile: -------------------------------------------------------------------------------- 1 | amsmath 2 | qrcode 3 | -------------------------------------------------------------------------------- /examples/_custom-build-script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | lualatex hello_world.tex 4 | echo -e "\n\nMaybe we need a second run?\n\n" 5 | lualatex hello_world.tex 6 | -------------------------------------------------------------------------------- /examples/_example_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Parameter for CI/CD and offline testing 4 | image=${1:-reitzig/texlive-base-luatex} 5 | echo "Will use image ${image}" 6 | 7 | # This is another hoop for CI/CD, you can usually ignore it: 8 | tty_params="" 9 | if [[ $- == *i* ]]; then 10 | tty_params="--interactive --tty" 11 | fi 12 | -------------------------------------------------------------------------------- /examples/complex-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname $0)/_example_setup.sh" "${@}" 4 | 5 | # This showcases how to use a build script, based on the "one-off build". 6 | # Other examples can be adapted similarly. 7 | 8 | mkdir -p out 9 | 10 | docker run --name=tld-example ${tty_params} --rm \ 11 | --volume `pwd`:/work/src:ro \ 12 | --volume `pwd`/out:/work/out \ 13 | --env 'BUILDSCRIPT=_custom-build-script.sh' \ 14 | ${image} \ 15 | work 16 | 17 | # Note that you can override the build script by specifying a build command; 18 | # this can be useful when re-using the same container (see some of the other 19 | # examples). 20 | 21 | mv out/* ./ && rm -rf out 22 | -------------------------------------------------------------------------------- /examples/custom-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname $0)/_example_setup.sh" "${@}" 4 | 5 | # This approach is good for builds with complicated setup: 6 | # create a new image that runs whatever you want. 7 | 8 | docker build --tag tld-hello-world --build-arg "base_image=${image}" . 9 | docker run --rm tld-hello-world:latest > hello_world.pdf 10 | # Note how we don't need any volume bindings! 11 | docker rmi tld-hello-world 12 | -------------------------------------------------------------------------------- /examples/hello_world.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{amsmath,qrcode} 4 | 5 | 6 | \begin{document} 7 | Hello, World! 8 | 9 | \[ \text{TeXlive} \xrightarrow{\text{scripting}} \text{Docker} \] 10 | 11 | \qrcode{https://github.com/reitzig/texlive-docker} 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/interactive-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname $0)/_example_setup.sh" "${@}" 4 | 5 | # This is the most flexible approach: 6 | # 7 | # - copy files in and out of the container at will 8 | # - run arbitrary commands inside the container 9 | # 10 | # With this approach, you can effectively use the 11 | # container TeXlive as if you had installed it on your 12 | # machine, without any convenience wrappers. 13 | 14 | # Start the container: it will not run any command yet! 15 | docker run --name=tld-example --detach --rm \ 16 | ${image} hold 17 | 18 | docker cp . tld-example:/work/src/ # You could also use a bind-mount instead 19 | docker exec tld-example work 'lualatex hello_world.tex' 20 | # NB: You don't _have_ to use `(entrypoint) work`, it just takes care of 21 | # the default file "flow" and dependency handling. 22 | docker cp tld-example:/work/out/ ./ \ 23 | && mv out/* ./ \ 24 | && rm -rf out # You could also use a bind-mount instead 25 | # Repeat these steps, maybe with modifications, as needed. 26 | 27 | # Use 28 | # docker exec tld-example clean 29 | # to empty out the working directory and let lualatex start from scratch. 30 | 31 | # Use 32 | # docker exec -it bash 33 | # to step "into" the container for debugging of full low-level access. 34 | 35 | docker rm --force tld-example > /dev/null 36 | -------------------------------------------------------------------------------- /examples/one-off-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname $0)/_example_setup.sh" "${@}" 4 | 5 | # This is the most ad-hoc approach: a single command will build 6 | # a LaTeX document in the current folder, once, and remove the 7 | # container again. 8 | 9 | mkdir -p out 10 | 11 | docker run --name=tld-example ${tty_params} --rm \ 12 | --volume `pwd`:/work/src:ro \ 13 | --volume `pwd`/out:/work/out \ 14 | ${image} \ 15 | work lualatex hello_world.tex 16 | 17 | mv out/* ./ && rm -rf out 18 | 19 | # Nota bene: the output files now belong to root; 20 | # this is an unfortunate restriction of docker. 21 | # See `repeated-build.sh` for a way to avoid this. 22 | -------------------------------------------------------------------------------- /examples/repeated-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname $0)/_example_setup.sh" "${@}" 4 | 5 | # This is a medium-complexity approach. 6 | # A document in the current directory is built but 7 | # the container is kept around; the document can be rebuilt 8 | # as sources (or dependencies) change without incurring the 9 | # full overhead. 10 | 11 | docker run --name=tld-example ${tty_params} \ 12 | --volume `pwd`:/work/src:ro \ 13 | ${image} \ 14 | work lualatex hello_world.tex 15 | 16 | docker start --attach ${tty_params} tld-example 17 | docker cp tld-example:/work/out/ ./ \ 18 | && mv out/* ./ \ 19 | && rm -rf out # You could also use a bind-mount instead 20 | # Repeat these steps as needed 21 | 22 | docker rm --force tld-example > /dev/null 23 | -------------------------------------------------------------------------------- /profiles/base-luatex.profile: -------------------------------------------------------------------------------- 1 | selected_scheme scheme-custom 2 | TEXDIR /usr/local/texlive/${tlversion} 3 | TEXMFCONFIG ~/.texlive${tlversion}/texmf-config 4 | TEXMFHOME ~/texmf 5 | TEXMFLOCAL /usr/local/texlive/texmf-local 6 | TEXMFSYSCONFIG /usr/local/texlive/${tlversion}/texmf-config 7 | TEXMFSYSVAR /usr/local/texlive/${tlversion}/texmf-var 8 | TEXMFVAR ~/.texlive${tlversion}/texmf-var 9 | binary_x86_64-linux 0 10 | binary_x86_64-linuxmusl 1 11 | collection-basic 1 12 | collection-fontsrecommended 1 13 | collection-latex 1 14 | collection-latexrecommended 1 15 | collection-luatex 1 16 | instopt_adjustpath 1 17 | instopt_adjustrepo 1 18 | instopt_letter 0 19 | instopt_portable 0 20 | instopt_write18_restricted 1 21 | tlpdbopt_autobackup 1 22 | tlpdbopt_backupdir tlpkg/backups 23 | tlpdbopt_create_formats 1 24 | tlpdbopt_desktop_integration 1 25 | tlpdbopt_file_assocs 1 26 | tlpdbopt_generate_updmap 0 27 | tlpdbopt_install_docfiles 0 28 | tlpdbopt_install_srcfiles 0 29 | tlpdbopt_post_code 1 30 | tlpdbopt_sys_bin /usr/local/bin 31 | tlpdbopt_sys_info /usr/local/info 32 | tlpdbopt_sys_man /usr/local/man 33 | tlpdbopt_w32_multi_user 1 34 | -------------------------------------------------------------------------------- /profiles/base.profile: -------------------------------------------------------------------------------- 1 | selected_scheme scheme-custom 2 | TEXDIR /usr/local/texlive/${tlversion} 3 | TEXMFCONFIG ~/.texlive${tlversion}/texmf-config 4 | TEXMFHOME ~/texmf 5 | TEXMFLOCAL /usr/local/texlive/texmf-local 6 | TEXMFSYSCONFIG /usr/local/texlive/${tlversion}/texmf-config 7 | TEXMFSYSVAR /usr/local/texlive/${tlversion}/texmf-var 8 | TEXMFVAR ~/.texlive${tlversion}/texmf-var 9 | binary_x86_64-linux 0 10 | binary_x86_64-linuxmusl 1 11 | collection-basic 1 12 | collection-fontsrecommended 1 13 | collection-latex 1 14 | collection-latexrecommended 1 15 | instopt_adjustpath 1 16 | instopt_adjustrepo 1 17 | instopt_letter 0 18 | instopt_portable 0 19 | instopt_write18_restricted 1 20 | tlpdbopt_autobackup 1 21 | tlpdbopt_backupdir tlpkg/backups 22 | tlpdbopt_create_formats 1 23 | tlpdbopt_desktop_integration 1 24 | tlpdbopt_file_assocs 1 25 | tlpdbopt_generate_updmap 0 26 | tlpdbopt_install_docfiles 0 27 | tlpdbopt_install_srcfiles 0 28 | tlpdbopt_post_code 1 29 | tlpdbopt_sys_bin /usr/local/bin 30 | tlpdbopt_sys_info /usr/local/info 31 | tlpdbopt_sys_man /usr/local/man 32 | tlpdbopt_w32_multi_user 1 33 | -------------------------------------------------------------------------------- /profiles/minimal.profile: -------------------------------------------------------------------------------- 1 | selected_scheme scheme-custom 2 | TEXDIR /usr/local/texlive/${tlversion} 3 | TEXMFCONFIG ~/.texlive${tlversion}/texmf-config 4 | TEXMFHOME ~/texmf 5 | TEXMFLOCAL /usr/local/texlive/texmf-local 6 | TEXMFSYSCONFIG /usr/local/texlive/${tlversion}/texmf-config 7 | TEXMFSYSVAR /usr/local/texlive/${tlversion}/texmf-var 8 | TEXMFVAR ~/.texlive${tlversion}/texmf-var 9 | binary_x86_64-linux 0 10 | binary_x86_64-linuxmusl 1 11 | collection-basic 1 12 | instopt_adjustpath 1 13 | instopt_adjustrepo 1 14 | instopt_letter 0 15 | instopt_portable 0 16 | instopt_write18_restricted 1 17 | tlpdbopt_autobackup 1 18 | tlpdbopt_backupdir tlpkg/backups 19 | tlpdbopt_create_formats 1 20 | tlpdbopt_desktop_integration 1 21 | tlpdbopt_file_assocs 1 22 | tlpdbopt_generate_updmap 0 23 | tlpdbopt_install_docfiles 0 24 | tlpdbopt_install_srcfiles 0 25 | tlpdbopt_post_code 1 26 | tlpdbopt_sys_bin /usr/local/bin 27 | tlpdbopt_sys_info /usr/local/info 28 | tlpdbopt_sys_man /usr/local/man 29 | tlpdbopt_w32_multi_user 1 30 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | 'docker:pinDigests', 6 | 'customManagers:dockerfileVersions', 7 | ], 8 | packageRules: [ 9 | { 10 | matchFileNames: [ 11 | 'demo/**', 12 | 'examples/**', 13 | ], 14 | enabled: false, 15 | }, 16 | { 17 | matchFileNames: [ 18 | 'Dockerfile', 19 | ], 20 | schedule: [ 21 | // Update dependencies in time for test run and release: 22 | // (compare schedules in test-and-publish.yml and trigger-release.yml) 23 | '* * 28-31 * *', 24 | '* 0-5 1 * *', 25 | ], 26 | automerge: true, 27 | groupName: 'base image', 28 | }, 29 | ], 30 | } 31 | --------------------------------------------------------------------------------