Invalid Date

Are you running Gotenberg or Puppeteer as Docker containers to generate PDFs? If you're struggling with Chromium's memory consumption, bloated container images, and scaling complexity, delegating PDF generation to an API is a better approach.

This guide shows you how to call the FUNBREW PDF API from Docker Compose and Kubernetes environments to generate PDFs, with practical code examples.

For the basics of the API itself, see the quickstart guide.

Challenges of Self-Hosted PDF Generation

Problems with Gotenberg / Puppeteer Containers

Issue Details
Image size 1GB+ with Chromium. Slow to pull and deploy
Memory usage Chromium processes consume 500MB-1GB. Common cause of OOMKill
Scaling Rendering is CPU-intensive. Horizontal scaling requires significant resources
Maintenance Chromium version management and security patching required
Font management Adding CJK fonts and managing font caches is cumbersome

Benefits of API-Based PDF Generation

Benefit Details
Lightweight containers Just an HTTP client. Dramatically smaller image size
Low resources Minimal CPU and memory consumption
No scaling needed The API handles PDF generation scaling
Font support Multilingual fonts including CJK are supported out of the box
Engine choice Switch between quality (Chromium) and fast (wkhtmltopdf) per use case

For more on engine differences and selection criteria, see the HTML to PDF conversion guide.

Docker Compose Integration

Basic Configuration

Here's a Docker Compose configuration for an application that uses the PDF API. Without a Chromium container, the setup is much simpler.

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - FUNBREW_API_KEY=${FUNBREW_API_KEY}
      - FUNBREW_API_URL=https://api.pdf.funbrew.cloud
      - NODE_ENV=production
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  worker:
    build:
      context: .
      dockerfile: Dockerfile
    command: ["node", "worker.js"]
    environment:
      - FUNBREW_API_KEY=${FUNBREW_API_KEY}
      - FUNBREW_API_URL=https://api.pdf.funbrew.cloud
      - REDIS_URL=redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    volumes:
      - redis_data:/data

volumes:
  redis_data:

Lightweight Dockerfile

Since PDF generation is delegated to the API, your Dockerfile stays lightweight. No Chromium or font installation needed.

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# No Chromium needed!
# RUN apt-get install -y chromium fonts-noto-cjk  # Not needed anymore

EXPOSE 3000
USER node
CMD ["node", "server.js"]

Compare this to a Gotenberg-based setup:

# Before: Using Gotenberg (image size: ~1.5GB)
services:
  gotenberg:
    image: gotenberg/gotenberg:8
    ports:
      - "3000:3000"
    deploy:
      resources:
        limits:
          memory: 2G

# After: Using FUNBREW PDF API (image size: ~150MB)
services:
  app:
    build: .
    # No Chromium container needed

Application Code (Node.js)

// src/pdf-service.ts
import { FunbrewPdf } from '@funbrew/pdf';

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_API_KEY!,
});

export async function generateInvoicePdf(data: InvoiceData): Promise<Buffer> {
  const html = renderInvoiceTemplate(data);

  const response = await client.generate({
    html,
    engine: 'quality',
    format: 'A4',
  });

  return Buffer.from(response.data);
}

export async function generateReportPdf(markdown: string): Promise<Buffer> {
  const response = await client.markdown({
    markdown,
    theme: 'github',
    format: 'A4',
  });

  return Buffer.from(response.data);
}

For SDK usage details, see the language-specific quickstart. For concrete invoice implementation examples, see the invoice PDF automation guide.

Environment Variables

# .env (do not commit to Git)
FUNBREW_API_KEY=sk-your-api-key-here
# .env.example (commit to repository)
FUNBREW_API_KEY=sk-your-api-key-here-replace-me

For secure API key management, see the security guide.

Kubernetes (k8s) PDF Generation Service

Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pdf-service
  labels:
    app: pdf-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pdf-service
  template:
    metadata:
      labels:
        app: pdf-service
    spec:
      containers:
        - name: pdf-service
          image: your-registry.com/pdf-service:latest
          ports:
            - containerPort: 3000
          env:
            - name: FUNBREW_API_KEY
              valueFrom:
                secretKeyRef:
                  name: funbrew-credentials
                  key: api-key
            - name: NODE_ENV
              value: "production"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /health
              port: 3000
            failureThreshold: 30
            periodSeconds: 10

Notice how low the resource requirements are. Since PDF generation is delegated to the API, you need only 256Mi memory instead of the 2Gi+ required by Chromium containers.

