How to Batch Convert DOCX to PDF with LibreOffice Headless
Updated October 27, 2026 .md
Short answer: Install the no-GUI LibreOffice packages with apt, then pass all your files to one soffice --headless --convert-to pdf call. In our test, 50 DOCX files took 6 seconds that way, 51 seconds as one call per file, and 2.6 seconds split across 4 processes, each with its own profile.
Every number on this page comes from one default sandbox (4 vCPU, 8 GB) on prod on 2026-10-27. The test files were 50 generated invoices: 3 pages each, 12 paragraphs, a 21-row table, Calibri as the body font.
Results at a glance#
| Method | 50 DOCX files to PDF | Per file |
|---|---|---|
One soffice call per file, in a loop |
50.9 s to 63.9 s | about 1 s |
One soffice call with all 50 files |
5.8 s to 6.3 s | 0.12 s |
4 soffice calls at once, separate profiles |
2.6 s to 2.7 s | 0.05 s |
| Gotenberg over a private HTTPS URL, 1 file | 0.5 s to 1.3 s per request | |
| Gotenberg over HTTPS, 19 files in one request | 5.5 s (zip back) | 0.29 s |
Each range is two runs. The loop is slow because every call starts LibreOffice again. Starting it once and feeding it the whole batch is 8 to 10 times faster.
1. Install LibreOffice without the desktop#
LibreOffice is not in the base image. Ubuntu 24.04 ships -nogui builds of each LibreOffice app, which skip the desktop UI. Add poppler-utils for pdfinfo and pdffonts, which you use to check the output.
sbx new --type default --name docs-convert
id=sb_imhk8ycbqk # the id that sbx new printed
sbx exec $id --timeout 600 "sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends libreoffice-writer-nogui libreoffice-calc-nogui libreoffice-impress-nogui poppler-utils && soffice --version"
# LibreOffice 24.2.7.2 420(Build:2)
# real 0m10.684sThe install took under 11 seconds and used about 400 MB of disk (/ went from 5.5G to 5.9G used). Writer handles DOCX, Calc handles XLSX and Impress handles PPTX. Leave out the ones you don't need.
2. Convert one file, then check the fonts#
The commands in steps 2 to 4, and the docker and curl localhost lines in step 5, run inside the sandbox, for example in an sbx ssh $id session.
cd ~/lo
soffice --headless --convert-to pdf --outdir first docs/invoice-001.docx
# convert /home/user/lo/docs/invoice-001.docx as a Writer document -> /home/user/lo/first/invoice-001.pdf using filter : writer_pdf_Export
# real 0m1.109s
pdfinfo first/invoice-001.pdf | grep Pages
# Pages: 3
pdffonts first/invoice-001.pdf | tail -n +3
# BAAAAA+DejaVuSans TrueType WinAnsi yes yes yes 219 0The PDF is valid, but the font is wrong. The documents ask for Calibri, which isn't installed, so LibreOffice fell back to DejaVu Sans. DejaVu is wider, so line breaks and page counts can drift from what the author saw in Word.
Install the metric-compatible fonts. Carlito matches Calibri's widths and Caladea matches Cambria's:
fc-match Calibri
# DejaVuSans.ttf: "DejaVu Sans" "Book"
sudo apt-get install -y -qq fonts-crosextra-carlito fonts-crosextra-caladea
fc-match Calibri
# Carlito-Regular.ttf: "Carlito" "Regular"
soffice --headless --convert-to pdf --outdir first docs/invoice-001.docx
pdffonts first/invoice-001.pdf | tail -n +3
# BAAAAA+Carlito-Regular TrueType WinAnsi yes yes yes 219 0Run pdffonts on a sample of your real documents before a big batch. Any font it lists that isn't the one the document asked for is a substitution.
You will also see Warning: failed to launch javaldx - java may not function correctly on every run. It is harmless for conversions: we skipped Java on purpose with --no-install-recommends.
3. Convert a whole folder in one call#
Pass every file to a single soffice. It starts once and converts them in order:
soffice --headless --convert-to pdf --outdir out-batch docs/*.docx
ls out-batch | wc -l
# 50That took 5.8 to 6.3 seconds for 50 files, against 51 to 64 seconds for a for loop that called soffice once per file.
4. Run several converters at once, each with its own profile#
soffice keeps its settings in a profile directory (~/.config/libreoffice by default), and only one process can use a profile at a time. We started two batches at once with the default profile:
(soffice --headless --convert-to pdf --outdir clash docs/invoice-00[1-5].docx; echo "a exit=$?") &
(soffice --headless --convert-to pdf --outdir clash docs/invoice-01[0-4].docx; echo "b exit=$?") &
wait
# b exit=1
# a exit=0
ls clash/*.pdf | wc -l
# 5The second process exited with status 1, printed nothing but the Java warning and converted nothing. Five of the ten files are missing. If you parallelize without separate profiles, check exit codes and file counts, or you will lose files quietly.
The fix is -env:UserInstallation, which gives each process its own profile. This splits the 50 files into 4 groups, one per vCPU:
ls docs/*.docx | split -n r/4 -d - /tmp/chunk.
for i in 0 1 2 3; do
soffice -env:UserInstallation=file:///tmp/lo-profile-$i \
--headless --convert-to pdf --outdir out-par $(cat /tmp/chunk.0$i) &
done
wait
ls out-par | wc -l
# 50All 50 PDFs arrived in 2.6 to 2.7 seconds, about 2.3 times faster than one batch call. Match the number of groups to the sandbox's vCPUs (nproc). For thousands of files, start the script with sbx exec --detach and check on it later, as in run a long job in a sandbox.
5. Serve conversions over HTTPS with Gotenberg#
If other services need to send documents in, run Gotenberg, an open-source HTTP API around LibreOffice and Chromium, as a container. Docker is already in the sandbox. --restart unless-stopped tells Docker to start the container again whenever the Docker daemon starts.
docker run -d --name gotenberg --restart unless-stopped -p 3000:3000 gotenberg/gotenberg:8
curl -s localhost:3000/health
# {"status":"up","details":{"chromium":{"status":"up",…},"libreoffice":{"status":"up",…}}}The pull took 20 seconds and the image is 1.7 GB. It brings its own LibreOffice (26.8.1.1 on the day we tested), separate from the apt one.
Give the port a private HTTPS URL and call the LibreOffice route from your laptop:
sbx host $id 3000
# "url":"https://sharp-fancy-plum-3000.box.sandboxer.com?_token=…","access":"private"
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST \
https://sharp-fancy-plum-3000.box.sandboxer.com/forms/libreoffice/convert \
-F files=@invoice-007.docx
# HTTP 401
TOKEN=… # the _token value from the URL above; keep it out of logs
curl -s -o inv7.pdf -w "HTTP %{http_code} %{size_download} bytes %{time_total}s\n" -X POST \
"https://sharp-fancy-plum-3000.box.sandboxer.com/forms/libreoffice/convert?_token=$TOKEN" \
-F files=@invoice-007.docx
# HTTP 200 21720 bytes 1.330897s
# HTTP 200 21720 bytes 0.641700s
# HTTP 200 21720 bytes 0.503791sWithout the token the proxy answers 401 before Gotenberg sees the request. The first converted request took 1.3 seconds, later ones about 0.5 seconds, round trip from our test machine.
Send several files fields in one request to get a zip of PDFs. Our 19 files came back in 5.5 seconds as a zip named invoice-001.docx.pdf and so on. Add -F merge=true to get one merged PDF instead: 57 pages in 5.6 seconds.
The private URL's token works like a password. Keep it in a secret store and out of logs.
What survives a stop and resume#
We ran sbx stop $id --wait (2.2 seconds), then sbx resume $id (1.4 seconds). The resume reported "memoryRestored": true, so the Gotenberg container was still running. The apt packages, fonts and all 50 PDFs were still on disk. The next HTTPS conversion returned 200 in 0.67 seconds.
When a resume can't restore memory, for example on a host with a different CPU type, the sandbox boots from disk. Your files and packages are kept, and running processes are not. The Docker daemon starts at boot, and the --restart unless-stopped policy then brings Gotenberg back. A plain soffice batch that was mid-run would need starting again, so write your batch script so it skips PDFs that already exist.
FAQ#
Why does LibreOffice headless exit with status 1 and convert nothing?#
Usually another soffice process is already using the same profile. That includes a hung earlier run. Give each concurrent process its own -env:UserInstallation=file:///tmp/lo-profile-N, or check for a leftover soffice.bin with pgrep soffice.
Why does my PDF look different from Word?#
The document uses fonts the sandbox doesn't have, so LibreOffice substitutes them. pdffonts shows what was used. Install fonts-crosextra-carlito and fonts-crosextra-caladea for Calibri and Cambria, and add any other fonts your documents need to ~/.fonts or /usr/share/fonts.
Which sandbox size should I use?#
The batch numbers above are from default, with 4 vCPU. small has 2 vCPU, so run 2 parallel groups instead of 4. Bigger sizes give more vCPUs for more parallel groups; see Machines & sizes.
Should I use soffice directly or Gotenberg?#
Use soffice for batch jobs inside the sandbox; it was faster in our test and needs no container. Use Gotenberg when other programs need to send documents over HTTP, or when you want merging and a health check without writing them yourself.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.