Self-Host Gotenberg: A PDF Rendering API for Your Apps and Agents

Self-Host Gotenberg: A PDF Rendering API for Your Apps and Agents

Sooner or later every app needs to spit out a PDF. An invoice, a monthly report, a signed contract, a receipt an AI agent just generated. And every time, you reach for the same cursed toolbox: bundle a headless Chrome into your container, or wrestle wkhtmltopdf, or npm-install Puppeteer and watch your image balloon past a gigabyte. Then it works on your laptop and dies in production because a font is missing.

I have shipped that exact bug more than once. Gotenberg is how I stopped.

Gotenberg is a stateless, Docker-based API that does one thing well: you POST it a file over multipart/form-data, and it hands you back a PDF. Under the hood it wraps Chromium, LibreOffice, and a set of PDF tools, so you never install or manage any of them yourself. Your app makes an HTTP call. That is the whole integration.

What you'll need

  • A running Gotenberg instance (we'll get one in a second)
  • Anything that can make an HTTP request: curl, your backend, or an AI agent's tool call
  • Zero PDF libraries in your own codebase

Getting an instance running

Locally, it is one command:

docker run --rm -p 3000:3000 gotenberg/gotenberg:8

That gives you the current v8 listening on port 3000. One thing to know before you expose that anywhere: Gotenberg ships with no authentication. On your laptop that is fine. On a public network it means anyone who finds the URL can render documents on your box, so it belongs behind auth or a private network.

That is the boring, easy-to-get-wrong part. The managed Gotenberg on Elestio runs it for around $11/month with SSL, backups, and updates handled, and it puts the API behind HTTP Basic Auth out of the box. You get an instance URL plus a root user and an admin password from the dashboard, so it is a private endpoint your other services call rather than an open renderer. The API below is identical either way; on the managed instance you just add credentials.

Turning a URL into a PDF

The simplest route grabs a live web page and renders it:

curl --request POST http://localhost:3000/forms/chromium/convert/url \
  --form url=https://example.com \
  -o example.pdf

Chromium actually renders the page, runs its JavaScript, and loads web fonts, so you get the real thing, not a broken 2010-era snapshot.

Turning your own HTML into a PDF

This is the one people use most, and it has the single gotcha that trips up everyone. You send your markup as a file, and that file must be named index.html:

curl --request POST http://localhost:3000/forms/chromium/convert/html \
  --form files=@index.html \
  -o output.pdf

Send report.html instead of index.html and you get a confusing error. Name it correctly and you are done. Any CSS, images, or fonts you reference get uploaded as additional files parts in the same request.

Want to control the page geometry? Chromium takes form fields for that:

Form field What it does Example
paperWidth / paperHeight Page size in inches 8.27 / 11.7 (A4)
marginTop / marginBottom Margins in inches 0.5
landscape Rotate to landscape true
waitDelay Wait before printing (for late JS) 2s

Office documents, too

Have a .docx, .xlsx, or .pptx you need as PDF? That is the LibreOffice route, and it handles over 100 formats:

curl --request POST http://localhost:3000/forms/libreoffice/convert \
  --form files=@contract.docx \
  -o contract.pdf

Drop in a spreadsheet, a slide deck, an OpenDocument file, even an RTF, and it comes back as PDF. No Office install anywhere in your stack.

Merging is a route too

Already have several PDFs and need one? The PDF engine merges them in the order you send them:

curl --request POST http://localhost:3000/forms/pdfengines/merge \
  --form files=@cover.pdf \
  --form files=@body.pdf \
  -o final.pdf

Calling your managed instance

Everything above targets a bare local container. When you point at your Elestio instance, the only difference is authentication and the URL. Pass the root credentials with -u, and use the handy Gotenberg-Output-Filename header to name the returned file:

curl --request POST 'https://your-instance.vm.elestio.app/forms/chromium/convert/url' \
  --header 'Gotenberg-Output-Filename: result' \
  --form 'url="https://www.wikipedia.org/"' \
  -u root:YOUR_ADMIN_PASSWORD \
  -O -J

The -O -J flags tell curl to save the response using that server-provided filename. Same routes, same form fields, just add -u root:YOUR_ADMIN_PASSWORD to any request.

Why this is perfect for AI agents

Here is the part I did not expect to love. Give an agent a "generate PDF" tool and the implementation is a single authenticated HTTP call to Gotenberg. The agent writes some HTML, POSTs it, gets back a document it can email or store. You are not shipping a headless browser inside your agent runtime, and you are not exposing your agent to a third-party PDF SaaS with per-document pricing. The rendering stays on infrastructure you own.

Troubleshooting

"index.html not found" or a vague 400. Your HTML file was named something else. The HTML route requires the main file to be exactly index.html. This is the number one issue.

401 Unauthorized on your managed instance. You left off the credentials. Add -u root:YOUR_ADMIN_PASSWORD; the password is in your Elestio dashboard.

The PDF is missing content that loads via JavaScript. Chromium printed before your scripts finished. Add waitDelay=2s, or better, use waitForExpression to block until a specific JS condition is true instead of guessing a delay.

Emojis or special characters render as boxes. The font is not present in the container. Reference a web font in your HTML, or use a Gotenberg image variant that ships more fonts.

A LibreOffice conversion looks slightly off. Complex Office layouts do not always map perfectly to PDF. For pixel-perfect output, render from your own HTML through Chromium instead of relying on the Office file.

Requests time out on big pages. Heavy pages need more time and memory. Raise Gotenberg's timeout configuration and give the instance a little more RAM. It is light at idle, but Chromium spikes during a render.

The payoff

Once Gotenberg is running, PDF generation stops being a dependency you dread and becomes a URL you call. No headless browser in your image, no font roulette, no per-document SaaS bill. Your apps and your agents get clean PDFs from one HTTP request, and you get to delete a pile of fragile code.

If you would rather not babysit the container, its timeouts, and its auth, the managed Gotenberg on Elestio gives you a secured private endpoint with backups and updates handled, so you can go straight to calling it.

Thanks for reading ❤️ See you in the next one 👋