# 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](/docs/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 ```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 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`: ```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: 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](/docs/images). 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. ```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 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](/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 September 2026 - Ubuntu 24.04 package pages (libreoffice-*-nogui, fonts-liberation, fonts-crosextra-carlito, fonts-crosextra-caladea), https://packages.ubuntu.com/noble/, read 25 September 2026 - Launchpad, published libreoffice sources for noble, https://launchpad.net/ubuntu/+source/libreoffice, read 25 September 2026 Facts on this page were checked on 25 September 2026.