Skip to main content

How to Self-Host Lago — Open Source Billing 2026

·OSSAlt Team
lagobillingself-hostingstripe-alternativesubscriptions

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

ServiceMonthly FeeRevenue Fee100K MRR Cost
Chargebee Growth$299/month0.5%$799/month
Stripe Billing~$00.5-0.8%$500-800/month
Lago Cloud$0 (open core)0%$0 + VPS
Lago Self-Hosted~€10/month VPS0%€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

Comments