python docx converter API service with template function of docxtpl to output rendered pdfs
450
Generates DOCX and PDF documents by merging JSON data into a DOCX template.
Payload values are plain JSON — the template decides presentation: literal text, markdown rendered as real Word formatting, or a QR code. Pandoc is baked into the image; PDF output additionally needs a reachable Collabora Online instance.
linux/amd64 and linux/arm64. Runs as nobody, listens on 8000, writes only to /tmp.
docker run -p 8000:8000 NAMESPACE/docx2pdf:2.1.0
Interactive API docs: http://localhost:8000/docs — health check: /health.
DOCX generation works out of the box. For PDF, point the service at Collabora:
services:
app:
image: NAMESPACE/docx2pdf:2.1.0
ports: ["8000:8000"]
environment:
- COLLABORA_URL=http://collabora:9980/cool/convert-to/pdf
depends_on: [collabora]
collabora:
image: collabora/code:latest
environment:
- aliasgroup1=http://app:8000
- DONT_GEN_SSL_CERT=true
- extra_params=--o:ssl.enable=false
cap_add: [MKNOD]
curl -X POST http://localhost:8000/generate \
-F 'template=@your_template.docx' \
-F 'data={
"name": "John Doe",
"date": "2026-03-02",
"case_ref": "AB-1234",
"biography": "# Biography\nJohn is a **software engineer**."
}' \
-F 'output_format=pdf' \
--output generated.pdf
output_format is docx (default) or pdf.
| Tag | Purpose |
|---|---|
{{ name }} | a value as literal text |
{{p notes|rich }} | markdown rendered as headings, bold, lists, tables |
{{ case_ref|qr }} / {{ case_ref|qr(15, 2) }} | QR code — width in mm, quiet zone in modules |
{%p if ... %} … {%p endif %} | conditionals, each tag alone in its paragraph |
{%tr for c in cases %} … {%tr endfor %} | repeat a table row — each tag in its own row |
Two traps worth knowing before you build a template:
|rich needs the {{p …}} form. A plain {{ notes|rich }} renders fine in DOCX and is
silently dropped from the PDF. Nothing detects it — check the PDF when adding a rich field.{{ office }} renders empty, but
{{ office.city }} on a payload without office raises and returns 400.All environment variables are optional.
| Variable | Description | Default |
|---|---|---|
COLLABORA_URL | Collabora convert-to endpoint | http://localhost:9980/cool/convert-to/pdf |
COLLABORA_USER | Basic-auth user; empty sends no auth header | (empty) |
COLLABORA_PASS | Basic-auth password | (empty) |
COLLABORA_READ_TIMEOUT | Read timeout in seconds (connect is fixed at 5) | 60 |
MAX_UPLOAD_BYTES | Rejects larger templates or data payloads with 413 | 20000000 |
PANDOC_TIMEOUT | Seconds before one markdown conversion is abandoned | 30 |
PANDOC_CONCURRENCY | Max simultaneous pandoc processes | 4 |
PANDOC_SANDBOX | Run pandoc with --sandbox; see Security below | true |
MAX_MARKDOWN_FIELDS | Max |rich conversions per document | 50 |
MAX_QR_CODES | Max |qr codes per document | 50 |
QR_BORDER | Default QR quiet zone, in modules | 4 |
SENTRY_DSN | Error reporting; empty disables Sentry entirely | (empty) |
SENTRY_ENVIRONMENT | Environment tag on reported events | (empty) |
SENTRY_RELEASE | Release tag on reported events | (empty) |
SENTRY_TRACES_SAMPLE_RATE | Performance tracing sample rate; 0 reports errors only | 0 |
Request bodies are never sent to Sentry — the data field carries application data. Exception
messages, the URL and the stack trace are.
Templates are uploaded by callers, so the service treats them as untrusted input:
SandboxedEnvironment. With a plain environment, a template
containing {{ ''.__class__.__mro__… }} would achieve arbitrary code execution.
would be an SSRF and  a file read, because Pandoc fetches and embeds image
targets. Pandoc runs with --sandbox; startup verifies this with a real conversion, and where it
is not usable the service rejects non-data: image links itself. Inline images as base64 data:
URIs. PANDOC_SANDBOX=false disables both mechanisms — trusted callers only.MAX_MARKDOWN_FIELDS, MAX_QR_CODES) bound the work one request can
cause, since the template controls how many conversions happen. Counters are per request.Still open: the sandbox stops code execution but not unbounded computation — a template containing
{% for i in range(10**9) %} will occupy a worker until something kills it. Enforce a request
timeout at the ingress.
The image is Alpine-based with no package manager and no pip; dependencies are installed in a build
stage. trivy image reports 0 vulnerabilities.
The rich-text object is gone. Send markdown as a plain string and choose the rendering in the template:
- "biography": {"content": "# Bio\nA **dev**.", "is_markdown": true} → {{p biography }}
+ "biography": "# Bio\nA **dev**." → {{p biography|rich }}
A payload still containing "is_markdown" is rejected with a 400 explaining the migration, rather
than rendering a literal dict into your document.
Content type
Image
Digest
sha256:596bcbfd7…
Size
101.1 MB
Last updated
about 2 months ago
docker pull dabde/docx2pdf