Docker / CI 설정

제 디스크에 풀려 있는 TeX Live 2024는 8.7 GB입니다. 그런데 Docker Hub가 공개하는 texlive/texlive:latest는 압축 기준 약 2.5 GB, 최소 구성인 latest-minimal은 약 335 MB입니다(둘 다 2026년 8월 시점 amd64 표기값). LaTeX을 Docker로 돌린다는 선택은 무엇보다 이 숫자에 관한 이야기입니다. 이 페이지는 texlive/texlive 이미지 자체——누가 만들고, 안에 무엇이 들었고, 어느 태그를 고르고, 어떻게 버전을 고정하는가——와 컨테이너 특유의 함정, 곧 나온 PDF가 root 소유가 되는 문제를 다룹니다. GitHub Actions 워크플로를 쓰는 법 자체는 CI 페이지가 담당합니다.

왜 TeX을 컨테이너로 돌리는가

이유는 하나, 재현성입니다. 같은 .tex에서 같은 PDF가 나오는지는 원고만으로 정해지지 않습니다. TeX Live가 몇 년판인지, 각 패키지가 어느 리비전인지, 어떤 글꼴이 등록되어 있는지에 좌우됩니다. Docker 이미지는 그 환경을 운영체제째로 굳혀서 상자에 담아 배포한 것입니다. 안에는 특정 스냅숏의 TeX Live와 엔진·패키지·글꼴이 들어 있어서, 내 컴퓨터가 윈도우든 macOS든, CI 러너 위든, 같은 상자를 열면 같은 환경이 섭니다. “내 PC에서는 되는데 공저자와 CI에서는 안 된다”가 거의 사라지는 이유입니다.

컨테이너와 로컬 설치는 배타적이지 않습니다. 현실적인 분담은 평소 집필은 편집기와 로컬 TeX Live로 하고, 최종 빌드와 공저자에게 보낼 판본만 컨테이너에 맡기는 것입니다. 내 환경을 더럽히지 않고 이것저것 시험할 수 있다는 이점도 있습니다. 어떤 \usepackage{...}가 왜 실패하는지 가려내고 싶을 때, 티 없는 TeX Live를 몇 분 만에 마련할 수 있습니다. 물론 대가도 있습니다. 수 GB짜리 이미지를 최소 한 번은 내려받아야 하고, 컨테이너 안에는 GUI 뷰어도 SyncTeX 역방향 검색도 없습니다. 솔직한 정리는 컨테이너는 편집 환경이 아니라 빌드 환경이라는 것입니다.

texlive/texlive 이미지는 누가 만들고 안에는 무엇이 있는가

만드는 곳은 TeX Users Group이 아니라 Island of TeX라는 공동체입니다(TeX Live의 공식 이미지가 아닙니다). 같은 이미지를 Docker Hub의 texlive/texlive와 GitLab의 registry.gitlab.com/islandoftex/images/texlive 양쪽에서 받을 수 있습니다. 바탕은 Debian testing의 slim 이미지이고, Dockerfile에는 Alpine을 고르지 않은 이유가 적혀 있습니다. 작성 당시 biber 같은 바이너리가 Linux/musl 플랫폼용으로 배포되지 않았다는 것입니다. 안에 든 것은 TeX Live 본체만이 아닙니다. arara를 위한 Java, biberxindy를 위한 Perl, minted를 위한 Python과 Pygments, EPS 변환을 위한 Ghostscript, 나아가 pgfplots를 위한 gnuplot까지 함께 들어 있습니다.

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

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

--rm은 종료 후 컨테이너를 버리고, -v "$PWD":/workdir은 현재 위치를 컨테이너 안의 /workdir에 묶으며, -w는 그곳을 작업 디렉터리로 만듭니다. 입력도 출력도 내 폴더에 있으므로 PDF는 그 자리에 떨어집니다. 일본어라면 LuaLaTeX이 가장 짧은 길이라 latexmk -lualatex main.tex로 바꾸기만 하면 됩니다. upLaTeX과 dvipdfmx 경로를 쓸 거라면 뒤에 나오는 .latexmkrc를 원고 옆에 두고 latexmk main.tex를 부릅니다. 한 가지 기억할 것은, 이미지 안에서 tlmgr update --self --all로 갱신할 수 있지만 컨테이너를 버리는 순간 되돌아간다는 점입니다.

어느 태그를 받을까: scheme과 실제 크기

태그는 scheme(규모)doc / src 포함 여부의 곱입니다. scheme은 minimal, basic, small, medium, full의 다섯 단계이고(ConTeXt용 context까지 세면 여섯), 접미사 없는 latestlatest-full의 별칭입니다. 여기에 -doc(설명서), -src(.dtx 원본), -doc-src(둘 다)를 조합합니다. 차이는 극적입니다. Docker Hub 표기로 latest(= full)가 약 2.5 GB인데 latest-doc-src는 약 6.7 GB——문서를 더한 것만으로 두 배가 넘습니다. texmf-dist/doc만 해도 3.7 GB이니 당연하다면 당연합니다.

