Open source · Self-hosted · DOCX → PDF

DOCX in.
PDF out.

DocMorph is a stateless HTTP microservice that converts DOCX documents to PDF using headless LibreOffice. Docker-ready, concurrency-safe and built to do one thing well.

MIT LicenseOne endpointNode ≥ 22Docker-ready

01 / The transformation

A document enters. A PDF comes out.

Nothing else gets in the way. Four deterministic stages, no hidden state, no queues you have to operate.

Validate

The extension is checked, then the upload is read for the OOXML ZIP signature. Bad files never reach the converter.

.docx · PK\x03\x04

Isolate

Every job gets its own temp directory and a dedicated LibreOffice user profile, so concurrent conversions never share state.

/tmp/lo-<uuid>/profile

Convert

soffice runs headless under a per-conversion timeout, while a semaphore bounds how many jobs run at the same time.

soffice --headless --convert-to pdf

Verify

The result must begin with the PDF header. Only then is it streamed back as application/pdf.

%PDF-1.7

02 / Small by design

DocMorph does less. On purpose.

A small surface is easier to trust. Every feature DocMorph does not have is a thing that cannot break.

No database

Nothing to migrate, back up, or keep in sync. State lives in the request and ends with the response.

No job history

The request is the job. Success or failure, it is over as soon as the HTTP response is written.

No accounts

Optional API keys from a comma-separated environment variable. That is the entire user model.

No dashboard

One public /health endpoint reports pool load, limits and auth. Operate it with the tools you already have.

03 / Guardrails

Small API. Serious guardrails.

Every request is checked, bounded and timed. Failures are explicit, never silent.

Extension check

Only .docx is accepted. Anything else is rejected up front.

endsWith(".docx")

OOXML magic bytes

The ZIP/OOXML signature is verified — not just the file name.

PK\x03\x04

Size limit

Uploads are capped before they reach the converter.

MAX_FILE_SIZE

PDF output assert

The generated file must start with the PDF header before it is streamed.

%PDF

Optional API key

When API_KEYS is set, every route except /health requires a key.

x-api-key

Per-IP rate limit

A fixed window per client IP. /health is excluded so probes stay cheap.

60 / min

Conversion timeout

A stalled LibreOffice process is killed instead of holding a slot forever.

CONVERT_TIMEOUT_MS
10 MiB
max input
60 s
conversion timeout
3
concurrent jobs
60 / min
requests per IP

Defaults shown. Every value is an environment variable — override it on the command line or in your compose file.

04 / API in action

The API is intentionally boring.

If you can POST a file, you can use DocMorph. One multipart field, one PDF back.

POST/convertfield: file
curl
curl -o out.pdf \
  -H "x-api-key: your-secret-key" \
  -F "file=@document.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
  http://localhost:8080/convert
GET/healthpublic probe
POST /convert error responses
StatusWhat it means
401Missing or invalid API key
412Missing file field or unsupported extension
415File content is not a valid .docx (magic-byte check)
429Rate limit exceeded (Retry-After + RateLimit-* headers)
500Conversion failed

Successful responses carry Content-Type: application/pdf and an inline filename sanitized to [A-Za-z0-9._-].

05 / Operability

Built to be operated.

Know what the service is doing without guessing.

  • health

    GET /health is always public and excluded from rate limiting, so Docker and load-balancer probes never need credentials.

  • pool

    Active, queued and maximum concurrency are reported live, straight from the semaphore.

  • limits

    Max file size, rate-limit window and conversion timeout travel in the same payload.

  • logs

    Request-scoped key=value lines go to stdout and stderr. Pipe them anywhere.

GET /health · 200 public
{
"status":"ok",
"authEnabled":true,
"concurrency":{
"active":0,
"queued":0,
"max":3
},
"maxFileSize":10485760,
"rateLimit":{
"max":60,
"windowMs":60000
},
"timeoutMs":60000
}

06 / Self-host it

One container. Your infrastructure.

Your documents stay inside your network. The service stays out of your way.

  1. Pull the image

    ghcr.io/darcas/docmorph:latest is public. No build step, no toolchain, no fonts to install.

  2. Set a key — optional

    Pass API_KEYS to require x-api-key or Authorization: Bearer on every route except /health. Leave it unset for open access.

  3. Publish a port

    Map container port 8080 wherever your infrastructure expects it.

  4. Point your backend at it

    POST a .docx to /convert and stream the PDF back. That is the whole integration.

docker run
docker run -d --name docmorph \
  -p 8080:8080 \
  -e API_KEYS="your-secret-key" \
  ghcr.io/darcas/docmorph:latest
docker-compose.yml
name: docmorph

services:
  api:
    image: ghcr.io/darcas/docmorph:latest
    restart: unless-stopped
    environment:
      API_KEYS: "your-secret-key"
      MAX_CONCURRENT: "3"
    ports:
      - "8080:8080"

Ship the endpoint

Give your backend a DOCX → PDF endpoint.

Clone it, run it in a container, and let it do exactly what it says on the box.