Runtime

How to convert Markdown to PDF or Word with Pandoc in a sandbox

Install pandoc and a PDF engine in a microVM, run pandoc in.md -o out.pdf or out.docx with --sandbox, and download the file.

On Runtime, model-written Markdown becomes a PDF or a Word file on a machine that can read nothing but that Markdown. Each sandbox is a Firecracker microVM with its own kernel and disk, and pandoc's own --sandbox flag narrows it further to the files named on the command line. Runtime bills the CPU a render uses, not the CPU it holds: a 2 vCPU, 4 GiB sandbox costs $0.03125 an hour between documents and $0.08 with both CPUs busy (pricing).

Which pandoc

Source Version on 25 September 2026 Install
Ubuntu 24.04 package pandoc 3.1.3 sudo apt-get install -y pandoc
pandoc's GitHub release, 29 August 2026 3.11 pandoc-3.11-1-amd64.deb, then sudo dpkg -i

pandoc's install page recommends the release .deb for Linux. It downloads from GitHub over HTTPS, which the free trial reaches.

Choose the PDF engine

pandoc makes a PDF through another program. Its manual names pdflatex as the default and lists lualatex, xelatex, latexmk, tectonic, wkhtmltopdf, weasyprint, pagedjs-cli, prince, context, groff, pdfroff and typst. The ones Ubuntu 24.04 packages:

Engine Ubuntu package(s) Good for
pdflatex texlive-latex-recommended, texlive-fonts-recommended, lmodern Papers, maths, the classic LaTeX look
xelatex texlive-xetex System fonts and non-Latin scripts
weasyprint weasyprint (61.1) Styling a PDF with CSS, a small install
wkhtmltopdf wkhtmltopdf (0.12.6) HTML-first layouts

TeX Live packages are 2023.20240207 in noble. Typst is not in Ubuntu's archive for 24.04.

Markdown to PDF and DOCX

TypeScriptimport { writeFile } from "node:fs/promises";import { Sandbox } from "withruntime";const markdown =  "# Quarterly report\n\nRevenue rose **12%**.\n\n| Region | Sales |\n|---|---|\n| West | 120 |\n";await using sbx = await Sandbox.create({ diskMiB: 6144, timeoutSeconds: 900 });await sbx.exec(  "curl -fsSLO https://github.com/jgm/pandoc/releases/download/3.11/pandoc-3.11-1-amd64.deb" +    " && sudo dpkg -i pandoc-3.11-1-amd64.deb" +    " && sudo apt-get update -qq && sudo apt-get install -y -qq weasyprint",  { check: true, timeoutMs: 600_000 },);await sbx.files.write("/workspace/report.md", markdown);await sbx.files.upload("./report.css", "/workspace/report.css");await sbx.exec(  "pandoc --sandbox report.md -s --css report.css --pdf-engine=weasyprint -o report.pdf",  { check: true, timeoutMs: 120_000 },);await sbx.exec("pandoc --sandbox report.md -o report.docx", { check: true });await writeFile("report.pdf", await sbx.files.read("/workspace/report.pdf"));await writeFile("report.docx", await sbx.files.read("/workspace/report.docx"));
Pythonfrom withruntime import Sandboxmarkdown = "# Quarterly report\n\nRevenue rose **12%**.\n"with Sandbox.create(disk_mib=6144, timeout_seconds=900) as sbx:    sbx.exec("curl -fsSLO https://github.com/jgm/pandoc/releases/download/3.11/pandoc-3.11-1-amd64.deb"             " && sudo dpkg -i pandoc-3.11-1-amd64.deb"             " && sudo apt-get update -qq && sudo apt-get install -y -qq weasyprint",             check=True, timeout_ms=600_000)    sbx.files.write("/workspace/report.md", markdown)    sbx.files.upload("report.css", "/workspace/report.css")    sbx.exec("pandoc --sandbox report.md -s --css report.css --pdf-engine=weasyprint -o report.pdf",             check=True, timeout_ms=120_000)    sbx.exec("pandoc --sandbox report.md -o report.docx", check=True)    sbx.files.download("/workspace/report.pdf", "report.pdf")    sbx.files.download("/workspace/report.docx", "report.docx")

pandoc picks the output format from the file extension, and .docx needs no extra program.

Why --sandbox

Markdown can pull in other files, through an image path or an include. When the text comes from a model or a user, that is a way to read files it should not. pandoc's manual describes --sandbox as limiting input and output in readers and writers "to reading the files specified on the command line", which it offers "as security against disclosure of files through the use of include directives".

In a Runtime sandbox the two layers stack: pandoc reads only the files you named, and the machine it runs on holds only the files you uploaded. Turn the internet off after the install too, so nothing a document references can be fetched (turn off sandbox internet).

House style for Word output

--reference-doc takes a .docx whose styles, margins, page size, header and footer pandoc copies into the output, per its manual. Its contents are ignored. Start from one pandoc wrote, edit the styles in Word, and upload it:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "pandoc" });await sbx.files.upload("./house-style.docx", "/workspace/house-style.docx");await sbx.files.upload("./memo.md", "/workspace/memo.md");await sbx.exec("pandoc --sandbox memo.md --reference-doc=house-style.docx -o memo.docx", {  check: true,});await sbx.files.download("/workspace/memo.docx", "./memo.docx");

To make the starting file: pandoc -o custom-reference.docx --print-default-data-file reference.docx.

Start every sandbox with pandoc installed

The pandoc image the last sample uses, as a custom image. Recipe commands run as root, so they need no sudo:

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({  name: "pandoc",  recipe: {    apt: ["weasyprint"],    commands: [      "curl -fsSLO https://github.com/jgm/pandoc/releases/download/3.11/pandoc-3.11-1-amd64.deb",      "dpkg -i pandoc-3.11-1-amd64.deb && rm pandoc-3.11-1-amd64.deb",    ],  },});
Pythonfrom withruntime import RuntimeRuntime().images.build(name="pandoc", recipe={    "apt": ["weasyprint"],    "commands": [        "curl -fsSLO https://github.com/jgm/pandoc/releases/download/3.11/pandoc-3.11-1-amd64.deb",        "dpkg -i pandoc-3.11-1-amd64.deb && rm pandoc-3.11-1-amd64.deb",    ],})

For LaTeX output, add the TeX Live packages from the engine table to apt and raise the build machine's scratch disk with build: { diskMiB: 8192 }; it takes 1 to 32 GiB. Building is free, and the trial stores three images free.

Limits that apply

  • Disk. The default 4 GiB disk had about 2.5 GiB free on 24 September 2026. WeasyPrint fits; TeX Live wants diskMiB: 8192, within the trial's 10 GiB.
  • Output in the result. pandoc -t plain or -t html to stdout is kept up to 64 KiB in the command result; write longer output to a file.
  • Fonts. A PDF uses the fonts installed in the sandbox. Add the font packages your CSS or LaTeX template names.

To go from Word, Excel or PowerPoint to PDF instead, see LibreOffice in a sandbox. For a chat product that returns generated files, see a code interpreter for chatbots.

Sources

Facts on this page were checked on 25 September 2026.