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

Invoicing on your own server: Invoice Ninja. Quotes, invoices, recurring bills, Stripe payments and expense tracking — and no monthly fee.

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

TaskWhat
Folder~/docker/invoiceninja/compose.yaml (app + MariaDB + nginx)
Webhttps://billing.example.com — public, with HTTPS (clients use the portal)
NeedsAPP_KEY, APP_URL, SMTP for sending invoices
First runcreate the account → Settings → Company details, tax rates, invoice design
PaymentsSettings → Online payments → Stripe (test mode first)
Backupdatabase 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:

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.

Call us