How to Self-Host Invoicing with Invoice Ninja (Quotes, Invoices and Online Payment for a Small Business)

A sole trader or small business pays £10–30 a month for invoicing software that holds its customer list and accounts. Invoice Ninja is the open-source version of that whole product: clients, quotes, invoices, recurring invoices, payments (Stripe, PayPal, GoCardless), expenses, time tracking, projects, a client portal where customers view and pay — in a container on your server, free. It's the one app in this series that absolutely has to be backed up and kept online, because it's the books.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/invoiceninja/compose.yaml (app + MariaDB + nginx) |
| Web | https://billing.example.com — public, with HTTPS (clients use the portal) |
| Needs | APP_KEY, APP_URL, SMTP for sending invoices |
| First run | create the account → Settings → Company details, tax rates, invoice design |
| Payments | Settings → Online payments → Stripe (test mode first) |
| Backup | database dump + ./storage nightly; off-site (54) |
1. The compose file
Invoice Ninja's official compose is three containers (the PHP app, a web server, the database) plus a config file. Use their docker-compose.yml and env from the GitHub dockerfiles repo as the base — the shape:
mkdir -p ~/docker/invoiceninja && cd ~/docker/invoiceninja
git clone --depth 1 https://github.com/invoiceninja/dockerfiles .
Edit env:
APP_URL=https://billing.example.com
APP_KEY=base64:GENERATE_ME
APP_DEBUG=false
REQUIRE_HTTPS=true
MULTI_DB_ENABLED=false
DB_HOST=db
DB_DATABASE=ninja
DB_USERNAME=ninja
DB_PASSWORD=long-random
MYSQL_ROOT_PASSWORD=another-long-random
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=billing@example.com
MAIL_PASSWORD=app-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=billing@example.com
MAIL_FROM_NAME="Your Business"
IN_USER_EMAIL=you@example.com
IN_PASSWORD=your-first-login-password
TRUSTED_PROXIES=*
Generate the key: docker run --rm -it invoiceninja/invoiceninja:5 php artisan key:generate --show. The MAIL settings are what send invoices to clients (a dedicated address looks professional; the Gmail app password from 49 works, a proper domain mailbox is better).
docker compose up -d
docker compose logs -f app # wait for migrations
2. Proxy
The bundled nginx listens on 80 inside the stack; map it to 127.0.0.1:8095 and proxy billing.example.com → it (28), HTTPS, client_max_body_size 50M. This one is public — clients open invoice links and pay — so a real certificate (27), and CrowdSec (107) in front.
3. First run
Open https://billing.example.com, log in with IN_USER_EMAIL / IN_PASSWORD. Then Settings:
- Company details: name, address, logo, VAT number if registered.
- Localization: GBP, en-GB, date format, first day of week.
- Tax settings: a VAT rate (20%) if applicable; or none.
- Invoice design: pick a template, set the number pattern (INV-{$year}-{$counter}), payment terms (14 days), footer with bank details.
- Email settings: the from name/address; send yourself a test invoice.
- Account management: enable 2FA.
4. The daily flow
Clients → New: contact, address, email. Quotes → New: line items from a Products list you build once (Callout, Hourly labour, SSD 1TB…), send it; the client approves in the portal; Convert to invoice. Send; the client gets an email with a View invoice link to the portal and a Pay now button. Record a bank transfer by hand, or let Stripe take the card. Recurring invoices for monthly retainers; Expenses with receipt photos (link to Paperless, 76); Tasks with a timer for hourly work that roll into invoices; Reports for the accountant at year end.
5. Payments
Settings → Online payments → Stripe: connect with your Stripe account's keys (test mode first — a 4242 4242 4242 4242 card). Fees are Stripe's; nothing extra. GoCardless for direct debit, PayPal, or just the bank details in the footer. Payments update the invoice and the client's statement automatically.
6. Back it up like it matters
It's your accounts and your clients' data: nightly database dump (65) + ./storage (documents, logos) + the env file, in restic (55) and off-site (54), with the drill (159) including "restore Invoice Ninja and open last month's invoice". Keep the box updated (36, 50 — pin the major version, read release notes). Uptime Kuma (35) watching the public URL. If you handle card data, Stripe handles the PCI side; your job is HTTPS, updates and access control.
Troubleshooting
Emails don't send. The MAIL settings; docker compose logs app | grep -i mail. Send a test from Settings → Email settings.
Client portal links show http. APP_URL and REQUIRE_HTTPS, and the proxy must send X-Forwarded-Proto.
PDFs blank or missing. The PDF generator (built-in or snappdf) needs the right image/flags; the official compose includes it — check PDF_GENERATOR in env.
Upgrade broke the UI. Clear caches: docker compose exec app php artisan optimize:clear, and run migrations: php artisan migrate.
Wrapping up
The official compose with your env, a public HTTPS name, company and tax settings, a product list, SMTP that works, Stripe in test mode then live. Quotes become invoices become payments, the client portal does the chasing, the books are on your server — and it's backed up three ways because they're the books.
