Skip to Content

Production Guide: Deploy Invoice Ninja with Docker Compose + Caddy + MySQL on Ubuntu

Self-host open-source invoicing and billing with automatic TLS

Chasing unpaid invoices, switching between spreadsheets and email, and manually reconciling payments is a drain on every small business and freelance operation. Invoice Ninja is a mature, open-source invoicing and billing platform that covers the full billing lifecycle: quotes, invoices, recurring billing, expense tracking, time tracking, client portal access, payment gateway integrations (Stripe, PayPal, Braintree, and more), and a clean REST API for automation. This guide walks through a complete production deployment of Invoice Ninja v5 using Docker Compose, Caddy as the reverse proxy with automatic TLS, and MySQL as the persistent database, all on Ubuntu 22.04 or 24.04.

Architecture and flow overview

The production stack consists of three services managed by Docker Compose:

  • Invoice Ninja (app container) — the PHP/Laravel application server handling the billing UI, REST API, and background job processing on internal port 80
  • MySQL — the relational database storing all invoices, clients, payments, expenses, and configuration; Invoice Ninja v5 requires MySQL 5.7+ or 8.0+
  • Caddy — the reverse proxy handling inbound HTTPS on ports 80 and 443, automatic TLS certificate issuance via Let's Encrypt/ACME, and forwarding HTTP traffic to the Invoice Ninja container over the internal Docker bridge network

Caddy listens externally, terminates TLS, and proxies all requests to invoiceninja:80 on the internal network. MySQL is never exposed to the host network. Docker named volumes persist the database data directory, Invoice Ninja's public storage folder (uploaded files, attachments, logos), and Caddy's certificate store across container restarts and upgrades.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with at least 1 vCPU and 1 GB RAM (2 GB recommended for active billing environments)
  • Docker Engine 24+ and Docker Compose v2 installed (docker compose version should return v2.x)
  • A domain name (or subdomain like billing.yourdomain.com) with an A record pointing to the server's public IP
  • Ports 80 and 443 open in the firewall: ufw allow 80 && ufw allow 443
  • A valid email address for Let's Encrypt certificate notifications and optionally for outbound SMTP

Step-by-step deployment

1. Create the project directory

mkdir -p /opt/invoiceninja && cd /opt/invoiceninja

2. Generate application secrets

Invoice Ninja requires a 32-character APP_KEY for Laravel encryption. Generate one now and store it — you will need it in the next step:

openssl rand -base64 32

Copy the output. It will serve as your APP_KEY. Also generate a strong MySQL root password and application database password.

3. Create the environment file

cat > /opt/invoiceninja/.env << 'EOF'
APP_ENV=production
APP_DEBUG=false
APP_URL=https://billing.yourdomain.com
APP_KEY=base64:REPLACE_WITH_GENERATED_KEY

REQUIRE_HTTPS=true
PHANTOMJS_PDF_GENERATION=false
PDF_GENERATOR=snappdf

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=invoiceninja
DB_USERNAME=ninja
DB_PASSWORD=change_this_strong_db_password

MYSQL_ROOT_PASSWORD=change_this_root_password
MYSQL_DATABASE=invoiceninja
MYSQL_USER=ninja
MYSQL_PASSWORD=change_this_strong_db_password

MAIL_MAILER=smtp
MAIL_HOST=smtp.yourdomain.com
MAIL_PORT=587
[email protected]
MAIL_PASSWORD=your_smtp_password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="Your Company Invoices"

[email protected]
IN_PASSWORD=change_this_admin_password
EOF

Replace every placeholder before starting the stack. IN_USER_EMAIL and IN_PASSWORD are used for the initial admin account created on first boot.

4. Write the Docker Compose file

version: "3.9"

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    env_file: .env
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - backend

  invoiceninja:
    image: invoiceninja/invoiceninja:5
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:9000:80"
    volumes:
      - ninja_public:/var/www/app/public
      - ninja_storage:/var/www/app/storage
    depends_on:
      - db
    networks:
      - backend

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - backend

volumes:
  mysql_data:
  ninja_public:
  ninja_storage:
  caddy_data:
  caddy_config:

networks:
  backend:
    driver: bridge

5. Write the Caddyfile

billing.yourdomain.com {
    reverse_proxy invoiceninja:80
    encode gzip
    header {
        X-Frame-Options SAMEORIGIN
        X-Content-Type-Options nosniff
        Referrer-Policy strict-origin-when-cross-origin
    }
}

Replace billing.yourdomain.com with your actual domain. Caddy provisions and renews TLS certificates automatically.

6. Start the stack and initialize

cd /opt/invoiceninja
docker compose up -d
docker compose logs -f invoiceninja

On first boot, Invoice Ninja runs database migrations and seeds the initial admin account using IN_USER_EMAIL and IN_PASSWORD. Watch the logs until you see application startup complete — typically 30–60 seconds. Then open https://billing.yourdomain.com and log in with your admin credentials.

Configuration and secrets handling

All sensitive values live in /opt/invoiceninja/.env. Lock down permissions immediately:

chmod 600 /opt/invoiceninja/.env
chown root:root /opt/invoiceninja/.env

