NaN/NaN/NaN

コンテナ環境でPDFを生成するとき、GotenbergやPuppeteerをDockerコンテナとして自前で運用していませんか。Chromiumのメモリ消費、コンテナイメージの肥大化、スケーリングの複雑さに悩まされているなら、PDF生成をAPIに委任するアプローチが有効です。

この記事では、FUNBREW PDFのAPIをDocker ComposeやKubernetes環境から呼び出してPDFを生成する方法を、実践的なコード例付きで解説します。

API自体の基本的な使い方はクイックスタートガイドを参照してください。

セルフホスト型PDF生成の課題

Gotenberg / Puppeteer コンテナの問題点

課題 詳細
イメージサイズ Chromiumを含むと1GB以上。プル・デプロイに時間がかかる
メモリ消費 Chromiumプロセスが500MB〜1GB消費。OOMKillの原因になる
スケーリング レンダリングはCPU集約型。水平スケールにはリソースが必要
メンテナンス Chromiumのバージョン管理、セキュリティパッチの適用が必要
フォント管理 日本語フォントの追加、フォントキャッシュの管理が煩雑

PDF APIに委任するメリット

メリット 詳細
軽量コンテナ HTTPクライアントだけで十分。イメージサイズが大幅に削減
低リソース CPU・メモリの消費が最小限
スケール不要 PDF生成のスケーリングはAPI側が担当
フォント対応 日本語を含む多言語フォントが標準サポート
品質選択 quality(Chromium)とfast(wkhtmltopdf)を用途で使い分け

エンジンの違いや選択基準についてはHTML to PDF変換ガイドも参考にしてください。

Docker ComposeでのAPI統合

基本構成

PDF APIを使うアプリケーションのDocker Compose構成です。Chromiumコンテナが不要になるため、構成がシンプルになります。

# 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:

軽量なDockerfile

PDF生成をAPIに委任するため、Dockerfileは非常に軽量になります。Chromiumやフォントのインストールは一切不要です。

# 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 . .

# Chromiumは不要!
# RUN apt-get install -y chromium fonts-noto-cjk  # ← これが不要に

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

Gotenbergを使っていた場合との比較です。

# Before: Gotenberg使用時(イメージサイズ: ~1.5GB)
services:
  gotenberg:
    image: gotenberg/gotenberg:8
    ports:
      - "3000:3000"
    deploy:
      resources:
        limits:
          memory: 2G

# After: FUNBREW PDF API使用時(イメージサイズ: ~150MB)
services:
  app:
    build: .
    # Chromiumコンテナは不要

アプリケーションコード(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);
}

SDKの使い方は言語別クイックスタートで詳しく解説しています。請求書PDFの具体的な実装例は請求書PDF自動生成ガイドを参照してください。

環境変数ファイル

# .env(Gitにコミットしないこと)
FUNBREW_API_KEY=sk-your-api-key-here
# .env.example(リポジトリにコミット)
FUNBREW_API_KEY=sk-your-api-key-here-replace-me

APIキーの安全な管理方法についてはセキュリティガイドを参照してください。

Kubernetes(k8s)でのPDF生成サービス

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

PDF生成をAPIに委任しているため、コンテナのリソース要求が非常に小さいことに注目してください。Chromiumコンテナではmemory: 2Gi以上が必要でしたが、API呼び出しだけなら256Miで十分です。

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

シークレット管理

Kubernetes Secrets

# APIキーをSecretとして作成
kubectl create secret generic funbrew-credentials \
  --from-literal=api-key=sk-your-api-key-here

External Secrets Operator(AWS / GCP / Azure連携)

本番環境では、External Secrets Operatorを使ってクラウドプロバイダのシークレットストアと連携するのが推奨です。

# 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対応)

GitOpsワークフローを採用している場合、Sealed Secretsを使うと暗号化されたSecretをGitリポジトリに安全にコミットできます。

# Sealed Secretの作成
kubeseal --format=yaml \
  --cert=pub-cert.pem \
  < k8s/secret.yaml \
  > k8s/sealed-secret.yaml

Horizontal Pod Autoscaler(HPA)

PDF生成リクエストの負荷に応じて、Podを自動スケールさせます。

# 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

PDF生成をAPI側に任せているため、アプリケーションPodのCPU・メモリ使用量は低く保たれます。スケーリングの閾値は控えめに設定して問題ありません。

ヘルスチェックの実装

アプリケーション側のヘルスチェックエンドポイント

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

const app = express();

// Liveness: アプリが生きているか
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Readiness: リクエストを受け付けられるか
app.get('/ready', async (req, res) => {
  try {
    // 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' });
  }
});

PDF APIの可用性に依存しすぎない設計

Readiness Probeで外部APIの疎通を確認する場合、一時的なネットワークエラーでPodが不必要に除外されないよう注意が必要です。

readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3  # 3回連続失敗で初めてNot Ready
  successThreshold: 1

