Runtime

How to convert DOCX, XLSX and PPTX to PDF with LibreOffice in a sandbox

Install the LibreOffice -nogui packages in a microVM, run soffice --headless --convert-to pdf, and download the PDF from --outdir.

On Runtime, documents from strangers are opened on a machine that exists only for them. An uploaded file is untrusted input, and every sandbox is a Firecracker microVM with its own kernel and no route to your network, which you can also cut off from the internet entirely. Conversion is short, bursty CPU work, and Runtime bills the CPU used: a 2 vCPU, 4 GiB sandbox costs $0.08 an hour flat out and $0.03125 while it waits for the next file (pricing). Ubuntu 24.04's updates carried LibreOffice 24.2.7 on 25 September 2026.

Pick the packages

Ubuntu builds each LibreOffice program in a variant without a graphical interface. Install only the ones for the formats you convert:

Converting from Package
.docx, .doc, .odt, .rtf libreoffice-writer-nogui
.xlsx, .xls, .ods, .csv libreoffice-calc-nogui
.pptx, .ppt, .odp libreoffice-impress-nogui
All of them libreoffice-nogui

Fonts decide whether a converted page looks like the original. These Ubuntu packages are metric-compatible stand-ins for the fonts Office documents usually name, so lines break where the author saw them:

Package Stands in for
fonts-liberation Times New Roman, Arial, Courier
fonts-crosextra-carlito Calibri
fonts-crosextra-caladea Cambria

Descriptions and versions are from packages.ubuntu.com, read 25 September 2026.

Convert a document

TypeScriptimport { writeFile } from "node:fs/promises";import { Sandbox } from "withruntime";const packages =  "libreoffice-writer-nogui fonts-liberation fonts-crosextra-carlito fonts-crosextra-caladea";await using sbx = await Sandbox.create({ diskMiB: 8192, timeoutSeconds: 1800 });await sbx.exec(`sudo apt-get update -qq && sudo apt-get install -y -qq ${packages}`, {  check: true,  timeoutMs: 900_000,});await sbx.network.off(); // nothing the document triggers can reach the internetawait sbx.files.upload("./contract.docx", "/workspace/in/contract.docx");await sbx.exec(  [    "soffice",    "--headless",    "--convert-to",    "pdf",    "--outdir",    "/workspace/out",    "/workspace/in/contract.docx",  ],  { check: true, timeoutMs: 300_000 },);await writeFile("contract.pdf", await sbx.files.read("/workspace/out/contract.pdf"));
Pythonfrom withruntime import SandboxPACKAGES = ("libreoffice-writer-nogui fonts-liberation "            "fonts-crosextra-carlito fonts-crosextra-caladea")with Sandbox.create(disk_mib=8192, timeout_seconds=1800) as sbx:    sbx.exec(f"sudo apt-get update -qq && sudo apt-get install -y -qq {PACKAGES}",             check=True, timeout_ms=900_000)    sbx.network.off()  # nothing the document triggers can reach the internet    sbx.files.upload("contract.docx", "/workspace/in/contract.docx")    sbx.exec(["soffice", "--headless", "--convert-to", "pdf", "--outdir", "/workspace/out",              "/workspace/in/contract.docx"], check=True, timeout_ms=300_000)    sbx.files.download("/workspace/out/contract.pdf", "contract.pdf")

The output keeps the input's name with the new extension. Without --outdir, LibreOffice writes to the working directory, which in a sandbox is /workspace.

Other conversions from the same command

LibreOffice's command-line guide gives the format as extension[:filter[:options]]:

Command Result
soffice --headless --convert-to pdf *.docx --outdir out Every Word file in the folder to PDF
soffice --headless --convert-to pdf:writer_pdf_Export in.doc PDF with the Writer export filter named
soffice --headless --convert-to docx in.doc An old .doc to .docx
soffice --headless --convert-to csv in.xlsx A spreadsheet to CSV
soffice --cat in.docx The document's text on stdout, for a model to read

--cat implies headless mode and cannot be combined with --convert-to. Its output lands in the command result, which keeps up to 64 KiB of stdout; redirect longer documents to a file and read it with files.readText.

Convert many files side by side

LibreOffice keeps its settings in a user profile. To run conversions in parallel inside one sandbox, give each its own with -env:UserInstallation:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "office", vcpu: 2, diskMiB: 8192 });await sbx.files.upload("./inbox", "/workspace/in");const inputs = (await sbx.files.glob("in/*.docx")).map((entry) => entry.path);await Promise.all(  inputs.map((input, i) =>    sbx.exec(      [        "soffice",        `-env:UserInstallation=file:///tmp/lo-${i}`,        "--headless",        "--convert-to",        "pdf",        "--outdir",        "/workspace/out",        input,      ],      { check: true, timeoutMs: 600_000 },    ),  ),);await sbx.files.download("/workspace/out", "./pdf");

The whole out folder comes back as one compressed archive. For thousands of files, spread them across sandboxes instead: a paid account runs 100 at once to start with, and the free trial eight.

Start every sandbox with LibreOffice installed

The office image the sample above uses, as a custom image. Recipe packages install once, at build time; the build machine's scratch disk can be raised to 32 GiB if a larger set needs room.

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({  name: "office",  recipe: {    apt: [      "libreoffice-writer-nogui",      "libreoffice-calc-nogui",      "libreoffice-impress-nogui",      "fonts-liberation",      "fonts-crosextra-carlito",      "fonts-crosextra-caladea",    ],  },  build: { diskMiB: 8192 },});
Pythonfrom withruntime import Runtimeruntime = Runtime()runtime.images.build(name="office", recipe={"apt": [    "libreoffice-writer-nogui", "libreoffice-calc-nogui", "libreoffice-impress-nogui",    "fonts-liberation", "fonts-crosextra-carlito", "fonts-crosextra-caladea",]}, build={"disk_mib": 8192})

Building an image is free and uses no trial hours. A stored image is charged on its size, and the free trial stores three free.

Versions and limits

  • Versions. 24.2.7 in noble-updates; Ubuntu's noble-backports pocket carried 26.2.5 on 25 September 2026, per Launchpad.
  • Disk. The default 4 GiB disk had about 2.5 GiB free on 24 September 2026; ask for diskMiB: 8192 when installing, up to 10 GiB on the trial.
  • Time. Give installs and large decks timeoutMs; a command's default is 60 seconds.
  • Network. network.off() after the install is the one line that keeps a hostile document from calling home. Downloads during the install use port 443, which the trial reaches.

To go the other way, from Markdown to Word or PDF, see Pandoc in a sandbox. To pull text out of scanned PDFs, see Tesseract OCR in a sandbox. For why a microVM is the boundary here, see microVM vs container.

Sources

Facts on this page were checked on 25 September 2026.