Invoice Ninja
Invoice Ninja issues invoices and quotes, tracks payments, and gives each client a portal to view and pay them. Four containers: the application with its scheduler and queue workers, a web server, a database, and a cache.
Verified 29 Jul 2026 against 5.13.26
Not exercised: a restore — the backup is configured but has not been rehearsed.
Needs: Traefik running, a domain, and SMTP credentials.
Installation
Section titled “Installation”From business/invoiceninja/:
-
Terminal window cp .env.example .env -
Set the domain, the timezone, and the address the application will send from:
APP_TRAEFIK_HOST=invoice.example.comTZ=Europe/ViennaIN_USER_EMAIL=you@example.com -
Tell it which proxy it may believe about the client’s address. Without this the application sees every visitor as the proxy, and abuse detection then measures the wrong thing:
Terminal window docker network inspect proxy-public \--format '{{range .IPAM.Config}}{{.Subnet}} {{end}}'Copy the result into
TRUSTED_PROXIES, comma-separated. With IPv6 enabled there are two. -
Set mail. Port 465 is encrypted from the first byte; 587 starts unencrypted and switches over partway:
MAIL_MAILER=smtpMAIL_HOST=smtp.example.comMAIL_PORT=465MAIL_ENCRYPTION=sslMAIL_USERNAME=your-smtp-loginMAIL_FROM_ADDRESS=invoices@example.com -
Create the secrets. Nothing sensitive goes in
.env:Terminal window mkdir -p .secretsopenssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/db_root_pwd.txtopenssl rand -hex 32 | tr -d '\n' > .secrets/redis_pwd.txtopenssl rand -base64 24 | tr -d '\n' > .secrets/in_pwd.txtprintf 'your-smtp-password' > .secrets/mail_pwd.txtchmod 600 .secrets/*.txtin_pwd.txtis the first administrator’s password. It is read on the first start only — note it down, then change it in the interface. -
Let the application generate its own encryption key. It has to exist before the first start, and it must carry the
base64:prefix that Laravel’s own generator produces:Terminal window docker run --rm "invoiceninja/invoiceninja-debian:$(grep '^APP_TAG=' .env | cut -d= -f2)" \php artisan key:generate --show | tr -d '\n' > .secrets/app_key.txtchmod 600 .secrets/app_key.txtThe tag comes from the
.envyou copied in step 1, so the key is generated by the same image the stack will run. -
Start it:
Terminal window docker compose up -ddocker compose logs -f appThe first start runs migrations and seeds the database — one to three minutes. Wait for
Production setup completed, then the four supervisor programs reportingRUNNING.
Verify
Section titled “Verify”docker compose psdocker compose exec app supervisorctl -c /etc/supervisor/supervisord.conf statusExpected: four containers healthy, and php-fpm, both queue workers and the
scheduler all RUNNING. The queue workers send the mail and the scheduler
raises recurring invoices, so a stack where only php-fpm runs looks fine and
quietly does half its job.
Then log in with IN_USER_EMAIL and the password from .secrets/in_pwd.txt,
and change it.
Backup
Section titled “Backup”Two things carry the state, and one of them is not in a volume:
| Where | |
|---|---|
| Database | dumped through Borgmatic’s hook, not copied |
| Attachments, logos, generated PDFs | the app_storage volume |
| The encryption key | .secrets/app_key.txt — without it the database restores unreadable |
source_directories: - /srv/blueprint - /var/lib/docker/volumes/invoiceninja_app_storage/_data
mysql_databases: - name: ninja container: invoiceninja-mysql username: ninja password: "{credential file /srv/blueprint/business/invoiceninja/.secrets/db_pwd.txt}" tls: falseNo quiescing hook: the dump is consistent on its own. app_public is left out —
the image rebuilds it on every start.
Back up before every update. Invoice Ninja migrates its database on start, and a failed migration with no dump behind it is not recoverable.
Going further
Section titled “Going further”Opening it to clients
Section titled “Opening it to clients”The stack ships APP_TRAEFIK_ACCESS=acc-tailscale — reachable over your VPN
only. That is deliberate for setting up, and it is also a decision you have to
revisit: a client portal nobody outside can reach cannot serve clients.
Switch to acc-public once the instance is configured and you have sent
yourself a test invoice.
Framing
Section titled “Framing”The shipped header profile sends X-Frame-Options: DENY, so the client portal
cannot be embedded in someone else’s page. If you want customers to see it
inside their own site, that is the setting to change — otherwise leave it.
Troubleshooting
Section titled “Troubleshooting”The design preview returns an error, but invoices still arrive — the
renderer is running out of shared memory. Docker gives a container 64 MB of
/dev/shm and Chromium needs more; the stack sets shm_size: 512m. Invoice
PDFs keep working because those are rendered by the queue worker with no
request timing out behind them, which makes it look like an interface fault.
docker compose exec app df -h /dev/shm # expect 512M, not 64MA 404 from the proxy, not a 403 — the web server container is unhealthy, and
Traefik drops unhealthy containers from the load balancer. Check
docker compose ps before looking at routing.
Mail does not leave — check the port first, as above, then
docker compose logs app | grep -i mail.
Repository files
Section titled “Repository files”business/invoiceninja/README.md— full setup, backup, diagnosticsbusiness/invoiceninja/UPSTREAM.md— which image variant and why, known deviationsbusiness/invoiceninja/ops/entrypoint.sh— how the secrets reach the application
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content licence not yet decided