# 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](/docs/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 ```ts check import { 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 internet await 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")); ``` ```python check from withruntime import Sandbox PACKAGES = ("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`: ```ts check import { 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](/docs/images). 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. ```ts check import { 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 }, }); ``` ```python check from withruntime import Runtime runtime = 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](/integrations/pandoc). To pull text out of scanned PDFs, see [Tesseract OCR in a sandbox](/integrations/tesseract). For why a microVM is the boundary here, see [microVM vs container](/compare/microvm-vs-container). ## Sources - LibreOffice help, command-line parameters, https://help.libreoffice.org/latest/en-US/text/shared/guide/start_parameters.html, read 25 October 2026 - Ubuntu 24.04 packages (libreoffice-*-nogui, fonts-liberation, fonts-crosextra-carlito, fonts-crosextra-caladea), from the noble package indexes at http://archive.ubuntu.com/ubuntu/dists/, read 25 October 2026 - Launchpad, published libreoffice sources for noble, https://launchpad.net/ubuntu/+source/libreoffice, read 25 October 2026 Facts on this page were checked on 25 October 2026.