CI/CDパイプライン(GitHub Actions)

テスト時のPDF生成

CI/CDパイプラインでPDFの生成テストを実行する構成です。

# .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); });
          "

ビルド & デプロイ

# .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

GitHub ActionsでのPDFレポート生成

PRごとにPDFレポートを自動生成してアーティファクトとして保存する例です。

# .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: |
          # 変更内容をMarkdownで生成
          git log --oneline origin/main..HEAD > changelog.md

          # Markdown→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

Markdown→PDF変換の詳細はMarkdown to PDF APIガイドを参照してください。

Pythonアプリケーションの例(FastAPI + Docker)

FastAPIでのPDFエンドポイント

# 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))

PythonでのPDF API活用はDjango & FastAPIガイドでさらに詳しく解説しています。

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/

# Chromiumは不要!軽量イメージのまま
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

Gotenbergからの移行

Gotenbergを使っている場合、FUNBREW PDF APIへの移行は比較的簡単です。

APIコール変換の対応表

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 APIサーバー側で管理
paperWidth / paperHeight format: "A4" / format: "Letter"

移行コード例

// 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);
}

Docker Compose構成からGotenbergサービスを削除でき、インフラが大幅に簡素化されます。セルフホストとAPIの比較はPDF API比較ガイドも参考にしてください。

大量PDF生成のバッチ処理

KubernetesのJobを使って、大量のPDFをバッチ生成する構成です。

# 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() {
  // データベースからバッチ分のレコードを取得
  const records = await getRecordsBatch(batchIndex, 100);

  for (const record of records) {
    const html = renderTemplate(record);
    const response = await client.generate({
      html,
      engine: 'fast', // バッチ処理にはfastエンジンが適切
      format: 'A4',
    });

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

processBatch().catch(console.error);

バッチ処理の詳しいパターンはバッチ処理ガイドを参照してください。

本番運用のベストプラクティス

リトライとサーキットブレーカー

外部APIを呼び出すコンテナでは、リトライロジックとサーキットブレーカーの実装が重要です。

// 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秒

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

  async generate(options: any, retries = 3): Promise<Buffer> {
    // サーキットブレーカーチェック
    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();

        // 4xxエラーはリトライしない
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }

        if (attempt === retries) throw error;

        // 指数バックオフ
        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();

エラーハンドリングのパターンについてはエラーハンドリングガイドを参照してください。

監視(Prometheus + Grafana)

PDF生成のメトリクスを公開して、Prometheusで収集します。

// 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],
});

// メトリクスエンドポイント
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

NetworkPolicy

PDF APIへのアウトバウンド通信のみを許可するNetworkPolicyです。

# 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: []  # 外部API(FUNBREW PDF)への通信を許可
      ports:
        - port: 443
          protocol: TCP
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - port: 6379
          protocol: TCP

トラブルシューティング

よくある問題と対処法

問題 原因 対処
コンテナのOOMKill メモリ制限が低すぎる resources.limits.memoryを引き上げる
DNS解決エラー クラスタDNSの問題 CoreDNSの状態を確認
APIタイムアウト ネットワークポリシーがHTTPSを遮断 Egress 443を許可
Secret未設定 Secret名の不一致 kubectl get secretsで確認
イメージプル失敗 レジストリ認証エラー imagePullSecretsを設定

本番環境での運用に関するより詳しい情報は本番環境ガイドを参照してください。

デバッグコマンド

# Podの状態確認
kubectl get pods -l app=pdf-service

# ログの確認
kubectl logs -l app=pdf-service --tail=100

# Pod内でAPIへの疎通テスト
kubectl exec -it deploy/pdf-service -- \
  curl -s https://api.pdf.funbrew.cloud/v1/health

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

# HPAの状態確認
kubectl get hpa pdf-service-hpa

Next.js / Nuxtでの活用

コンテナ化されたNext.jsやNuxtアプリケーションでもFUNBREW PDF APIを同様に使えます。フロントエンドフレームワークとの統合についてはNext.js & Nuxtガイドを参照してください。

まとめ

Docker & Kubernetes環境でのPDF生成は、APIに委任することで大幅に簡素化できます。

  • Docker Compose: Gotenberg/Puppeteerコンテナが不要に。イメージサイズ1/10以下
  • Kubernetes: 低リソースDeployment + HPAで自動スケール
  • シークレット管理: External Secrets OperatorでクラウドKMSと連携
  • CI/CD: GitHub ActionsでPDF生成テスト・レポート自動生成
  • バッチ処理: k8s Jobで並列バッチ生成

Webhook連携でさらに非同期処理を強化したい場合はWebhook連携ガイド、サーバーレス環境との比較はAWS Lambda PDF生成ガイドも参考にしてください。

まずは無料アカウントを作成して、PlaygroundでAPI動作を確認してみてください。

Powered by FUNBREW PDF