Carbone.io: Self-Hosted Document Generation Made Easy
Document generation is a common requirement in enterprise applications—invoices, contracts, reports, certificates, and more. While there are many SaaS solutions available, privacy concerns, compliance requirements, and cost considerations often make self-hosted solutions more attractive. Enter Carbone.io, a powerful, open-source document generation engine that you can run on your own infrastructure.
What is Carbone?
Carbone is a template-based document generator that creates PDF, DOCX, XLSX, ODT, ODS, and other formats from JSON data. It uses LibreOffice-compatible templates with a simple templating syntax, making it accessible to non-developers while remaining powerful enough for complex use cases.
Key Features
- Template-Based: Use familiar tools like Microsoft Word, Excel, or LibreOffice to design templates
- Multiple Formats: Generate PDF, DOCX, XLSX, PPTX, ODT, ODS, and more
- Self-Hosted: Complete control over your data and infrastructure
- JSON Data Injection: Simple data binding with powerful formatting options
- Conditionals & Loops: Dynamic content based on your data
- Image Support: Insert images from URLs or base64 data
- Multi-Language: Support for any language with proper fonts
- No Dependencies: Works without internet access once set up
Installation Options
Option 1: Docker (Recommended for Production)
The easiest way to get started is using Docker:
# Pull the official Carbone image
docker pull carbone/carbone-ee
# Run Carbone container
docker run -d \
--name carbone \
-p 4000:4000 \
carbone/carbone-ee
For docker-compose:
version: '3.8'
services:
carbone:
image: carbone/carbone-ee
container_name: carbone-server
ports:
- "4000:4000"
environment:
- CARBONE_WORKERS=4
volumes:
- ./templates:/app/templates
- ./output:/app/output
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
Option 2: Node.js Library
For integrating directly into your Node.js application:
npm install carbone
Option 3: Carbone Cloud API (Optional)
If you prefer managed hosting for some use cases, Carbone also offers a cloud API with a generous free tier.
Getting Started
1. Creating Your First Template
Create a simple invoice template in Microsoft Word or LibreOffice Writer:
invoice_template.docx:
INVOICE #{d.invoiceNumber}
Date: {d.date}
Customer: {d.customer.name}
Items:
{d.items[i].description} {d.items[i].quantity} x ${d.items[i].price} = ${d.items[i].total}
Total: ${d.total}
2. Generating a Document with Node.js
const carbone = require('carbone');
const fs = require('fs');
// Your data
const data = {
invoiceNumber: 'INV-2025-001',
date: '2025-10-02',
customer: {
name: 'Acme Corporation',
email: '[email protected]'
},
items: [
{ description: 'Web Development', quantity: 40, price: 150, total: 6000 },
{ description: 'Consulting', quantity: 10, price: 200, total: 2000 }
],
total: 8000
};
// Generate document
carbone.render('./templates/invoice_template.docx', data, (err, result) => {
if (err) {
console.error('Error generating document:', err);
return;
}
// Write result to file
fs.writeFileSync('invoice_INV-2025-001.pdf', result);
console.log('Invoice generated successfully!');
});
3. Converting to PDF
To convert to PDF, specify the convertTo
option:
const options = {
convertTo: 'pdf'
};
carbone.render('./templates/invoice_template.docx', data, options, (err, result) => {
if (err) return console.error(err);
fs.writeFileSync('invoice.pdf', result);
});
Advanced Features
Conditional Rendering
Show or hide content based on your data:
Template:
{d.customer.isPremium:ifEQ(true):show}
Thank you for being a premium customer!
{d.customer.isPremium:show}
Loops and Iterations
Iterate over arrays in your data:
Template:
Order Items:
{d.items[i].name} {d.items[i].quantity} ${d.items[i].price}
Formatters
Apply formatting to your data:
Date: {d.date:formatD(YYYY-MM-DD)}
Amount: {d.amount:formatC(USD)}
Percentage: {d.rate:formatN(0.00)}%
Common formatters:
formatD
: Date formattingformatC
: Currency formattingformatN
: Number formattingupper
: Uppercase textlower
: Lowercase textconvCRLF
: Convert line breaks
Image Insertion
Insert images from URLs or base64:
Template (in Word):
- Insert a placeholder image
- Add alt text:
{d.productImage}
Data:
const data = {
productImage: 'https://example.com/product.jpg'
// or base64: 'data:image/png;base64,iVBORw0KG...'
};
Nested Data Structures
Handle complex nested objects:
const data = {
company: {
name: 'Tech Solutions Inc',
address: {
street: '123 Main St',
city: 'San Francisco',
country: 'USA'
}
}
};
Template:
{d.company.name}
{d.company.address.street}
{d.company.address.city}, {d.company.address.country}
Dynamic Tables
Create dynamic tables with variable rows:
Template (Excel/Word table):
Product Name | Quantity | Price | Total
{d.items[i].name} | {d.items[i].qty} | {d.items[i].price} | {d.items[i].total}
Real-World Use Cases
1. Invoice Generation System
const express = require('express');
const carbone = require('carbone');
const app = express();
app.post('/api/invoices/generate', async (req, res) => {
const { customer, items } = req.body;
// Calculate totals
const subtotal = items.reduce((sum, item) => sum + item.total, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
const data = {
invoiceNumber: `INV-${Date.now()}`,
date: new Date().toISOString().split('T')[0],
customer,
items,
subtotal,
tax,
total
};
const options = { convertTo: 'pdf' };
carbone.render('./templates/invoice.docx', data, options, (err, result) => {
if (err) {
return res.status(500).json({ error: 'Generation failed' });
}
res.contentType('application/pdf');
res.send(result);
});
});
app.listen(3000);
2. Certificate Generator
async function generateCertificate(studentData) {
const data = {
studentName: studentData.name,
courseName: studentData.course,
completionDate: new Date().toLocaleDateString(),
instructorName: 'Dr. Jane Smith',
certificateId: `CERT-${studentData.id}-${Date.now()}`
};
return new Promise((resolve, reject) => {
carbone.render(
'./templates/certificate.docx',
data,
{ convertTo: 'pdf' },
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
}
3. Contract Generation with E-Signature
const carbone = require('carbone');
const PDFLib = require('pdf-lib');
async function generateSignableContract(contractData) {
// Generate base contract
const contractPdf = await new Promise((resolve, reject) => {
carbone.render(
'./templates/contract.docx',
contractData,
{ convertTo: 'pdf' },
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
// Add signature fields using pdf-lib
const pdfDoc = await PDFLib.PDFDocument.load(contractPdf);
const pages = pdfDoc.getPages();
const lastPage = pages[pages.length - 1];
// Add signature placeholder
lastPage.drawText('Signature: ___________________', {
x: 50,
y: 100,
size: 12
});
const finalPdf = await pdfDoc.save();
return finalPdf;
}
4. Bulk Report Generation
async function generateMonthlyReports(customers) {
const reports = [];
for (const customer of customers) {
const data = {
customerName: customer.name,
month: 'October 2025',
transactions: customer.transactions,
totalSpent: customer.totalSpent,
charts: {
spendingTrend: customer.chartUrl
}
};
const report = await new Promise((resolve, reject) => {
carbone.render(
'./templates/monthly_report.xlsx',
data,
{ convertTo: 'pdf' },
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
reports.push({
customerId: customer.id,
report
});
}
return reports;
}
5. Multi-Language Documents
const translations = {
en: { title: 'Invoice', total: 'Total', date: 'Date' },
fr: { title: 'Facture', total: 'Total', date: 'Date' },
es: { title: 'Factura', total: 'Total', date: 'Fecha' }
};
function generateLocalizedInvoice(data, locale = 'en') {
const labels = translations[locale];
const localizedData = {
...data,
labels
};
carbone.render(
'./templates/invoice_i18n.docx',
localizedData,
{ convertTo: 'pdf', lang: locale },
(err, result) => {
// Handle result
}
);
}
6. Generate Documents on the Fly with REST API
If you’re running Carbone as a service (using Carbone EE or Carbone Cloud), you can generate documents via HTTP API without storing templates on disk.
API Endpoint Setup
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// Carbone server configuration
const CARBONE_URL = process.env.CARBONE_URL || 'http://localhost:4000';
const CARBONE_VERSION = '5';
app.post('/api/generate-document', async (req, res) => {
try {
const { templateBase64, data, convertTo = 'pdf', lang = 'en-US' } = req.body;
// Generate document using Carbone API
const response = await axios.post(
`${CARBONE_URL}/render/template?download=true`,
{
data: data,
template: templateBase64,
enum: {},
translations: {},
isDebugActive: false,
convertTo: convertTo,
lang: lang
},
{
headers: {
'Content-Type': 'application/json',
'carbone-version': CARBONE_VERSION
},
responseType: 'arraybuffer'
}
);
// Set appropriate content type
const contentType = convertTo === 'pdf'
? 'application/pdf'
: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', `attachment; filename="document.${convertTo}"`);
res.send(response.data);
} catch (error) {
console.error('Error generating document:', error.message);
res.status(500).json({
error: 'Failed to generate document',
details: error.response?.data || error.message
});
}
});
app.listen(3000, () => {
console.log('Document generation API running on port 3000');
});
Complete Example with Template Conversion
const fs = require('fs');
const axios = require('axios');
// Helper: Convert template file to base64
function templateToBase64(templatePath) {
const fileBuffer = fs.readFileSync(templatePath);
return fileBuffer.toString('base64');
}
// Generate document on the fly
async function generateDocumentOnTheFly(templatePath, data, options = {}) {
const {
convertTo = 'pdf',
lang = 'en-US',
isDebugActive = false,
download = true
} = options;
// Convert template to base64
const templateBase64 = templateToBase64(templatePath);
try {
const response = await axios.post(
`http://localhost:4000/render/template?download=${download}`,
{
data: data,
template: templateBase64,
enum: {},
translations: {},
isDebugActive: isDebugActive,
convertTo: convertTo,
lang: lang
},
{
headers: {
'Content-Type': 'application/json',
'carbone-version': '5'
},
responseType: 'arraybuffer'
}
);
return response.data;
} catch (error) {
console.error('Carbone API Error:', error.response?.data || error.message);
throw error;
}
}
// Usage example
async function example() {
const data = {
firstname: 'John',
lastname: 'Doe',
company: 'Tech Solutions Inc',
position: 'Senior Developer'
};
try {
const pdfBuffer = await generateDocumentOnTheFly(
'./templates/certificate.docx',
data,
{ convertTo: 'pdf', lang: 'en-US' }
);
// Save generated PDF
fs.writeFileSync('certificate.pdf', pdfBuffer);
console.log('Certificate generated successfully!');
} catch (error) {
console.error('Failed to generate certificate:', error);
}
}
example();
Using with Frontend (React/Vue)
// Frontend service to call your backend API
async function generateDocument(templateName, data, format = 'pdf') {
try {
const response = await fetch('/api/generate-document', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
templateBase64: await fetchTemplateAsBase64(templateName),
data: data,
convertTo: format,
lang: 'en-US'
})
});
if (!response.ok) {
throw new Error('Document generation failed');
}
// Download the file
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `document.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Helper to fetch template from server and convert to base64
async function fetchTemplateAsBase64(templateName) {
const response = await fetch(`/api/templates/${templateName}`);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Usage in React component
function CertificateGenerator() {
const [loading, setLoading] = useState(false);
const handleGenerate = async () => {
setLoading(true);
try {
await generateDocument('certificate', {
firstname: 'Jane',
lastname: 'Smith',
courseName: 'Advanced JavaScript',
completionDate: new Date().toLocaleDateString()
}, 'pdf');
alert('Certificate generated successfully!');
} catch (error) {
alert('Failed to generate certificate');
} finally {
setLoading(false);
}
};
return (
<button onClick={handleGenerate} disabled={loading}>
{loading ? 'Generating...' : 'Generate Certificate'}
</button>
);
}
Debug Mode for Development
When developing templates, enable debug mode to see detailed information:
const response = await axios.post(
'http://localhost:4000/render/template?download=true',
{
data: {
firstname: 'Will',
lastname: 'Smith'
},
template: templateBase64,
enum: {},
translations: {},
isDebugActive: true, // Enable debug mode
convertTo: 'html', // Use HTML to see rendered output
lang: 'en-US'
},
{
headers: {
'Content-Type': 'application/json',
'carbone-version': '5'
}
}
);
console.log('Debug output:', response.data);
Error Handling and Validation
async function generateDocumentWithValidation(templatePath, data, options) {
// Validate input
if (!fs.existsSync(templatePath)) {
throw new Error(`Template not found: ${templatePath}`);
}
if (!data || typeof data !== 'object') {
throw new Error('Data must be a valid object');
}
const validFormats = ['pdf', 'docx', 'xlsx', 'html', 'odt', 'ods'];
if (options.convertTo && !validFormats.includes(options.convertTo)) {
throw new Error(`Invalid format: ${options.convertTo}`);
}
try {
const templateBase64 = templateToBase64(templatePath);
const response = await axios.post(
'http://localhost:4000/render/template?download=true',
{
data: data,
template: templateBase64,
enum: options.enum || {},
translations: options.translations || {},
isDebugActive: options.debug || false,
convertTo: options.convertTo || 'pdf',
lang: options.lang || 'en-US'
},
{
headers: {
'Content-Type': 'application/json',
'carbone-version': '5'
},
responseType: 'arraybuffer',
timeout: 30000 // 30 second timeout
}
);
return {
success: true,
data: response.data,
format: options.convertTo || 'pdf'
};
} catch (error) {
return {
success: false,
error: error.response?.data || error.message,
statusCode: error.response?.status
};
}
}
// Usage with proper error handling
async function main() {
const result = await generateDocumentWithValidation(
'./templates/invoice.docx',
{
firstname: 'Will',
lastname: 'Smith',
invoiceNumber: 'INV-2025-001'
},
{
convertTo: 'pdf',
lang: 'en-US',
debug: false
}
);
if (result.success) {
fs.writeFileSync(`output.${result.format}`, result.data);
console.log('Document generated successfully');
} else {
console.error('Generation failed:', result.error);
}
}
Self-Hosting Best Practices
1. Resource Management
# docker-compose.yml with resource limits
services:
carbone:
image: carbone/carbone-ee
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
2. Template Versioning
const templateVersions = {
'invoice_v1': './templates/invoice_v1.docx',
'invoice_v2': './templates/invoice_v2.docx'
};
function generateDocument(data, templateVersion = 'invoice_v2') {
const templatePath = templateVersions[templateVersion];
carbone.render(templatePath, data, options, callback);
}
3. Caching Strategy
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 3600 });
async function generateWithCache(templateName, data) {
const cacheKey = `${templateName}_${JSON.stringify(data)}`;
// Check cache
const cached = cache.get(cacheKey);
if (cached) return cached;
// Generate new document
const document = await generateDocument(templateName, data);
// Cache result
cache.set(cacheKey, document);
return document;
}
4. Queue Management for High Volume
const Queue = require('bull');
const documentQueue = new Queue('document-generation', {
redis: { host: 'localhost', port: 6379 }
});
// Add job to queue
documentQueue.add('generate-invoice', {
template: 'invoice',
data: invoiceData,
userId: user.id
});
// Process jobs
documentQueue.process('generate-invoice', async (job) => {
const { template, data } = job.data;
const document = await new Promise((resolve, reject) => {
carbone.render(`./templates/${template}.docx`, data, { convertTo: 'pdf' },
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
// Save to storage
await saveDocument(job.data.userId, document);
return { status: 'completed' };
});
5. Monitoring and Logging
const logger = require('winston');
function generateDocumentWithLogging(template, data) {
const startTime = Date.now();
logger.info('Document generation started', {
template,
dataSize: JSON.stringify(data).length
});
carbone.render(template, data, { convertTo: 'pdf' }, (err, result) => {
const duration = Date.now() - startTime;
if (err) {
logger.error('Document generation failed', {
template,
error: err.message,
duration
});
return;
}
logger.info('Document generation completed', {
template,
duration,
size: result.length
});
});
}
Performance Optimization
1. Parallel Processing
const Promise = require('bluebird');
async function generateBatch(templates, dataArray) {
return Promise.map(
dataArray,
(data, index) => generateDocument(templates[index], data),
{ concurrency: 5 } // Limit concurrent generations
);
}
2. Template Preloading
const templateCache = new Map();
function preloadTemplates() {
const templates = ['invoice', 'contract', 'report'];
templates.forEach(name => {
const buffer = fs.readFileSync(`./templates/${name}.docx`);
templateCache.set(name, buffer);
});
}
// Use cached template
function renderFromCache(templateName, data) {
const template = templateCache.get(templateName);
carbone.render(template, data, callback);
}
Security Considerations
1. Input Validation
const Joi = require('joi');
const invoiceSchema = Joi.object({
invoiceNumber: Joi.string().pattern(/^INV-\d+$/).required(),
customer: Joi.object({
name: Joi.string().max(100).required(),
email: Joi.string().email().required()
}),
items: Joi.array().items(
Joi.object({
description: Joi.string().max(200),
quantity: Joi.number().positive(),
price: Joi.number().positive()
})
)
});
function validateAndGenerate(data) {
const { error, value } = invoiceSchema.validate(data);
if (error) throw new Error('Invalid data');
return generateInvoice(value);
}
2. Template Sandboxing
// Restrict template access to specific directory
const path = require('path');
function sanitizeTemplatePath(templateName) {
const basePath = path.resolve('./templates');
const templatePath = path.resolve(basePath, templateName);
// Prevent path traversal
if (!templatePath.startsWith(basePath)) {
throw new Error('Invalid template path');
}
return templatePath;
}
Comparison with Alternatives
Feature | Carbone | Puppeteer/HTML2PDF | Gotenberg | PhantomJS |
---|---|---|---|---|
Self-hosted | ✅ | ✅ | ✅ | ✅ |
Office formats | ✅ | ❌ | ✅ | ❌ |
Template editing | Easy (Word/Excel) | Hard (HTML/CSS) | Medium | Hard |
Performance | Fast | Slow | Fast | Deprecated |
Memory usage | Low | High | Medium | High |
PDF quality | Excellent | Good | Excellent | Good |
Conclusion
Carbone.io is an excellent choice for self-hosted document generation, offering:
- Developer-friendly: Simple API with minimal learning curve
- Business-friendly: Non-technical users can create and edit templates
- Cost-effective: No per-document fees for self-hosted deployment
- Flexible: Support for multiple formats and complex use cases
- Performant: Fast generation even for complex documents
- Privacy: Complete control over sensitive data
Whether you’re building an invoicing system, certificate generator, or automated reporting tool, Carbone provides the flexibility and power you need while keeping your data under your control.
Resources
Happy document generating!