How to Self-Host Lago — Open Source Billing 2026
How to Self-Host Lago — Open Source Billing 2026
TL;DR
Lago is a fully open source, self-hostable billing and metering platform — the most complete alternative to Stripe Billing, Chargebee, and Recurly for developers who want control over their subscription and usage-based billing logic. It handles subscription plans, tiered pricing, metered billing, invoicing, coupons, credit notes, and multi-currency — all via a developer-first API. Self-hosting eliminates Chargebee's $299+/month minimum and keeps billing logic in-house where data privacy regulations require it.
Key Takeaways
- Lago: 7K+ GitHub stars, MIT + EE license (core is fully open), backed by YC
- Billing models supported: flat rate, per-seat, usage-based (metered), tiered, graduated pricing, pay-as-you-go, package pricing, percentage billing
- Invoicing: auto-generates PDF invoices with tax support (Lago Tax or custom), sends via webhook to your email provider
- Integration: works with any payment processor (Stripe, Adyen, GoCardless) via webhooks — Lago is the billing engine, not the payment processor
- Alternatives: OpenMeter (metering-only), Kill Bill (Java, enterprise), MedusaJS (e-commerce billing), Stripe Billing itself
Why Self-Host Billing Logic?
Most SaaS billing platforms take a percentage of revenue on top of subscription fees. Chargebee's entry price is $299/month. Stripe Billing adds 0.5-0.8% per transaction on top of Stripe's payment processing fees. At $100K MRR, that's $500-800/month in billing fees alone.
Beyond cost:
- GDPR and data sovereignty: billing data contains PII and financial information — some regulations require it to stay in your jurisdiction
- Complex pricing models: usage-based billing with custom formulas is difficult in Stripe Billing, trivial in Lago
- Audit trails: complete billing event history in your own database
- Multi-processor support: use Stripe in some countries, Adyen in others — Lago sits above the payment layer
Prerequisites
- VPS: 2 vCPU / 4GB RAM (Hetzner CX22 at €4.15/mo works for small volumes)
- Docker + Docker Compose
- Domain with SSL for the API endpoint
- Payment processor credentials (Stripe, Adyen, etc.) for actual charge capture — Lago doesn't process payments itself
Docker Compose Setup
Create /opt/lago/docker-compose.yml:
version: '3.8'
services:
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_DB: lago
POSTGRES_USER: lago
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lago"]
interval: 10s
redis:
image: redis:7-alpine
restart: always
volumes:
- ./redis:/data
lago-api:
image: getlago/api:latest
restart: always
environment:
LAGO_API_URL: https://billing.example.com
DATABASE_URL: postgresql://lago:${POSTGRES_PASSWORD}@db:5432/lago
REDIS_URL: redis://redis:6379
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
LAGO_RSA_PRIVATE_KEY: ${LAGO_RSA_PRIVATE_KEY}
ENCRYPTION_PRIMARY_KEY: ${ENCRYPTION_PRIMARY_KEY}
ENCRYPTION_DETERMINISTIC_KEY: ${ENCRYPTION_DETERMINISTIC_KEY}
ENCRYPTION_KEY_DERIVATION_SALT: ${ENCRYPTION_KEY_DERIVATION_SALT}
LAGO_SIDEKIQ_WEB: "true"
LAGO_SIGNUP_DISABLED: "false"
RAILS_ENV: production
# Email (for invoice delivery)
LAGO_SMTP_ADDRESS: smtp.resend.com
LAGO_SMTP_PORT: 587
LAGO_SMTP_USERNAME: resend
LAGO_SMTP_PASSWORD: ${SMTP_PASSWORD}
LAGO_FROM_EMAIL: billing@example.com
ports:
- "127.0.0.1:3000:3000"
depends_on:
db:
condition: service_healthy
volumes:
- ./storage:/app/storage
lago-worker:
image: getlago/api:latest
restart: always
command: bundle exec sidekiq
environment:
DATABASE_URL: postgresql://lago:${POSTGRES_PASSWORD}@db:5432/lago
REDIS_URL: redis://redis:6379
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
LAGO_RSA_PRIVATE_KEY: ${LAGO_RSA_PRIVATE_KEY}
RAILS_ENV: production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
lago-front:
image: getlago/front:latest
restart: always
environment:
API_URL: https://billing.example.com
APP_ENV: production
ports:
- "127.0.0.1:80:80"
volumes:
postgres:
redis:
Create .env:
POSTGRES_PASSWORD=strong-db-password
# Generate: bundle exec rails secret (or openssl rand -hex 64)
SECRET_KEY_BASE=your-secret-key-base-here
# Generate RSA key: openssl genrsa -out /tmp/lago.pem 2048 && cat /tmp/lago.pem | base64
LAGO_RSA_PRIVATE_KEY=base64-encoded-rsa-private-key
# Generate: openssl rand -hex 32 (for each)
ENCRYPTION_PRIMARY_KEY=your-primary-key
ENCRYPTION_DETERMINISTIC_KEY=your-deterministic-key
ENCRYPTION_KEY_DERIVATION_SALT=your-derivation-salt
SMTP_PASSWORD=your-smtp-password
docker compose up -d
# Wait 60 seconds for migrations to complete
# Access UI at http://localhost:80 and API at http://localhost:3000
Creating Billing Plans via API
Lago's API is the primary interface for developers. Here's a complete example of setting up a SaaS billing model:
import Lago from 'lago-javascript-client'
const lagoClient = new Lago({
apiKey: 'your-lago-api-key',
baseUrl: 'https://billing.example.com/api/v1',
})
// 1. Create a billable metric for API usage
await lagoClient.billableMetrics.createBillableMetric({
billable_metric: {
name: 'API Calls',
code: 'api_calls',
aggregation_type: 'sum_agg',
field_name: 'count',
description: 'Number of API calls made',
},
})
// 2. Create a plan with flat rate + metered usage
await lagoClient.plans.createPlan({
plan: {
name: 'Pro',
code: 'pro',
interval: 'monthly',
amount_cents: 4900, // $49/month base fee
amount_currency: 'USD',
charges: [
{
billable_metric_code: 'api_calls',
charge_model: 'graduated',
properties: {
graduated_ranges: [
{ from_value: 0, to_value: 10000, per_unit_amount: '0', flat_amount: '0' }, // first 10K free
{ from_value: 10001, to_value: 100000, per_unit_amount: '0.001', flat_amount: '0' }, // $0.001 per call
{ from_value: 100001, to_value: null, per_unit_amount: '0.0005', flat_amount: '0' }, // volume discount
],
},
},
],
},
})
// 3. Create a customer
await lagoClient.customers.createCustomer({
customer: {
external_id: 'user-123', // your app's user ID
name: 'Acme Corp',
email: 'billing@acme.com',
currency: 'USD',
billing_configuration: {
payment_provider: 'stripe',
provider_customer_id: 'cus_stripe123', // Stripe customer ID
},
},
})
// 4. Subscribe customer to plan
await lagoClient.subscriptions.createSubscription({
subscription: {
external_customer_id: 'user-123',
plan_code: 'pro',
subscription_at: new Date().toISOString(),
},
})
Ingesting Usage Events
For metered billing, send usage events as they happen in your application:
// Track each API call in real time
app.use('/api', async (req, res, next) => {
const start = Date.now()
res.on('finish', async () => {
if (req.user) {
// Send usage event to Lago
await lagoClient.events.createEvent({
event: {
transaction_id: crypto.randomUUID(), // unique per event
external_customer_id: req.user.id,
code: 'api_calls',
timestamp: new Date().toISOString(),
properties: {
count: 1,
endpoint: req.path,
method: req.method,
response_time_ms: Date.now() - start,
},
},
})
}
})
next()
})
Events are batched and processed async — Lago deduplicates on transaction_id so duplicates from retries are safe.
Webhooks for Invoice and Payment Events
Configure webhook endpoints in the Lago dashboard → Developers → Webhooks:
// app/api/lago-webhook/route.ts
export async function POST(req: Request) {
const body = await req.json()
switch (body.webhook_type) {
case 'invoice.created':
// Invoice ready — trigger payment capture via Stripe
const invoice = body.object
await stripe.paymentIntents.create({
amount: invoice.total_amount_cents,
currency: invoice.currency.toLowerCase(),
customer: invoice.customer.billing_configuration.provider_customer_id,
confirm: true,
off_session: true,
metadata: { lago_invoice_id: invoice.lago_id },
})
break
case 'invoice.payment_failure':
// Notify customer, pause subscription
await notifyPaymentFailed(body.object.customer.external_id)
break
case 'subscription.terminated':
await downgradeUserAccount(body.object.external_customer_id)
break
}
return new Response('OK')
}
Lago intentionally does not capture payments — it generates invoices and fires webhooks. Your code captures the actual charge via Stripe, Adyen, or any other processor. This separation is a feature: swap payment processors without changing billing logic.
Coupons, Credits, and Free Trials
Lago's coupon and credit system covers every common promotional billing scenario:
// Create a coupon (percentage discount)
await lagoClient.coupons.createCoupon({
coupon: {
name: 'Launch30',
code: 'LAUNCH30',
coupon_type: 'percentage',
percentage_rate: 30,
frequency: 'once',
expiration: 'time_limit',
expiration_at: '2026-06-30T23:59:59Z',
reusable: false,
},
})
// Apply coupon to a customer
await lagoClient.appliedCoupons.applyNewCoupon({
applied_coupon: {
external_customer_id: 'user-123',
coupon_code: 'LAUNCH30',
},
})
// Add credits to a customer wallet (for prepaid billing)
await lagoClient.wallets.createWallet({
wallet: {
external_customer_id: 'user-123',
name: 'Prepaid Credits',
rate_amount: '1.0', // 1 credit = $1.00
paid_credits: '100.0', // $100 added
granted_credits: '10.0', // $10 free bonus
currency: 'USD',
},
})
Free trials are configured at the plan level:
await lagoClient.plans.createPlan({
plan: {
name: 'Pro (with trial)',
code: 'pro_trial',
interval: 'monthly',
amount_cents: 4900,
amount_currency: 'USD',
trial_period: 14, // 14-day free trial before first charge
charges: [...]
},
})
During the trial period, Lago tracks usage but doesn't generate invoices. After the trial ends, it automatically starts the billing cycle and generates the first invoice.
Upgrading and Downgrading Subscriptions
// Upgrade a customer from 'starter' to 'pro' mid-cycle
await lagoClient.subscriptions.createSubscription({
subscription: {
external_customer_id: 'user-123',
plan_code: 'pro',
// 'upgrade' creates a new subscription terminating the old one
// Lago automatically prorates the remaining days
},
})
// Scheduled downgrade at next billing cycle end
await lagoClient.subscriptions.updateSubscription('sub-external-id', {
subscription: {
plan_code: 'starter',
subscription_at: 'next_billing_date', // takes effect at period end
},
})
Lago handles proration automatically — if a customer upgrades mid-month, the next invoice credits the unused portion of the old plan and charges the new plan from the upgrade date.
Nginx Configuration and Upgrading
Nginx reverse proxy for Lago API:
server {
listen 443 ssl http2;
server_name billing.example.com;
ssl_certificate /etc/letsencrypt/live/billing.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/billing.example.com/privkey.pem;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Upgrading Lago:
cd /opt/lago
# Pull latest images
docker compose pull lago-api lago-worker lago-front
# Run migrations (Lago handles this automatically on startup)
docker compose run --rm lago-api bundle exec rails db:migrate
# Restart all services
docker compose up -d --force-recreate
# Verify migrations succeeded
docker compose logs lago-api | grep -i migration
Check Lago releases before upgrading — the changelog lists breaking API changes and new features. Lago follows semver; patch releases are always safe to apply.
Backup Strategy
Billing data is critical — back up daily at minimum:
#!/bin/bash
DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR=/opt/backups/lago
mkdir -p $BACKUP_DIR
# Full PostgreSQL dump
docker compose exec -T db pg_dump -U lago lago | gzip > "$BACKUP_DIR/lago-$DATE.sql.gz"
# Verify backup isn't empty
SIZE=$(stat -c%s "$BACKUP_DIR/lago-$DATE.sql.gz")
if [ "$SIZE" -lt 1000 ]; then
echo "WARNING: backup file suspiciously small ($SIZE bytes)" | mail -s "Lago backup alert" admin@example.com
fi
# Retain 30 days for billing compliance
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete
Keep billing backups for at least 90 days — many payment regulations (PSD2, SOC 2) require long-term audit trail retention.
Cost Comparison
| Service | Monthly Fee | Revenue Fee | 100K MRR Cost |
|---|---|---|---|
| Chargebee Growth | $299/month | 0.5% | $799/month |
| Stripe Billing | ~$0 | 0.5-0.8% | $500-800/month |
| Lago Cloud | $0 (open core) | 0% | $0 + VPS |
| Lago Self-Hosted | ~€10/month VPS | 0% | €10/month |
At $100K MRR, Lago self-hosted saves $500-800/month vs managed alternatives — payback in under one month.
Monitoring Billing Health
Two Lago metrics to watch closely:
Invoice generation success rate: Failed invoice generation usually means missing customer data (no email, no billing address). Lago's Sidekiq dashboard (accessible at /sidekiq when LAGO_SIDEKIQ_WEB=true) shows job failures with full stack traces.
# Check for failed Sidekiq jobs
docker compose exec lago-api bundle exec rails runner "puts Sidekiq::DeadSet.new.size"
# If > 0, inspect failures:
docker compose exec lago-api bundle exec rails runner "Sidekiq::DeadSet.new.each { |j| puts j['class'], j['error_message'] }"
Event ingestion lag: If your app sends high-volume usage events, check that Lago is processing them in real time. Events older than your billing period that haven't been aggregated will be missed in the current invoice cycle.
# Check event processing queue
docker compose exec redis redis-cli llen sidekiq:queue:default
# Should be near 0 during off-peak. Large backlogs mean workers need scaling.
Scale workers by adding more lago-worker replicas in docker-compose.yml:
lago-worker:
image: getlago/api:latest
deploy:
replicas: 3 # add more workers for high event volume
Methodology
- Lago documentation: getlago.com/docs
- GitHub: github.com/getlago/lago (7K+ stars)
- Lago JavaScript client: github.com/getlago/lago-javascript-client
- Tested with Lago 1.x, Docker Compose, PostgreSQL 16
Browse open source alternatives to Stripe Billing and subscription management on OSSAlt.
Related: Best Open Source Alternatives to Stripe Billing 2026 · How to Self-Host Postal — Open Source Mailgun Alternative 2026