태그내용Docker Hub 표시 크기(amd64, 2026년 8월)
latestlatest-full의 별칭. 모든 패키지, 문서·원본 없음약 2.53 GB — 망설여지면 이것
latest-mediummedium scheme. 일상적인 문서는 대개 통합니다약 898 MB
latest-smallsmall scheme. CI를 가볍게 하고 싶을 때약 590 MB
latest-basicbasic scheme. 기본 LaTeX에 약간의 추가약 367 MB
latest-minimalminimal scheme. 사실상 plain TeX으로 가장 작습니다약 335 MB
latest-doc-srcfull에 설명서와 원본까지. 여기서는 texdoc도 됩니다약 6.69 GB — 손안에서 찾아볼 때
TL2018-historic과거 릴리스. 2013년 이후 것이 마련되어 있습니다옛 원고를 다시 빌드할 때. 아래 주의도 참고

실무의 선택은 단순합니다. CI에서는 접미사 없는 latest, 필요한 패키지가 들어 있다면 latest-medium. -doc 계열은 손안에서 texdoc를 보고 싶을 때만 받습니다. scheme을 줄일 때 주의가 하나 있습니다. Island of TeX의 README에 따르면, full이 아닌 이미지에서는 tlmgr install로 새 실행 파일을 넣어도 자동으로 PATH에 오르지 않습니다. tlmgr install <pkg> && tlmgr path add처럼 이어서 실행해야 합니다. ARMv8(arm64) 빌드도 제공되지만 README가 실험적이라고 못 박고 있으니, Apple Silicon에서 이상한 동작을 만나면 --platform linux/amd64를 시험해 볼 값어치가 있습니다.

“historic 태그면 불변”이 아니다 —— digest까지 고정하기

과거 릴리스 태그라도 가리키는 이미지는 바뀝니다. Island of TeX의 README는 historic 이미지에 대해 “매달 다시 빌드되며 바탕 운영체제 이미지에 갱신이 있으면 갱신된다”고 분명히 밝힙니다. TeX Live 쪽은 얼어 있어도 그 아래 Debian 라이브러리는 움직입니다. 게다가 latest는 주마다 새 CTAN 스냅숏으로 옮겨 가고, 그 주간 빌드에는 TL{릴리스}-{연}-{월}-{일} 형식의 태그가 붙습니다(TL2021 이전에는 시·분까지 포함하는 형식이었습니다). 그러므로 논문의 최종 빌드를 정말로 고정하고 싶다면 태그가 아니라 digest를 적는 것이 유일하게 확실한 방법입니다.

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

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

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

digest는 이미지 내용 자체의 해시이므로, 원격에서 무슨 일이 일어나도 같은 것만 받게 됩니다. 학위논문이나 몇 해 뒤 심사자가 재현을 요구할 수 있는 작업이라면, 이 한 줄을 저장소에 적어 두는 것만으로 미래의 자신이 구원받습니다. 반대로 그만한 엄밀함이 필요 없는 일상 빌드라면 latest로 충분합니다. 여기서의 판단 기준은 “아무거나”가 아니라 무엇을 위한 빌드인가입니다.

나온 PDF가 root 소유가 되는 문제

리눅스에서 가장 먼저 부딪히는 문제입니다. 이미지의 기본 사용자는 root입니다——Island of TeX의 README도 “apt로 패키지를 손쉽게 더할 수 있도록 기본 사용자를 root로 두었다”고 명시합니다——따라서 바인드 마운트한 디렉터리에 쓰인 main.pdfmain.aux는 root 소유가 되고, 그 뒤로 편집기에서 덮어 저장할 수 없고 git clean으로도 지울 수 없게 됩니다. macOS와 윈도우의 Docker Desktop은 파일 공유 계층이 소유자를 다시 쓰기 때문에 드러나지 않지만, 리눅스와 CI에서는 일상적으로 밟습니다. 해결책은 --user로 자기 UID와 GID를 넘기는 것입니다.

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

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

--user를 넘기면 이번에는 홈 디렉터리가 없는 사용자로 돌게 되어, 캐시를 쓰려는 LuaTeX이나 fontconfig가 불평할 수 있습니다. 그때는 위처럼 HOME을 쓸 수 있는 디렉터리로 돌려 주면 됩니다. 또 2025년 2월부터 이미지에는 texlive라는 비 root 사용자가 마련되어 있고, README는 파생 이미지에서 이를 쓰라고 권합니다——다만 같은 문단에서 바인드 마운트의 권한은 조정이 필요할 수 있다고 덧붙입니다. 근본적으로는 호스트와 컨테이너가 같은 UID를 쓰는 것이 유일한 해법입니다.