Service & Ingress

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: pdf-service
spec:
  selector:
    app: pdf-service
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pdf-service-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
spec:
  ingressClassName: nginx
  rules:
    - host: pdf.your-domain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: pdf-service
                port:
                  number: 80
  tls:
    - hosts:
        - pdf.your-domain.com
      secretName: pdf-service-tls

Secret Management

Kubernetes Secrets

# Create the API key as a Secret
kubectl create secret generic funbrew-credentials \
  --from-literal=api-key=sk-your-api-key-here

External Secrets Operator (AWS / GCP / Azure)

For production environments, use the External Secrets Operator to sync with your cloud provider's secret store.

# k8s/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: funbrew-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: funbrew-credentials
  data:
    - secretKey: api-key
      remoteRef:
        key: funbrew-pdf-api-key

Sealed Secrets (GitOps)

If you're using a GitOps workflow, Sealed Secrets lets you safely commit encrypted Secrets to your Git repository.

# Create a Sealed Secret
kubeseal --format=yaml \
  --cert=pub-cert.pem \
  < k8s/secret.yaml \
  > k8s/sealed-secret.yaml

Horizontal Pod Autoscaler (HPA)

Automatically scale Pods based on PDF generation request load.

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: pdf-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: pdf-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60

Since PDF generation is handled by the API, your application Pods maintain low CPU and memory usage. You can set conservative scaling thresholds.

Health Check Implementation

Application Health Endpoints

// src/health.ts
import express from 'express';

const app = express();

// Liveness: Is the app alive?
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Readiness: Can it accept requests?
app.get('/ready', async (req, res) => {
  try {
    // Check connectivity to FUNBREW PDF API
    const response = await fetch(
      `${process.env.FUNBREW_API_URL}/v1/health`,
      { signal: AbortSignal.timeout(5000) }
    );

    if (response.ok) {
      res.status(200).json({ status: 'ready', pdfApi: 'connected' });
    } else {
      res.status(503).json({ status: 'not ready', pdfApi: 'unavailable' });
    }
  } catch (error) {
    res.status(503).json({ status: 'not ready', pdfApi: 'unreachable' });
  }
});

Avoiding Over-Dependency on External API Availability

When checking external API connectivity in Readiness Probes, be careful not to unnecessarily remove Pods due to transient network errors.

readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3  # Only mark Not Ready after 3 consecutive failures
  successThreshold: 1

CI/CD Pipeline (GitHub Actions)

PDF Generation Tests

Run PDF generation tests in your CI/CD pipeline.

# .github/workflows/pdf-test.yml
name: PDF Generation Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm test

      - name: PDF generation smoke test
        env:
          FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
        run: |
          node -e "
          const { FunbrewPdf } = require('@funbrew/pdf');
          const client = new FunbrewPdf({ apiKey: process.env.FUNBREW_API_KEY });
          async function test() {
            const res = await client.generate({
              html: '<h1>CI Test</h1><p>Generated at $(date -u)</p>',
              engine: 'fast',
              format: 'A4',
            });
            console.log('PDF generated:', res.data.length, 'bytes');
            if (res.data.length < 100) throw new Error('PDF too small');
          }
          test().catch(e => { console.error(e); process.exit(1); });
          "

Build & Deploy

# .github/workflows/deploy.yml
name: Build & Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-arn: arn:aws:iam::role/github-actions
          aws-region: ap-northeast-1

      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push image
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $REGISTRY/pdf-service:$IMAGE_TAG .
          docker build -t $REGISTRY/pdf-service:latest .
          docker push $REGISTRY/pdf-service:$IMAGE_TAG
          docker push $REGISTRY/pdf-service:latest

      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/pdf-service \
            pdf-service=$REGISTRY/pdf-service:${{ github.sha }}
          kubectl rollout status deployment/pdf-service --timeout=300s

Automated PDF Reports in GitHub Actions

Generate a PDF report for each PR and save it as an artifact.

# .github/workflows/report.yml
name: Generate PDF Report

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  generate-report:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Generate changelog PDF
        env:
          FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
        run: |
          # Generate changelog as Markdown
          git log --oneline origin/main..HEAD > changelog.md

          # Convert Markdown to PDF
          curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-markdown \
            -H "Authorization: Bearer $FUNBREW_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"markdown\": \"$(cat changelog.md | jq -Rs .)\"}" \
            --output report.pdf

      - name: Upload PDF artifact
        uses: actions/upload-artifact@v4
        with:
          name: pr-report
          path: report.pdf

