Skip to content
Runtime

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. 5,000 thirty-second conversions come to $1.77 at the rates published on 26 October 2026, as worked out below. The uploaded file is opened in a Firecracker microVM with its own kernel and no network, so a hostile document cannot reach anything 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, moves that work somewhere else: a machine with no network card, no credentials and a time limit that stops 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 26 October 2026):

TypeScriptimport { 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) },);
Pythonfrom withruntime import Runtimeruntime = 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 an account without credit keeps its first three images free (custom images).

Convert one document

--headless runs LibreOffice with no user interface, and --convert-to pdf writes the result into --outdir (LibreOffice's help, read 26 October 2026).

TypeScriptimport { 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"));
Pythonfrom withruntime import Runtimeruntime = 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 starting it once per file. Upload the folder in one call, convert every file with a glob, and download the results in one call:

TypeScriptimport { 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 the input and output formats, and -o names the output file. Its PDF output goes through LaTeX by default, which needs a LaTeX engine installed. For PDF, it is simpler to make DOCX with pandoc and pass that file to LibreOffice:

Terminalpandoc -f markdown -t docx -o report.docx report.mdsoffice --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 calls out to the internet 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; timeoutSeconds 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, because 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). A new account gets 100 free hours of a 1 GB machine in its first 30 days without a card.

Sources

Related: PDF processing, run untrusted LLM code safely, turn off a sandbox's internet, Docker vs a virtual machine.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card