# How to Batch Convert DOCX to PDF with LibreOffice Headless

> 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.

Source: https://sandboxer.com/guides/convert-documents-with-libreoffice-headless. Updated 2026-10-27.

**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](/guides/whats-preinstalled-in-a-sandbox). 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.

```bash
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.684s
```

The 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.

```bash
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  0
```

The 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:

```bash
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  0
```

Run `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:

```bash
soffice --headless --convert-to pdf --outdir out-batch docs/*.docx
ls out-batch | wc -l
# 50
```

That 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:

```bash
(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
# 5
```

The 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:

```bash
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
# 50
```

All 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](/guides/run-a-long-job-in-a-cloud-sandbox).

## 5. Serve conversions over HTTPS with Gotenberg

If other services need to send documents in, run [Gotenberg](https://gotenberg.dev/docs/getting-started/introduction), 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.

```bash
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](/docs/hosting) and call the [LibreOffice route](https://gotenberg.dev/docs/convert-with-libreoffice/convert-to-pdf) from your laptop:

```bash
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.503791s
```

Without 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](/docs/machines).

### 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

- [Move files in and out of a sandbox](/guides/move-files-in-and-out-of-a-sandbox)
- [Run a long job in a cloud sandbox and check on it later](/guides/run-a-long-job-in-a-cloud-sandbox)
- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Hosting docs](/docs/hosting)