For more on Markdown to PDF conversion, see the Markdown to PDF API guide.

Python Application Example (FastAPI + Docker)

FastAPI PDF Endpoint

# app/main.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from funbrew_pdf import FunbrewPdf
import os
import io

app = FastAPI()
client = FunbrewPdf(api_key=os.environ["FUNBREW_API_KEY"])

@app.get("/health")
async def health():
    return {"status": "ok"}

@app.post("/generate-pdf")
async def generate_pdf(data: dict):
    try:
        response = client.generate(
            html=data["html"],
            engine=data.get("engine", "quality"),
            format=data.get("format", "A4"),
        )

        return StreamingResponse(
            io.BytesIO(response.data),
            media_type="application/pdf",
            headers={"Content-Disposition": "attachment; filename=output.pdf"},
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/generate-report")
async def generate_report(data: dict):
    try:
        response = client.markdown(
            markdown=data["markdown"],
            theme=data.get("theme", "github"),
            format="A4",
        )

        return StreamingResponse(
            io.BytesIO(response.data),
            media_type="application/pdf",
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

For more on PDF API usage with Python, see the Django & FastAPI guide.

Python Dockerfile

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

# No Chromium needed! Stays lightweight
EXPOSE 8000
USER nobody
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# requirements.txt
fastapi==0.115.0
uvicorn==0.32.0
funbrew-pdf==1.1.0

Migrating from Gotenberg

If you're using Gotenberg, migrating to FUNBREW PDF API is straightforward.

API Call Mapping

Gotenberg FUNBREW PDF API
POST /forms/chromium/convert/html POST /v1/pdf/from-html
POST /forms/chromium/convert/url POST /v1/pdf/from-url
POST /forms/chromium/convert/markdown POST /v1/pdf/from-markdown
waitTimeout Managed by API server
paperWidth / paperHeight format: "A4" / format: "Letter"

Migration Code Example

// Before: Gotenberg
async function generateWithGotenberg(html: string): Promise<Buffer> {
  const formData = new FormData();
  formData.append('files', new Blob([html], { type: 'text/html' }), 'index.html');

  const response = await fetch('http://gotenberg:3000/forms/chromium/convert/html', {
    method: 'POST',
    body: formData,
  });

  return Buffer.from(await response.arrayBuffer());
}

// After: FUNBREW PDF API
import { FunbrewPdf } from '@funbrew/pdf';

const client = new FunbrewPdf({ apiKey: process.env.FUNBREW_API_KEY! });

async function generateWithFunbrew(html: string): Promise<Buffer> {
  const response = await client.generate({
    html,
    engine: 'quality',
    format: 'A4',
  });

  return Buffer.from(response.data);
}

You can remove the Gotenberg service from your Docker Compose, significantly simplifying your infrastructure. For a comparison of self-hosted vs API approaches, see the PDF API comparison guide.

Batch PDF Generation

Use Kubernetes Jobs to generate PDFs in parallel batches.

# k8s/batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pdf-batch-generation
spec:
  parallelism: 5
  completions: 100
  completionMode: Indexed
  template:
    spec:
      containers:
        - name: pdf-generator
          image: your-registry.com/pdf-batch:latest
          env:
            - name: FUNBREW_API_KEY
              valueFrom:
                secretKeyRef:
                  name: funbrew-credentials
                  key: api-key
            - name: JOB_COMPLETION_INDEX
              valueFrom:
                fieldRef:
                  fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
      restartPolicy: OnFailure
  backoffLimit: 3
// batch/generate.ts
import { FunbrewPdf } from '@funbrew/pdf';

const client = new FunbrewPdf({ apiKey: process.env.FUNBREW_API_KEY! });
const batchIndex = parseInt(process.env.JOB_COMPLETION_INDEX || '0');

async function processBatch() {
  // Fetch batch records from database
  const records = await getRecordsBatch(batchIndex, 100);

  for (const record of records) {
    const html = renderTemplate(record);
    const response = await client.generate({
      html,
      engine: 'fast', // fast engine is ideal for batch processing
      format: 'A4',
    });

    await uploadToStorage(record.id, response.data);
    console.log(`Generated PDF for record ${record.id}`);
  }
}

processBatch().catch(console.error);

For more batch processing patterns, see the batch processing guide.

Production Best Practices

Retry and Circuit Breaker

For containers calling external APIs, implementing retry logic and a circuit breaker is essential.

// src/resilient-client.ts
import { FunbrewPdf } from '@funbrew/pdf';

class ResilientPdfClient {
  private client: FunbrewPdf;
  private failureCount = 0;
  private lastFailure = 0;
  private readonly threshold = 5;
  private readonly resetTimeout = 60000; // 60 seconds

  constructor() {
    this.client = new FunbrewPdf({
      apiKey: process.env.FUNBREW_API_KEY!,
    });
  }

  async generate(options: any, retries = 3): Promise<Buffer> {
    // Circuit breaker check
    if (this.isCircuitOpen()) {
      throw new Error('Circuit breaker is open. PDF API may be unavailable.');
    }

    for (let attempt = 1; attempt <= retries; attempt++) {
      try {
        const response = await this.client.generate(options);
        this.onSuccess();
        return Buffer.from(response.data);
      } catch (error: any) {
        this.onFailure();

        // Don't retry on 4xx errors
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }

        if (attempt === retries) throw error;

        // Exponential backoff
        await new Promise(r =>
          setTimeout(r, 1000 * Math.pow(2, attempt - 1))
        );
      }
    }

    throw new Error('Unreachable');
  }

  private isCircuitOpen(): boolean {
    if (this.failureCount >= this.threshold) {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.failureCount = 0;
        return false;
      }
      return true;
    }
    return false;
  }

  private onSuccess() { this.failureCount = 0; }
  private onFailure() {
    this.failureCount++;
    this.lastFailure = Date.now();
  }
}