프로젝트 전용 이미지를 만들기

매번 tlmgr install을 돌리느니, 필요한 것을 담은 이미지를 한 번 만들어 고정하는 편이 빠르고 원인 추적도 쉽습니다. FROM texlive/texlive:latest-medium으로 시작해 모자란 패키지나 글꼴을 더하면 됩니다. 여기에 재미있는 장치가 하나 숨어 있습니다. Island of TeX 이미지는 equivstexlive-local이라는 가상의 Debian 패키지(버전 번호 9999.99999999)를 만들어 설치된 것으로 등록해 둡니다. 덕분에 apt-get install pandoc처럼 TeX에 의존하는 Debian 패키지를 넣어도 Debian판 TeX Live가 이중으로 딸려 오지 않습니다.

terminal
# Dockerfile
FROM texlive/texlive:latest-medium

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

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

WORKDIR /workdir

docker build -t myproject-tex .로 구워 두면 그다음부터는 이 태그만 쓰면 됩니다. CJK 글꼴을 넣어 두면 LuaLaTeX과 luatexja-fontspec 조합이 상자 안에서 완결됩니다. 이미지를 공저자나 팀에 건네려면 사내 레지스트리에 push 하거나 docker save로 굳혀 전달하는 것이 확실합니다. 여기까지 오면 가장 큰 이점은 절약한 메가바이트가 아니라, 협력자에게 보낼 안내가 한 줄로 끝난다는 점입니다. “TeX Live를 각자 설치해 주세요”가 아니라요.

CI에서 쓸 때: GitHub Actions와 GitLab CI

GitLab CI에서는 작업의 image:에 적기만 하면 되고, GitHub Actions에서는 컨테이너를 직접 지정하거나 xu-cheng/latex-action을 씁니다. 여기서 오해하기 쉬운 점 하나를 짚고 갑니다. xu-cheng/latex-action이 쓰는 것은 texlive/texlive가 아닙니다. 이 액션은 같은 저자의 xu-cheng/latex-docker가 공개하는 ghcr.io/xu-cheng/texlive-alpine이나 ghcr.io/xu-cheng/texlive-debian을 받습니다. os 입력의 기본값이 alpine인 까닭이 여기 있고, texlive_version에는 2020부터 2026, 또는 latest를 줄 수 있습니다. 요컨대 같은 “TeX Live가 든 컨테이너”라도 경로에 따라 속은 다른 물건입니다.

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

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

이미지를 container:에 적는 방식은 로컬의 docker run과 CI가 문자 그대로 같은 환경이 된다는 이점이 있습니다. 반대로 xu-cheng/latex-action을 쓰는 방식은 기술이 짧고 latexmk_use_lualatex, latexmk_shell_escape 같은 입력으로 세밀하게 제어할 수 있다는 이점이 있습니다(기본 latexmk 인자는 -pdf -file-line-error -halt-on-error -interaction=nonstopmode). 선택은 “손안의 환경과의 동일성” 대 “기술의 간결함”의 맞바꿈이며, 재현성이 최우선이라면 전자입니다. 워크플로 구성, 캐시, 산출물 처리 같은 CI 쪽 작법은 CI 페이지가 자세히 다룹니다.

컨테이너에 넘기기 좋은 원고로 만들기

컨테이너에서 넘어지는 원고에는 공통점이 있습니다. 빌드 절차가 편집기 설정에만 적혀 있다는 점입니다. 루트 파일, 엔진, 보조 명령, 출력 위치——이 넷이 저장소 안의 글자로 존재하면 손안에서도 Docker에서도 CI에서도 latexmk main.tex 한 줄로 끝납니다. 일본어에서 upLaTeX과 dvipdfmx를 쓴다면 main.tex 옆에 .latexmkrc를 두고 경로를 명시하십시오. $pdf_mode = 3은 “DVI를 만든 뒤 dvipdfmx로 PDF로 바꾼다”는 지정입니다.

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

.latexmkrc를 커밋해 두면 docker run이든 GitLab CI든 GitHub Actions든 어디서 불러도 같은 규칙으로 PDF가 만들어집니다. 아울러 \usepackage가 실제로 늘었으면 이미지 태그도 다시 본다는 습관을 들이십시오. scheme을 medium에 묶어 둔 채 tikz-cd를 더하면, 어느 날 CI만 ! LaTeX Error: File 'tikz-cd.sty' not found.로 넘어집니다. 원고와 환경을 함께 버전 관리할 것——컨테이너로 조판한다는 것은 결국 그런 약속입니다.