Skip to content
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, and you can cut off its internet access entirely. Conversion is short CPU work in bursts, and Runtime bills the CPU used: a 2 vCPU, 4 GiB sandbox costs $0.08 an hour at full load and $0.03125 while it waits for the next file (pricing). Ubuntu 24.04's updates carried LibreOffice 24.2.7 on 25 October 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 substitutes for the fonts that Office documents usually name. Lines therefore 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 Ubuntu's package archive, read 25 October 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 goes to 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 conversion its own profile 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. After its first top-up, a paid account runs 100 sandboxes at the same time, and an account without credit runs four.

Start every sandbox with LibreOffice installed

This is the office image that the sample above uses. It is a custom image. The recipe's packages install once, at build time. If a larger set needs more room, the build machine's scratch disk can be raised to 32 GiB.

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. Without credit, the build counts toward its hours. A stored image is charged by its size. An account without credit stores three images free.

Versions and limits

  • Versions. 24.2.7 in noble-updates; Ubuntu's noble-backports pocket carried 26.2.5 on 25 October 2026, per Launchpad.
  • Disk. The default 4 GiB disk had about 2.5 GiB free on 24 September 2026. Request diskMiB: 8192 when you install. Without credit, the disk can be up to 10 GiB.
  • Time. Set timeoutMs for installs and large decks. A command's default timeout is 60 seconds.
  • Network. Calling 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 a sandbox without credit 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 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