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.
Last run on a host 8 Nov 2026 · data restored from a backup
Pinned at 5.13.53 (released 2 Nov 2026) · the newest upstream release
In the upstream images: 3 CRITICAL · 95 HIGH with a published fix — per image
To try it: Docker, nothing else. See Try it locally.
To deploy it behind the proxy: Traefik running, a domain, and SMTP credentials.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or certificate. From business/invoiceninja/:
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needsAPP_TAG=$(sed -n 's/^APP_TAG=//p' .env.local){ echo "DB_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')" echo "DB_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')" echo "REDIS_PASSWORD=$(openssl rand -hex 32)" echo "IN_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')" echo "APP_KEY=$(docker run --rm "invoiceninja/invoiceninja-debian:$APP_TAG" php artisan key:generate --show | tr -d '\n')"} >> .env.local
docker compose -f docker-compose.local.yml --env-file .env.local up -dThen open http://localhost:8080. Stop it again with the same command and down.
Invoices, quotes, reminders and payment receipts all leave the system by mail. Configure it during setup, not when the first invoice is due.
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[email protected] -
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 invoiceninja-edge \--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[email protected]MAIL_FROM_ADDRESSis the full address here, not just the part before the@. Many hosting companies block port 465 outbound; if yours does, useMAIL_PORT=587withMAIL_ENCRYPTION=tls. -
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”This covers what the backup needs to know about Invoice Ninja. Installing Borgmatic, the repository, the keys and the schedule are on Backup and restore.
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/docker - /var/lib/docker/volumes/invoiceninja_app_storage/_data
mysql_databases: - name: ninja container: invoiceninja-mysql username: ninja password: "{credential file /srv/docker/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=access-vpn — 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 access-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.
Updates
Section titled “Updates”Invoice Ninja runs its database migrations on start; within the v5 series updates are cumulative and need no intermediate version.
-
Read the release notes and check the dockerfiles repository for nginx or compose changes.
-
Back up the database, the storage volume and
.secrets/app_key.txt. -
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -ddocker compose logs app --follow -
Log in and open a recent invoice — the data and its PDF should be intact.
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 whether port 465 is open from the host:
openssl s_client -connect smtp.example.com:465 -brief </dev/nullCONNECTION ESTABLISHED means the port is open; otherwise use MAIL_PORT=587
with MAIL_ENCRYPTION=tls. If the port is fine, read
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 CC BY-NC 4.0