# How to convert Word documents to PDF on a server with LibreOffice headless Install LibreOffice in an isolated sandbox image, upload the file, run `soffice --headless --convert-to pdf`, and download the PDF. **On Runtime the converter image is built once for free, and each conversion pays only for the seconds it runs.** A 2 vCPU, 4 GiB sandbox with both CPUs busy costs $0.08 an hour, and 5,000 thirty-second conversions come to $1.77 at the rates published on 25 September 2026, worked out below. The uploaded file is opened inside a Firecracker microVM with its own kernel and no network, so a hostile document reaches nothing of yours. ## Why convert in a sandbox Users upload DOCX, ODT and RTF files, and agents produce Markdown that has to become a Word file or a web page. The converters that do this well, LibreOffice and pandoc, are large programs that parse complex formats written by strangers. Running them on an application server puts that parsing next to your database credentials and your other customers' files. A sandbox per job, or per batch, puts it somewhere else: a machine with no network card, no credentials and a lease that ends it. ## Build the converter image Ubuntu 24.04 packages both tools. `libreoffice-writer-nogui` is LibreOffice's word processor without a graphical interface (version 24.2.7 in noble), and `pandoc` is 3.1.3 there (Launchpad, read 25 September 2026): ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build( { name: "convert", recipe: { apt: ["libreoffice-writer-nogui", "pandoc"] } }, { onLog: (line) => console.log(line.text) }, ); ``` ```python check from withruntime import Runtime runtime = Runtime() runtime.images.build( name="convert", recipe={"apt": ["libreoffice-writer-nogui", "pandoc"]}, on_log=lambda line: print(line["text"]), ) ``` The build runs in its own Firecracker machine and costs nothing; the stored image is billed on its size at $0.08 per decimal GB per 30-day month, and a free trial keeps its first three images free ([custom images](/docs/images)). ## Convert one document `--headless` runs LibreOffice with no user interface, and `--convert-to pdf` writes the result into `--outdir` (LibreOffice's help, read 25 September 2026). ```ts check import { readFile, writeFile } from "node:fs/promises"; import { Runtime } from "withruntime"; const runtime = new Runtime(); await using sbx = await runtime.sandboxes.create({ image: "convert", network: { internet: false }, timeoutSeconds: 600, onLeaseEnd: "stop", }); await sbx.files.write("/workspace/in/contract.docx", await readFile("contract.docx")); const run = await sbx.exec( [ "soffice", "--headless", "--convert-to", "pdf", "--outdir", "/workspace/out", "/workspace/in/contract.docx", ], { timeoutMs: 180_000 }, ); if (run.exitCode !== 0 || run.timedOut) throw new Error(run.stderr); await writeFile("contract.pdf", await sbx.files.read("/workspace/out/contract.pdf")); ``` ```python check from withruntime import Runtime runtime = Runtime() with runtime.sandboxes.create( image="convert", network={"internet": False}, timeout_seconds=600, on_lease_end="stop", ) as sbx: with open("contract.docx", "rb") as source: sbx.files.write("/workspace/in/contract.docx", source.read()) run = sbx.exec( ["soffice", "--headless", "--convert-to", "pdf", "--outdir", "/workspace/out", "/workspace/in/contract.docx"], timeout_ms=180_000, ) if run.exit_code != 0 or run.timed_out: raise RuntimeError(run.stderr) with open("contract.pdf", "wb") as target: target.write(sbx.files.read("/workspace/out/contract.pdf")) ``` The command is an array, so a file name with spaces or quotes reaches LibreOffice as one argument and never passes through a shell. Treat the uploaded name as data: store it under a name you choose, as above. ## Convert a folder at once Starting LibreOffice once for many files is faster than once per file. Upload the folder in one call, convert every file with a glob, and bring the results back in one call: ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ image: "convert", network: { internet: false } }); await sbx.files.upload("./incoming", "/workspace/in"); await sbx.exec("soffice --headless --convert-to pdf --outdir /workspace/out /workspace/in/*.docx", { timeoutMs: 1_800_000, }); console.log(await sbx.files.glob("out/*.pdf")); await sbx.files.download("/workspace/out", "./converted"); ``` Directories travel as one compressed archive each way, and uploaded files keep their permissions. ## Markdown from an agent to DOCX or HTML pandoc converts between markup formats; `-f` and `-t` name them and `-o` names the output file. Its PDF output goes through LaTeX by default, which needs a LaTeX engine installed, so for PDF it is simpler to make DOCX with pandoc and hand that to LibreOffice: ```bash no-run pandoc -f markdown -t docx -o report.docx report.md soffice --headless --convert-to pdf report.docx ``` ## What a conversion service needs | Need | How Runtime covers it | | ------------------------------------- | ----------------------------------------------------------------- | | Isolation from hostile files | A Firecracker microVM with its own kernel per sandbox | | A document that phones home | `network: { internet: false }`, enforced outside the guest | | LibreOffice and pandoc ready at start | A custom image from a recipe, a Dockerfile or any container image | | A conversion that hangs | `timeoutMs` on the command; the lease ends the sandbox | | Big files | Chunked, SHA-256 checked uploads with no limit beyond the disk | | Bursts of uploads | Creates wait for room rather than failing, for up to two minutes | | Pinning a converter version | `image: "convert@3"` starts from exactly that build | ## What it costs Take 5,000 documents a month, each converted in its own 2 vCPU, 4 GiB sandbox that runs for 30 seconds with 15 CPU-seconds of work: ``` CPU: 5,000 × 15 s / 3,600 × $0.025 = $0.52 Memory: 5,000 × 30 s / 3,600 × 4 GiB × $0.0075 = $1.25 Total: $1.77 ``` Converting in batches of 50 per sandbox cuts the memory line further, since LibreOffice starts once per batch. Runtime bills $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory, with no plan fee ([pricing](/docs/pricing)). The trial's 50 free hours need no card. ## Sources - [LibreOffice command line parameters](https://help.libreoffice.org/latest/en-US/text/shared/guide/start_parameters.html): `--headless`, `--convert-to` and `--outdir`, read 25 September 2026. - [Pandoc user's guide](https://pandoc.org/MANUAL.html): formats, `-o`, and PDF through LaTeX, read 25 September 2026. - Launchpad package pages for [libreoffice-writer-nogui](https://launchpad.net/ubuntu/noble/+package/libreoffice-writer-nogui) and [pandoc](https://launchpad.net/ubuntu/noble/+package/pandoc) in Ubuntu 24.04, read 25 September 2026. Related: [PDF processing](/use-cases/pdf-processing), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code), [turn off a sandbox's internet](/how-to/turn-off-sandbox-internet), [Docker vs a virtual machine](/compare/docker-vs-virtual-machine). Facts on this page were checked on 25 September 2026.