export const pdfClient = new ResilientPdfClient();

For more error handling patterns, see the error handling guide.

Monitoring (Prometheus + Grafana)

Expose PDF generation metrics for Prometheus collection.

// src/metrics.ts
import { Counter, Histogram, register } from 'prom-client';

export const pdfGenerationCounter = new Counter({
  name: 'pdf_generation_total',
  help: 'Total number of PDF generation requests',
  labelNames: ['status', 'engine'],
});

export const pdfGenerationDuration = new Histogram({
  name: 'pdf_generation_duration_seconds',
  help: 'PDF generation duration in seconds',
  labelNames: ['engine'],
  buckets: [0.5, 1, 2, 5, 10, 30],
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

NetworkPolicy

Restrict egress traffic to only allow communication with the PDF API.

# k8s/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: pdf-service-policy
spec:
  podSelector:
    matchLabels:
      app: pdf-service
  policyTypes:
    - Egress
  egress:
    - to: []  # Allow outbound to external API (FUNBREW PDF)
      ports:
        - port: 443
          protocol: TCP
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - port: 6379
          protocol: TCP

Troubleshooting

Common Issues and Solutions

Issue Cause Solution
Container OOMKill Memory limit too low Increase resources.limits.memory
DNS resolution errors Cluster DNS issues Check CoreDNS status
API timeout NetworkPolicy blocking HTTPS Allow egress on port 443
Secret not found Secret name mismatch Verify with kubectl get secrets
Image pull failure Registry auth error Configure imagePullSecrets

For more on production operations, see the production guide.

Debug Commands

# Check Pod status
kubectl get pods -l app=pdf-service

# View logs
kubectl logs -l app=pdf-service --tail=100

# Test API connectivity from inside a Pod
kubectl exec -it deploy/pdf-service -- \
  curl -s https://api.pdf.funbrew.cloud/v1/health

# Verify Secret
kubectl get secret funbrew-credentials -o jsonpath='{.data.api-key}' | base64 -d

# Check HPA status
kubectl get hpa pdf-service-hpa

Using with Next.js / Nuxt

FUNBREW PDF API works the same way in containerized Next.js and Nuxt applications. For frontend framework integration, see the Next.js & Nuxt guide.

Summary

PDF generation in Docker and Kubernetes environments is dramatically simplified by delegating to an API.

  • Docker Compose: Eliminate Gotenberg/Puppeteer containers. Image size reduced by 10x
  • Kubernetes: Low-resource Deployments with HPA for autoscaling
  • Secret management: External Secrets Operator for cloud KMS integration
  • CI/CD: PDF generation tests and automated reports in GitHub Actions
  • Batch processing: Parallel batch generation with k8s Jobs

For webhook integration to enhance async processing, see the webhook integration guide. For a comparison with serverless, see the AWS Lambda PDF generation guide.

Get started by creating a free account and trying the API in the Playground.

Powered by FUNBREW PDF