Build your PDF service from templates — not code

PDF Server is a base Docker image with a real headless browser inside. Add your HTML templates, build your own image, deploy it inside your network — and turn JSON into pixel-perfect PDFs with one API call.

Get startedSee example PDFs

MIT License GitLab

Build your PDF service

From zero to your own PDF microservice — the whole cycle.

  1. Get PDF Server

    Pull the base image — a Go service with a headless browser already inside.

    docker pull registry.gitlab.com/c0va23/pdf-server:latest-chromium
  2. Create a template folder

    A template is just a folder — one per document. Everything it needs lives beside it.

    mkdir -p templates/invoice
  3. Write the template

    Plain HTML with placeholders for your data. Mustache, Handlebars, Go html/template, or static HTML — chosen by file extension.

    cat > templates/invoice/template.mustache <<'HTML'
    <link rel="stylesheet" href="assets/style.css" />
    <h1>Invoice for {{customer}}</h1>
    <p class="total">Total: {{total}}</p>
    HTML
  4. Describe the data with a schema

    Add an optional JSON Schema. Incoming requests are validated against it before rendering — invalid data gets a clear 422, never a broken PDF.

    cat > templates/invoice/schema.json <<'JSON'
    {
      "type": "object",
      "required": ["customer", "total"],
      "properties": {
        "customer": { "type": "string" },
        "total":    { "type": "number" }
      }
    }
    JSON
  5. Add assets

    Reference CSS, fonts, images, and SVG from your HTML — they load just like a normal web page.

    mkdir -p templates/invoice/assets
    cat > templates/invoice/assets/style.css <<'CSS'
    h1 { color: #5d2f86 }
    .total { font-weight: bold }
    CSS
  6. Debug with live preview

    Run the base image with your templates mounted, then open the built-in tester UI: pick a template, edit the data, and watch the PDF re-render on every save.

    docker run -p 9999:9999 \
      -v $PWD/templates:/src/templates \
      -e TEMPLATES_RELOAD_MODE=always \
      registry.gitlab.com/c0va23/pdf-server:latest-chromium
  7. Build your container image

    Packaging is a file copy on top of the base image — no app build, no code. Write the Dockerfile, then build your image.

    cat > Dockerfile <<'DOCKERFILE'
    FROM registry.gitlab.com/c0va23/pdf-server:latest-chromium
    COPY templates /src/templates
    RUN pdf-server validate
    DOCKERFILE
    docker build -t your-org/pdf-service .

    validate renders every example at build time — a broken template fails the build, not production.

  8. Deploy it

    Run it like any container — Docker, Compose, Kubernetes — inside your own network. Your data never leaves it.

    docker run -p 9999:9999 your-org/pdf-service

    Pooled browsers, OpenTelemetry tracing, structured logs, and a status endpoint are built in.

  9. Send a POST request, get a PDF

    One API call from your application. That's the whole integration.

    curl -X POST localhost:9999/templates/invoice/render \
      -d '{"customer": "ACME", "total": 90}' 

    The response body is the finished PDF (application/pdf).

Get PDF

What happens inside your container on every request.

  1. Validate the data

    If the template ships a schema.json, the incoming JSON is checked against it first.

    Invalid data is rejected with a clear 422 and the validation errors — a broken PDF is never produced.

  2. Render HTML from your template

    Your template is filled with the data — body, header, and footer.

    <h1>Invoice for {{customer}}</h1>

    Mustache, Handlebars, Go html/template, or static HTML — chosen by file extension, with partials.

  3. Open it in a real browser

    A headless Chromium/Firefox borrowed from the pool loads the page together with your assets — CSS, fonts, images.

    Driven over the Chrome DevTools Protocol. SVG and JS charts work too: lifecycle events wait for async content.

  4. Compose, if needed

    A composition merges several sub-renders — cover, invoice, terms — into one document.

    Data-driven plans (YAML with jq expressions, or JavaScript) can mix local templates, raw HTML, and remote documents.

  5. Print to PDF

    The browser's print engine produces the pages — the same output as the browser's Print to PDF.

    {"pdf": {"landscape": true, "printBackground": true}}

    Your params.json controls paper size, margins, orientation, headers and footers.

  6. The PDF comes back

    Returned in the same HTTP response — no polling, no intermediate storage.

    Curious how it looks? See real rendered PDFs in the gallery →

Why PDF Server?

  • One docker build, no browser plumbing. Your image is a file copy on top of the base image — the headless browser and all its dependencies are already inside. Nothing to install, patch, or wire up.
  • Develop and debug with a live tester UI. Edit a template and watch the PDF re-render instantly — pick a template, tweak the data, see the result. Catch problems on your laptop, not in production.
  • Runs on your own infrastructure. Deploy the container wherever you like, inside your own network, and scale it out with more replicas as load grows.
  • Bring your favorite frontend tools. Templates are just HTML and CSS, so you can author them with the toolchain you already use — with live rebuild — and let PDF Server print the result.

See the Overview for the full picture or jump straight to the Quickstart.