Key configuration points:

  • APP_KEY — the Laravel encryption key; if you change it after data is stored, existing encrypted data (PDF keys, tokens) becomes unreadable. Back it up alongside your database.
  • APP_URL — must exactly match the domain you use to access the app; Invoice Ninja embeds this URL in invoice PDFs, client portal links, and webhook payloads
  • REQUIRE_HTTPS=true — enforces HTTPS redirects at the application layer in addition to Caddy's TLS termination
  • PDF_GENERATOR=snappdf — uses Chromium-based PDF generation for invoice PDFs; this is the recommended mode for Docker deployments (no PhantomJS dependency)
  • MAIL_* — configure your outbound SMTP provider; Invoice Ninja sends invoice emails, payment receipts, and password reset links via this connection

To add a payment gateway, log in as admin, navigate to Settings → Payment Gateways, and connect Stripe, PayPal, or another supported provider. Gateway credentials are stored encrypted in the database using your APP_KEY.

Verification

# All containers should show "Up"
docker compose ps

# Invoice Ninja health check
curl -s https://billing.yourdomain.com/api/v1/ping

# Verify MySQL is healthy
docker compose exec db mysqladmin -u ninja -p status

# Check Caddy TLS certificate issuer
curl -sv https://billing.yourdomain.com 2>&1 | grep -E "subject|issuer"

The /api/v1/ping endpoint returns {"message":"pong"} when the application is up and connected to the database.

Common issues and fixes

  • Blank white screen after login — almost always a misconfigured APP_KEY. Verify the key starts with base64: and is exactly 44 characters after that prefix. Regenerate with docker compose exec invoiceninja php artisan key:generate --show and update .env.
  • Invoice PDFs failing to generate — ensure PDF_GENERATOR=snappdf is set and the container has sufficient memory (at least 512 MB). Chromium-based PDF generation requires a headless browser subprocess; check logs for snappdf errors and verify the ninja_storage volume is writable.
  • Emails not sending — test SMTP connectivity from inside the container: docker compose exec invoiceninja php artisan ninja:check-data. Common causes include firewall blocking port 587 outbound, incorrect SMTP credentials, or missing MAIL_ENCRYPTION=tls.
  • Database connection refused on first start — MySQL needs 10–20 seconds to initialize on first boot. Invoice Ninja may log SQLSTATE[HY000] Connection refused and retry automatically. If it fails after 60 seconds, check the MySQL container logs and verify DB_HOST=db matches the service name exactly.
  • Client portal returning 404 — verify APP_URL in .env exactly matches the URL in your browser, including whether you use www. prefix. Mismatched URLs cause Laravel routing to reject portal requests.

FAQ

Can I import clients and invoices from FreshBooks, QuickBooks, or Wave?

Yes. Invoice Ninja supports CSV import for clients, invoices, expenses, and products under Settings → Import/Export. For FreshBooks and QuickBooks, export each entity to CSV from the source application, map column headers to Invoice Ninja's fields in the import wizard, and run the import. Complex history with thousands of records may require batching the CSV files.

How do I upgrade Invoice Ninja to a new version?

cd /opt/invoiceninja
docker compose pull invoiceninja
docker compose up -d invoiceninja

Invoice Ninja runs database migrations automatically on startup. Always back up the MySQL volume and the ninja_storage volume before pulling a new image across minor or major version boundaries.

How do I back up all Invoice Ninja data?

# Database backup
docker compose exec db mysqldump -u ninja -p invoiceninja | gzip > /backup/invoiceninja_$(date +%Y%m%d).sql.gz

# Storage backup (logos, PDFs, attachments)
tar czf /backup/ninja_storage_$(date +%Y%m%d).tar.gz \
  $(docker volume inspect invoiceninja_ninja_storage --format '{{ .Mountpoint }}')

Does Invoice Ninja support multi-company or multi-currency billing?

Yes to both. Invoice Ninja v5 supports multiple companies under a single installation — each company has its own clients, invoices, settings, and payment gateways, all accessible from the same login. Multi-currency is enabled per invoice: set the currency in the client profile or override it per invoice. Exchange rates can be configured manually or pulled automatically.

How do I set up recurring invoices?

Navigate to Invoices → Recurring, create a new recurring invoice, set the frequency (weekly, monthly, quarterly, or custom), and specify the start date and optional end date. Invoice Ninja generates and optionally auto-sends the invoice on each cycle using the configured SMTP settings. You can also set automatic payment charging if the client has a saved payment method through a connected gateway.

Is Invoice Ninja suitable for VAT/GST and international tax compliance?

Invoice Ninja supports configurable tax rates, multiple tax rules per line item, and tax-inclusive or tax-exclusive pricing. Navigate to Settings → Tax Rates to define rates. For EU VAT MOSS or complex multi-jurisdiction rules, review the built-in tax report under Reports → Tax Summary which aggregates collected taxes by rate for filing periods. The platform does not automatically calculate jurisdiction-specific tax rates; you define the rules that match your legal obligations.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Planka with Docker Compose + Caddy + PostgreSQL on Ubuntu
Self-host a fast, open-source Trello-alternative kanban board with automatic TLS, PostgreSQL persistence, and production-ready secrets handling.