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 versionshould 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/invoiceninja2. 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 32Copy 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
EOFReplace 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: bridge5. 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 invoiceninjaOn 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/.envKey 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 withbase64:and is exactly 44 characters after that prefix. Regenerate withdocker compose exec invoiceninja php artisan key:generate --showand update.env. - Invoice PDFs failing to generate — ensure
PDF_GENERATOR=snappdfis set and the container has sufficient memory (at least 512 MB). Chromium-based PDF generation requires a headless browser subprocess; check logs forsnappdferrors and verify theninja_storagevolume 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 missingMAIL_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 refusedand retry automatically. If it fails after 60 seconds, check the MySQL container logs and verifyDB_HOST=dbmatches the service name exactly. - Client portal returning 404 — verify
APP_URLin.envexactly matches the URL in your browser, including whether you usewww.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 invoiceninjaInvoice 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
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Monica Personal CRM with Docker Compose + Caddy + MySQL
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis
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.