コンテナ環境で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動作を確認してみてください。