Gotenberg
API DevelopmentA Docker‑based API for converting HTML, URLs, Markdown and office documents to PDF
Features
- Convert HTML, URL or Markdown to PDF via Headless Chromium
- Convert over 100 Office document formats to PDF using LibreOffice
- Merge, split, rotate and flatten PDFs
Recent releases
View all 10 releases →- Operators with internet‑facing APIs should opt into strict posture using new flags (`--chromium-deny-private-ips`, `--chromium-deny-public-ips`, etc.).
- Review and configure IP‑class filtering flags for Chromium, webhook, API download‑from, and LibreOffice to maintain desired security posture.
- If previously relying on permissive SSRF defaults within private networks, no immediate action is required; the default behavior remains permissive.
- Reverted SSRF defaults to permissive (restores 8.30.x behavior).
- Required uploaded file for `image`/`pdf` stamp and watermark sources.
- Scoped `file://` sub-resources to the request working directory.
- Rejected `file://` URLs at `/forms/chromium/convert/url` to prevent directory enumeration.
- Scoped `file://` sub-resources to the request working directory, preventing cross‑request SSRF.
- Hardened Chromium against DNS rebinding via a loopback HTTP proxy that pins resolved IPs.
- LibreOffice outbound URL filtering with allow‑list/deny‑list flags (`--libreoffice-allow-list`, `--libreoffice-deny-list`).
- IP‑class filtering flags for Chromium, webhook, API download‑from, and LibreOffice modules (e.g., `--chromium-deny-private-ips`, `--webhook-deny-public-ips`).
Full changelog
Breaking Changes & Security Fixes ⚠️
-
Reverted SSRF defaults (breaking vs 8.31.0). 8.31.0 blocked private-IP destinations by default, which broke deployments running Gotenberg inside a private network. 8.32.0 restores the 8.30.x permissive defaults. Operators with internet-facing APIs opt into the strict posture via the new flags below.
-
Rejected
file://at/forms/chromium/convert/url. Submittingurl=file:///tmp/...used to let an unauthenticated caller enumerate the request working directory and read other in-flight uploads as rendered PDFs. The route now returns HTTP 400 for anyfile://URL. -
Required uploaded file for
image/pdfstamp and watermark sources. Twelve callsites acceptedstampSource=pdforwatermarkSource=pdfwith an expression pointing at any path the Gotenberg process could open, even when no file was uploaded. Handlers now return HTTP 400 unless the caller uploaded a matching file. -
Scoped
file://sub-resources to the request working directory. Crafted HTML could reference another request'sfile:///tmp/<reqdir>/.... The CDP request handler now restrictsfile://sub-resources to the current request's directory./convert/urland/screenshot/urlreject everyfile://sub-resource outright. -
Hardened Chromium against DNS rebinding. A short-TTL DNS authority could return a public IP at validation and a private IP at connect. A loopback HTTP / CONNECT proxy now sits between Chromium and the network, resolves DNS once, and pins the dial to the resolved IP. Skipped when
--chromium-proxy-serveror--chromium-host-resolver-rulesis set. -
Filtered LibreOffice outbound fetches through a proxy. Uploaded OOXML, RTF, and ODF files can embed external URLs that LibreOffice's libcurl resolves below every Go-side SSRF filter. LibreOffice now routes every outbound fetch through an in-process forward proxy on the same
gotenberg.DecideOutboundpath Chromium and webhook delivery use. See the four new flags below. -
Recovered webhook async panics. High-concurrency webhooks could panic the async goroutine and crash the whole process. The goroutine now snapshots the request context and recovers any future panic through the existing error path.
New Features
-
LibreOffice outbound URL filtering. Four flags mirror the Chromium and webhook layout:
--libreoffice-allow-list,--libreoffice-deny-list,--libreoffice-deny-private-ips,--libreoffice-deny-public-ips. All default permissive. -
IP-class filtering on four modules.
chromium,webhook,api-download-from, andlibreofficeeach accept matchingdeny-private-ipsanddeny-public-ipsflags. All default tofalse.
| Flag | What it does |
| -------------------------------------- | --------------------------------------------------------------------------- |
| --chromium-deny-private-ips | Reject Chromium navigations and sub-resources resolving to a non-public IP. |
| --chromium-deny-public-ips | Reject Chromium navigations and sub-resources resolving to a public IP. |
| --webhook-deny-private-ips | Reject webhook URLs (success, error, events) resolving to a non-public IP. |
| --webhook-deny-public-ips | Reject webhook URLs resolving to a public IP. |
| --api-download-from-deny-private-ips | Reject downloadFrom URLs resolving to a non-public IP. |
| --api-download-from-deny-public-ips | Reject downloadFrom URLs resolving to a public IP. |
| --libreoffice-deny-private-ips | Reject LibreOffice outbound fetches resolving to a non-public IP. |
| --libreoffice-deny-public-ips | Reject LibreOffice outbound fetches resolving to a public IP. |
A URL matching --*-allow-list skips the IP-class check. A URL matching --*-deny-list is always rejected. Setting both deny-private-ips=true and deny-public-ips=true rejects every URL unless the allow-list matches.
Bug Fixes
-
Charts print as blank rectangles (#1531, #1532, #1534, #1535):
chromedp v0.15.0suspended the BeginFrame-driven callback dispatch loop underemulatedMediaType=print.requestAnimationFrame,ResizeObserver,IntersectionObserver, CSStransitionend, and CSSanimationendall stopped firing. Pinningchromedpback tov0.14.2restores native dispatch. -
LibreOffice cached an unrecoverable first-start error (#1538): A short
--libreoffice-start-timeouttimed out the first request, then every subsequent request returned the same cached error until the container restarted. The lazy-start path now retries on failure.
Chore
- Updated
pdfcputov0.12.0. - Switched metadata read/write to direct
exiftoolinvocation. Removes the GPL-3.0go-exiftooldependency. - Bumped Go to
1.26.2. - Updated Go dependencies.
Thanks
Thanks to @Jalliuz (#1527) for reporting the 8.31.0 sub-resource regression. @notscottsmith (#1531), @spoltix (#1532), @rdelott-work (#1534), and @sillyas2010 (#1535) narrowed down the chromedp print-mode regression. @sillyas2010 also published the reproducer that pinned the bisect. @JeremyReist2 (#1536) flagged the go-exiftool GPL-3.0 license. @doronbehar (#1537) requested the pdfcpu upgrade. @mlafon (#1538) reported the LibreOffice supervisor cached-error bug.
- Update Docker image pull source from `thecodingmachine/gotenberg` to `gotenberg/gotenberg`.
- If you rely on internal webhook endpoints, override the new default `--webhook-deny-list` flag.
- Deprecated flags `--webhook-error-allow-list` and `--webhook-error-deny-list` are still functional but will be removed in a future release; use `--webhook-allow-list` and `--webhook-deny-list` respectively.
- Stopped publishing `thecodingmachine/gotenberg` Docker images; pull from `gotenberg/gotenberg` instead.
- SSRF hardening: outbound URLs (Chromium asset fetches, webhook delivery, download-from) now resolve and reject non-public addresses (loopback, RFC1918, link‑local, unspecified, multicast, IPv6 unique‑local, IPv4‑mapped IPv6) and pin the dial to validated IPs.
- Defaulted `--webhook-deny-list` to a regex blocking loopback, RFC1918, link‑local, and IPv6 unique‑local ranges; override flag to allow internal hosts.
- Added `embedsMetadata` field to all routes accepting `embeds`, allowing per‑file JSON metadata (mimeType, relationship, etc.)
Full changelog
Breaking Changes & Security Fixes ⚠️
- Stopped publishing
thecodingmachine/gotenbergimages. Pull fromgotenberg/gotenberginstead. - SSRF hardening (breaking). Resolves outbound URLs (Chromium asset fetches, webhook delivery, download-from) and rejects non-public addresses: loopback, RFC1918, link-local, unspecified, multicast, IPv6 unique-local, IPv4-mapped IPv6. Pins the dial to the validated IP to prevent DNS rebinding.
- Defaulted webhook deny list (breaking).
--webhook-deny-listnow defaults to a regex blocking loopback, RFC1918, link-local, and IPv6 unique-local ranges. Override the flag to call internal hosts. - Sanitized ExifTool metadata (breaking for
System:tags). Strips control characters and line breaks from/forms/pdfengines/metadata/writepayloads. DropsSystem:-prefixed tags. Blocks argument smuggling and filesystem pseudo-tag abuse.
New Features
- Embed files metadata. Adds
embedsMetadatato every route acceptingembeds(Chromium HTML/URL/Markdown, LibreOffice convert, PDF Engines merge/split/embed). Pass a JSON object keyed by filename with per-file fields (mimeType,relationship, etc.) - thanks @Jean-Beru!
Bug Fixes
- Pinned Chromium to v146 on ppc64le to work around an upstream regression.
Deprecated Flags
| Old | New |
| ---------------------------- | ---------------------- |
| --webhook-error-allow-list | --webhook-allow-list |
| --webhook-error-deny-list | --webhook-deny-list |
Old flags still work.
Chore
- Updated Go dependencies.
Fixed Chromium-only variant startup failures and restored cURL for orchestrator health checks.
Full changelog
Another release, another bug fixes 🫥
Bug Fixes
chromiumonly variants now start correctly - thanks @agross!- Re-added cURL for orchestrators health check - thanks @budivoogt, @gertjanstulp and @jfisbein!
- `Gotenberg-Webhook-Error-Url` is deprecated; error handling now flows through `Gotenberg-Webhook-Events-Url` when it is set
- Chromium‑only image `gotenberg/gotenberg:8.30.0-chromium` drops LibreOffice, Python3 and hyphenation (~30% smaller)
- LibreOffice‑only image `gotenberg/gotenberg:8.30.0-libreoffice` drops Chromium (~38% smaller)
- Full image reduced ~13% compared to 8.29.0 by simplifying the font stack from >30 packages to 8 (Noto, DejaVu, Liberation, Carlito, Caladea)
Full changelog
New Features
Docker Image Variants
- Chromium-Only Image (
gotenberg/gotenberg:8.30.0-chromium): Drops LibreOffice, python3, and hyphenation packages. ~30% smaller than the full image. - LibreOffice-Only Image (
gotenberg/gotenberg:8.30.0-libreoffice): Drops Chromium and its dependencies. ~38% smaller than the full image.
Pick the variant that matches your workload. The full image (gotenberg/gotenberg:8.30.0) still ships everything.
Leaner Docker Image
The full image is ~13% smaller than 8.29.0. The font stack was simplified from 30+ packages down to 8, covering Latin, Greek, Cyrillic, CJK, and most world scripts through Noto, plus color emoji.
| Package | Coverage |
| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fonts-noto-core | Arabic, Bengali, Devanagari, Ethiopic, Georgian, Gujarati, Gurmukhi, Hebrew, Kannada, Khmer, Lao, Malayalam, Myanmar, Sinhala, Tamil, Telugu, Thai, and more |
| fonts-noto-cjk | Chinese, Japanese, Korean |
| fonts-noto-color-emoji | Color emoji |
| fonts-dejavu | Latin, Greek, Cyrillic |
| fonts-crosextra-carlito | Metric-compatible with Calibri |
| fonts-crosextra-caladea | Metric-compatible with Cambria |
| fonts-liberation | Metric-compatible with Arial, Times New Roman, Courier New |
| fonts-liberation2 | Updated Liberation metrics |
Microsoft Core Fonts (ttf-mscorefonts-installer) are not shipped due to licensing constraints. The image includes metric-compatible replacements instead: Carlito for Calibri, Caladea for Cambria, and Liberation for Arial, Times New Roman, and Courier New. These preserve document layout in most cases.
Installing Additional Fonts
Build a custom Dockerfile to add fonts. Common scenarios:
Microsoft Core Fonts (you accept the Microsoft EULA):
FROM gotenberg/gotenberg:8
USER root
RUN echo "deb http://deb.debian.org/debian trixie contrib non-free" \
> /etc/apt/sources.list.d/contrib.list \
&& echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" \
| debconf-set-selections \
&& apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
ca-certificates \
wget \
ttf-mscorefonts-installer \
&& rm -rf /var/lib/apt/lists/*
USER gotenberg
Specialized script fonts for richer glyph sets, better hinting, or traditional typefaces beyond the basic Noto coverage:
| Script | Package |
| :----------------- | :--------------------- |
| Arabic (Naskh) | fonts-hosny-amiri |
| Bengali | fonts-beng |
| Devanagari (Hindi) | fonts-sarai |
| Ethiopic | fonts-sil-abyssinica |
| Gujarati | fonts-samyak-gujr |
| Gurmukhi (Punjabi) | fonts-lohit-guru |
| Hebrew | culmus |
| Kannada | fonts-lohit-knda |
| Malayalam | fonts-samyak-mlym |
| Myanmar | fonts-sil-padauk |
| Sinhala | fonts-lklug-sinhala |
| Tamil | fonts-samyak-taml |
| Telugu | fonts-telu |
| Thai | fonts-thai-tlwg |
FROM gotenberg/gotenberg:8
USER root
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
fonts-hosny-amiri \
fonts-thai-tlwg \
&& rm -rf /var/lib/apt/lists/*
USER gotenberg
Webhook
Gotenberg-Webhook-Error-UrlNow Optional: WhenGotenberg-Webhook-Events-Urlis set,Gotenberg-Webhook-Error-Urlis no longer required. Error handling flows through the events URL instead.Gotenberg-Webhook-Error-Urlis deprecated but continues to work.
Bug Fixes
- ExifTool Tag Filtering: Case-insensitive comparison and expanded blocklist for ExifTool metadata filtering. Excludes additional system tags while preserving safe derived tags.
- Regex Timeout: Added timeout to regex evaluation to prevent ReDoS on malformed patterns.
Chore
- Updated Go dependencies.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.