Skip to main content

Open-source alternatives guide

How to Self-Host Lago — Open Source Billing 2026

Complete guide to self-hosting Lago, the open source Stripe Billing and Chargebee alternative. Docker setup, metered billing, subscriptions, invoicing.

·OSSAlt Team
Share:

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

See open source alternatives to Lago on OSSAlt.

Operational Criteria That Matter More Than Feature Checklists

Most self-hosting decisions are framed as feature comparisons, but the better question is operational fit. Can the tool be upgraded without a maintenance window that panics the team? Is configuration stored as code or trapped in a UI? Are secrets rotated cleanly? Can one engineer explain the recovery process to another in twenty minutes? These are the properties that decide whether a self-hosted service remains in production or gets abandoned after the first incident. Fancy template libraries and long integration lists help at evaluation time, but the long-term win comes from boring traits: transparent backups, predictable networking, obvious logs, and a permission model that does not require guesswork.

That is also why platform articles benefit from linking horizontally across the stack. A deployment layer does not live alone. Coolify guide is relevant whenever the real goal is reducing friction for application deploys. Dokploy guide matters when multi-node Docker or simpler PaaS ergonomics drive the decision. Gitea guide becomes part of the same conversation because source control, CI triggers, and deployment permissions are tightly coupled in practice. Treating those services as a system instead of isolated products leads to much better architecture decisions.

A Practical Adoption Path for Teams Replacing SaaS

For teams moving from SaaS, the most reliable adoption path is phased substitution. Replace one expensive or strategically sensitive service first, document the real support burden for a month, and only then expand. This does two things. First, it keeps the migration politically survivable because there is always a rollback point. Second, it turns vague arguments about self-hosting into measured trade-offs around uptime, maintenance hours, vendor lock-in, and annual spend. A good article should push readers toward that discipline rather than implying that replacing ten SaaS products in a weekend is responsible.

Another overlooked issue is platform standardization. The more heterogeneous the stack, the more hidden cost accrues in upgrades, documentation, and debugging. When two tools solve adjacent problems, teams should prefer the one that matches their existing operational model unless the feature gap is material. That is why the best self-hosting guides talk about package boundaries, reverse proxy habits, backup patterns, and team runbooks. They are not just product recommendations. They are deployment strategy.

Decision Framework for Picking the Right Fit

The simplest way to make a durable decision is to score the options against the constraints you cannot change: who will operate the system, how often it will be upgraded, whether the workload is business critical, and what kinds of failures are tolerable. That sounds obvious, but many migrations still start with screenshots and end with painful surprises around permissions, backup windows, or missing audit trails. A short written scorecard forces the trade-offs into the open. It also keeps the project grounded when stakeholders ask for new requirements halfway through rollout.

One more practical rule helps: optimize for reversibility. A good self-hosted choice preserves export paths, avoids proprietary lock-in inside the replacement itself, and can be documented well enough that another engineer could take over without archaeology. The teams that get the most value from self-hosting are not necessarily the teams with the fanciest infrastructure. They are the teams that keep their systems legible, replaceable, and easy to reason about.

Operational Criteria That Matter More Than Feature Checklists

Most self-hosting decisions are framed as feature comparisons, but the better question is operational fit. Can the tool be upgraded without a maintenance window that panics the team? Is configuration stored as code or trapped in a UI? Are secrets rotated cleanly? Can one engineer explain the recovery process to another in twenty minutes? These are the properties that decide whether a self-hosted service remains in production or gets abandoned after the first incident. Fancy template libraries and long integration lists help at evaluation time, but the long-term win comes from boring traits: transparent backups, predictable networking, obvious logs, and a permission model that does not require guesswork.

That is also why platform articles benefit from linking horizontally across the stack. A deployment layer does not live alone. Coolify guide is relevant whenever the real goal is reducing friction for application deploys. Dokploy guide matters when multi-node Docker or simpler PaaS ergonomics drive the decision. Gitea guide becomes part of the same conversation because source control, CI triggers, and deployment permissions are tightly coupled in practice. Treating those services as a system instead of isolated products leads to much better architecture decisions.

A Practical Adoption Path for Teams Replacing SaaS

For teams moving from SaaS, the most reliable adoption path is phased substitution. Replace one expensive or strategically sensitive service first, document the real support burden for a month, and only then expand. This does two things. First, it keeps the migration politically survivable because there is always a rollback point. Second, it turns vague arguments about self-hosting into measured trade-offs around uptime, maintenance hours, vendor lock-in, and annual spend. A good article should push readers toward that discipline rather than implying that replacing ten SaaS products in a weekend is responsible.

Another overlooked issue is platform standardization. The more heterogeneous the stack, the more hidden cost accrues in upgrades, documentation, and debugging. When two tools solve adjacent problems, teams should prefer the one that matches their existing operational model unless the feature gap is material. That is why the best self-hosting guides talk about package boundaries, reverse proxy habits, backup patterns, and team runbooks. They are not just product recommendations. They are deployment strategy.

Operational Criteria That Matter More Than Feature Checklists

Most self-hosting decisions are framed as feature comparisons, but the better question is operational fit. Can the tool be upgraded without a maintenance window that panics the team? Is configuration stored as code or trapped in a UI? Are secrets rotated cleanly? Can one engineer explain the recovery process to another in twenty minutes? These are the properties that decide whether a self-hosted service remains in production or gets abandoned after the first incident. Fancy template libraries and long integration lists help at evaluation time, but the long-term win comes from boring traits: transparent backups, predictable networking, obvious logs, and a permission model that does not require guesswork.

That is also why platform articles benefit from linking horizontally across the stack. A deployment layer does not live alone. Coolify guide is relevant whenever the real goal is reducing friction for application deploys. Dokploy guide matters when multi-node Docker or simpler PaaS ergonomics drive the decision. Gitea guide becomes part of the same conversation because source control, CI triggers, and deployment permissions are tightly coupled in practice. Treating those services as a system instead of isolated products leads to much better architecture decisions.

A Practical Adoption Path for Teams Replacing SaaS

For teams moving from SaaS, the most reliable adoption path is phased substitution. Replace one expensive or strategically sensitive service first, document the real support burden for a month, and only then expand. This does two things. First, it keeps the migration politically survivable because there is always a rollback point. Second, it turns vague arguments about self-hosting into measured trade-offs around uptime, maintenance hours, vendor lock-in, and annual spend. A good article should push readers toward that discipline rather than implying that replacing ten SaaS products in a weekend is responsible.

Another overlooked issue is platform standardization. The more heterogeneous the stack, the more hidden cost accrues in upgrades, documentation, and debugging. When two tools solve adjacent problems, teams should prefer the one that matches their existing operational model unless the feature gap is material. That is why the best self-hosting guides talk about package boundaries, reverse proxy habits, backup patterns, and team runbooks. They are not just product recommendations. They are deployment strategy.

The SaaS-to-Self-Hosted Migration Guide (Free PDF)

Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.

Join 300+ self-hosters. Unsubscribe in one click.