Invoice Ninja mit Docker Compose selbst hosten: Rechnungen, Angebote und Zahlungen für kleine Unternehmen
Invoice Ninja 5.13 per Docker Compose betreiben: Stack aus App, nginx, MySQL 8.4 und Redis aufsetzen, Firma, Kunde, Angebot, Rechnung und Zahlung anlegen, PDF und Mailversand prüfen, Backup und Restore nach Totalverlust nachweisen und den APP_KEY richtig sichern. Mit Lizenzgrenzen und typischen Fehlern.
Geprüft am 09.10.2026 · für Invoice Ninja 5.13.44, Invoice Ninja Dockerfiles 5.13.44-d
Mit KI erstellt – redaktionelle Prüfung ausstehend
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Invoice Ninja ist eine quelloffene Rechnungssoftware für Angebote, Rechnungen, wiederkehrende Abrechnungen, Zahlungen, Zeiterfassung und ein Kundenportal. Diese Anleitung baut den offiziellen Debian-Stack aus invoiceninja/dockerfiles mit festen Versionen nach, richtet Firma, Kunde, Rechnung und Zahlung ein, prüft PDF und Mailversand und zeigt ein Backup, das nach einem Totalverlust zurückkommt. Stand der Prüfung: 9. Oktober 2026, Invoice Ninja 5.13.44 im Container.
Voraussetzungen
Was Invoice Ninja leistet und wo die Grenzen liegen
Invoice Ninja deckt den Ablauf vom Angebot bis zum Zahlungseingang ab, inklusive Mahnungen, Kundenportal und Zahlungsanbietern. Es ist keine Buchhaltung und kein revisionssicheres Archiv. Anforderungen an Aufbewahrung und Rechnungsinhalte klären Sie mit Ihrer Steuerberatung; diese Anleitung ist keine Steuer- oder Rechtsberatung.
Zur Lizenz: Der Quellcode steht unter der Elastic License 2.0. Laut Lizenz-FAQ des Projekts ist die Nutzung zur Abrechnung eigener Kunden kostenlos, auch Dienstleister dürfen sie für Kunden betreiben. Verboten sind der Weiterverkauf als eigenes SaaS und das Umgehen der Lizenzschlüssel-Funktion. Das Invoice-Ninja-Branding in Kundenportal und PDFs entfernt nur eine jährlich kostenpflichtige White-Label-Lizenz, die Admin-Oberfläche behält es.
Zur E-Rechnung: Die E-Invoicing-Dokumentation nennt unter anderem ZUGFeRD/XRechnung, EN 16931 und PEPPOL. Selbsthoster leiten PEPPOL-Rechnungen über die Plattform des Herstellers und brauchen dafür den White-Label-Schlüssel in LICENSE_KEY. Steuern müssen auf Positionsebene stehen, versendete E-Rechnungen sind unveränderlich. Ob das Format Ihren Empfängern genügt, prüfen Sie vorab mit einem Validator.
Technische Ausstattung
- Linux-Server oder VM mit Docker Engine und Docker Compose v2, x86_64 oder ARM64 (das Image gibt es für amd64 und arm64).
- 2 CPU-Kerne und 4 GB RAM. Im Test belegten App rund 710 MB und MySQL rund 925 MB, dazu kommen Redis und nginx.
- Mindestens 15 GB freier Speicher: Das App-Image ist mit Chrome für die PDF-Erzeugung rund 4 GB groß, MySQL 8.4 gut 1 GB.
- Eine eigene Subdomain und ein Reverse Proxy mit TLS; Unterverzeichnisse unterstützt Invoice Ninja nicht.
- Ein SMTP-Postfach mit SPF und DKIM für die Absenderdomain.
| Eckdaten | Wert |
|---|---|
| Image | invoiceninja/invoiceninja-debian:5.13.44 (Tags 5, 5.13, latest zeigen auf dasselbe Image) |
| Upstream-Release | v5.13.45 vom 8. Oktober 2026, Image zum Prüfzeitpunkt noch 5.13.44 |
| Port | nginx auf 127.0.0.1:8080, App intern FastCGI 9000 |
| Volumes | app_storage (Dateien, Logs), mysql_data, app_public, redis_data |
| Wichtige Variablen | APP_URL, APP_KEY, DB_*, IN_USER_EMAIL, IN_PASSWORD, MAIL_* |
| Healthcheck | eingebaut, prüft /health per FastCGI |
Schritt 1: Projektordner, nginx-Konfiguration und APP_KEY
Holen Sie die beiden nginx-Dateien aus dem offiziellen Repository. Sie leiten PHP-Anfragen an die App weiter und setzen das Upload-Limit.
sudo mkdir -p /opt/invoiceninja/nginx
cd /opt/invoiceninja
B=https://raw.githubusercontent.com/invoiceninja/dockerfiles/debian/debian/nginx
sudo curl -fsSL -o nginx/laravel.conf $B/laravel.conf
sudo curl -fsSL -o nginx/invoiceninja.conf $B/invoiceninja.conf
Erzeugen Sie dann den Anwendungsschlüssel (32 zufällige Bytes in Base64):
# Schlüssel erzeugen und ausgeben
echo "base64:$(openssl rand -base64 32)"
Legen Sie ihn zusätzlich im Passwortmanager ab. Invoice Ninja verschlüsselt damit einzelne Daten, im Test etwa die Konfiguration einer Zahlungsart.
Verifizieren: ls nginx zeigt invoiceninja.conf und laravel.conf, der Schlüssel beginnt mit base64: und ist 51 Zeichen lang.
Schritt 2: compose.yaml und .env anlegen
Gegenüber dem offiziellen Stack sind die Versionen festgeschrieben, nginx lauscht nur auf localhost und der build:-Abschnitt fehlt. Speichern Sie als /opt/invoiceninja/compose.yaml:
services:
app:
image: invoiceninja/invoiceninja-debian:${IN_TAG:-5.13.44}
restart: unless-stopped
env_file:
- ./.env
volumes:
- app_public:/var/www/html/public
- app_storage:/var/www/html/storage
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
nginx:
image: nginx:1.30-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- ./nginx:/etc/nginx/conf.d:ro
- app_public:/var/www/html/public:ro
- app_storage:/var/www/html/storage:ro
depends_on:
app:
condition: service_healthy
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u$$MYSQL_USER -p$$MYSQL_PASSWORD"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:8-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
volumes:
app_public:
app_storage:
mysql_data:
redis_data:
Die .env mit Platzhaltern:
APP_URL=https://rechnung.example.de
APP_KEY=base64:HIER_DEN_ERZEUGTEN_SCHLUESSEL_EINTRAGEN
APP_ENV=production
APP_DEBUG=false
REQUIRE_HTTPS=false
PDF_GENERATOR=snappdf
TRUSTED_PROXIES='*'
IS_DOCKER=true
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
FILESYSTEM_DISK=debian_docker
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=ninja
DB_USERNAME=ninja
DB_PASSWORD=bitte-langes-zufallspasswort
DB_ROOT_PASSWORD=bitte-anderes-langes-zufallspasswort
DB_CONNECTION=mysql
# nur für den allerersten Start, danach entfernen
IN_USER_EMAIL=admin@firma.example
IN_PASSWORD=bitte-starkes-startpasswort
MAIL_MAILER=smtp
MAIL_HOST=smtp.provider.example
MAIL_PORT=587
MAIL_USERNAME=rechnung@firma.example
MAIL_PASSWORD=smtp-passwort
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS='rechnung@firma.example'
MAIL_FROM_NAME='Firma GmbH'
Übernehmen Sie aus der Projektvorlage weder APP_DEBUG=true noch die öffentlich bekannten Beispiel-Passwörter. Schützen Sie die Datei mit chmod 600 .env.
Verifizieren: docker compose config --quiet läuft ohne Ausgabe und Warnung durch.
Schritt 3: Stack starten und Healthcheck prüfen
docker compose up -d
docker compose ps
curl -s http://127.0.0.1:8080/health
Der erste Start lädt rund 5 GB Images, spielt das Schema ein und legt das Startkonto an; im Test dauerte das 165 Sekunden. nginx startet erst, wenn die App ihren Healthcheck besteht.
Verifizieren: docker compose ps zeigt app, mysql und redis als healthy, der curl-Aufruf liefert {"status":"ok","message":"API is healthy"}. Im Log steht Production setup completed und danach queue-worker_00 entered RUNNING state.
Schritt 4: Erstanmeldung und Firmendaten
Melden Sie sich unter Ihrer APP_URL mit IN_USER_EMAIL und IN_PASSWORD an und ändern Sie das Passwort sofort im Benutzerprofil. Eine frische Installation heißt „Untitled Company“ und rechnet in US-Dollar. Tragen Sie unter Einstellungen vor der ersten Rechnung Firmenname, Anschrift, USt-ID, Euro und Deutsch ein; im Test trugen frühere Mails noch Dollarbeträge.
Entfernen Sie danach IN_USER_EMAIL und IN_PASSWORD aus der .env.
Verifizieren: Neue Rechnungen zeigen Beträge in Euro, die Seitenleiste oben links nennt Ihren Firmennamen. Ein Login mit falschem Passwort beantwortet die API mit HTTP 400 und These credentials do not match our records.
Schritt 5: Kunde, Angebot, Rechnung, Zahlung und PDF
Legen Sie einen Kunden mit Kontakt und E-Mail-Adresse an. Angebote lassen sich später per Aktion in Rechnungen umwandeln. Für eine Rechnung wählen Sie Rechnungen, Neue Rechnung und tragen Positionen mit Preis, Menge und Steuersatz ein. Fehlt die Steuerspalte, aktivieren Sie Steuern auf Positionsebene in den Steuereinstellungen.

Über Zeige PDF rendert der eingebaute Chrome die Rechnung. Nach dem Versand erfassen Sie den Zahlungseingang unter Zahlungen, die Rechnung wechselt auf Bezahlt.


Verifizieren: Die Rechnungsliste zeigt den Status Bezahlt mit Saldo 0,00 Euro. Per API liefert GET /api/v1/invoice/<invitation_key>/download mit gültigem X-API-TOKEN HTTP 200 und application/pdf; die Datei beginnt mit %PDF.
Schritt 6: E-Mail-Versand einrichten und testen
Die MAIL_*-Werte gelten, solange der E-Mail-Anbieter der Firma auf Standard steht. Zum gefahrlosen Testen fängt Mailpit jede Mail ab:
# compose.mailpit.yaml, nur zum Testen
services:
mailpit:
image: axllent/mailpit:latest
ports:
- "127.0.0.1:8025:8025"
Setzen Sie dafür in der .env MAIL_HOST=mailpit, MAIL_PORT=1025, MAIL_ENCRYPTION=null und starten Sie mit docker compose -f compose.yaml -f compose.mailpit.yaml up -d. Nach Änderungen an der .env muss up -d den Container neu erstellen, ein Neustart reicht nicht.

Die Kundenmail enthält standardmäßig einen Portal-Link, im Test ohne PDF-Anhang.
Verifizieren: docker compose exec app php artisan tinker --execute="echo config('mail.mailers.smtp.host');" gibt Ihren SMTP-Host aus, und eine Testrechnung erscheint im Postfach beziehungsweise in Mailpit unter http://127.0.0.1:8025.
Schritt 7: Reverse Proxy und HTTPS
Davor gehört ein TLS-Proxy, etwa Caddy, siehe Caddy als Reverse Proxy mit automatischem HTTPS:
rechnung.example.de {
reverse_proxy 127.0.0.1:8080
}
APP_URL muss die öffentliche HTTPS-Adresse sein, sonst stimmen Links in Mails nicht. TRUSTED_PROXIES='*' ist nur vertretbar, weil Port 8080 allein auf localhost lauscht.
Verifizieren: curl -I https://rechnung.example.de/ liefert HTTP 200, die Anmeldung im Browser klappt ohne Warnungen zu gemischten Inhalten, und Links in einer Testmail beginnen mit https://.
Schritt 8: Backup und Restore
Ein Backup besteht aus Datenbank-Dump, Storage-Volume (Dokumente, Logos) und .env mit APP_KEY.
cd /opt/invoiceninja && mkdir -p backup
docker compose exec -T mysql sh -c 'exec mysqldump --single-transaction --no-tablespaces -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' | gzip > backup/ninja-db.sql.gz
docker run --rm -v invoiceninja_app_storage:/data:ro -v "$PWD/backup":/backup alpine tar czf /backup/ninja-storage.tar.gz -C /data .
cp .env backup/env.backup
Restore nach Totalverlust: Vor dem Backup legten wir einen Kunden „Restore-Marker“ an und löschten dann mit docker compose down -v alle Volumes. Danach:
docker compose up -d --wait mysql redis
zcat backup/ninja-db.sql.gz | docker compose exec -T mysql sh -c 'exec mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"'
docker volume create invoiceninja_app_storage
docker run --rm -v invoiceninja_app_storage:/data -v "$PWD/backup":/backup:ro alpine tar xzf /backup/ninja-storage.tar.gz -C /data
docker compose up -d --wait
Login, Marker-Kunde, Rechnungen mit Status und PDF-Erzeugung funktionierten wieder.
Gegentest mit neu erzeugtem APP_KEY: Der Login lieferte HTTP 500, storage/logs/laravel.log meldete DecryptException: The MAC is invalid.. Erst der Originalschlüssel half. Ohne .env ist die Sicherung wertlos. Für verschlüsselte Sicherungen außer Haus eignet sich Backrest mit Restic.
Verifizieren: zcat backup/ninja-db.sql.gz | grep -c "INSERT INTO" liefert eine Zahl größer null, und ein Restore auf einem Testsystem bringt einen bekannten Kunden zurück.
Schritt 9: Updates und Rollback
Migrationen führt das Startskript automatisch aus. Erhöhen Sie nach einem Backup die Version in compose.yaml und starten Sie neu:
sed -i 's/5.13.44/5.13.45/' compose.yaml
docker compose pull app && docker compose up -d
docker compose exec app php artisan tinker --execute="echo config('ninja.app_version');"
Prüfen Sie vorher auf Docker Hub, ob der Tag existiert: Zum Prüfzeitpunkt gab es Release 5.13.45 auf GitHub, das Image aber erst bis 5.13.44. Rollback heißt altes Image plus Dump von vor dem Update. Über neue Tags informiert etwa Diun oder What's up Docker.
Verifizieren: Der tinker-Befehl gibt die neue Versionsnummer aus, docker compose ps zeigt app als healthy.
Schritt 10: Invoice Ninja wieder entfernen
Achtung: Der folgende Befehl löscht alle Volumes und damit sämtliche Rechnungen, Kunden und Dokumente unwiderruflich. Sichern Sie vorher wie in Schritt 8 und bewahren Sie die Sicherung entsprechend Ihrer Aufbewahrungspflichten auf.
cd /opt/invoiceninja
docker compose down -v
docker image rm invoiceninja/invoiceninja-debian:5.13.44
Verifizieren: docker volume ls | grep invoiceninja liefert keine Zeile mehr.
Troubleshooting
- App startet in Schleife, Log zeigt
Initialization failed - Set IN_USER_EMAIL and IN_PASSWORD in .env: Bei leerer Datenbank fehlen die Startzugangsdaten; ein Standardkonto greift entgegen der README nicht. SQLSTATE[HY000] [1045] Access denied for user 'ninja', App im StatusRestarting:DB_PASSWORDpasst nicht. MySQL übernimmt das Passwort nur beim ersten Start, spätere Änderungen brauchenALTER USER.- Login liefert HTTP 500, Log meldet
The MAC is invalid.: FalscherAPP_KEY, Originalschlüssel einsetzen. - Upload bricht mit HTTP 413 ab, nginx loggt
client intended to send too large body: Limit 10 MB ausnginx/invoiceninja.conf, dort und im äußeren Proxy erhöhen. Bind for 127.0.0.1:8080 failed: port is already allocated: Port incompose.yamlund Proxy-Ziel ändern.- API antwortet mit 403 und
{"message":"Invalid token"}: HeaderX-API-TOKENfehlt oder ist ungültig.
Häufige Fragen
Brauche ich einen Cronjob für wiederkehrende Rechnungen und Mahnungen?
Nein. Supervisor startet im Image Scheduler und zwei Queue-Worker, prüfbar mit docker compose logs app | grep RUNNING.
Werden Daten an den Hersteller übertragen?
Laut Datenschutzzusatz für Selbsthoster nur Kontodaten des Hauptkontos; Fehlerberichte sind optional. PEPPOL-Rechnungen laufen über die Herstellerplattform.
Ersetzt Invoice Ninja meine Buchhaltung?
Nein. Belege archivieren Sie etwa mit Paperless-ngx, die Buchhaltung bleibt bei Ihrer Finanzsoftware.
Fazit
Das Debian-Image bringt PDF-Erzeugung, Queue und Scheduler mit. Die Arbeit liegt im Betrieb: Version festschreiben, APP_KEY getrennt sichern, Backups samt Restore regelmäßig prüfen und die Lizenzgrenzen kennen. Wer Branding entfernen oder E-Rechnungen über PEPPOL senden will, plant die White-Label-Lizenz ein.
Weiterführende Anleitungen und Quellen
- Docker Compose Grundlagen: Stacks verstehen und verwalten
- Caddy als Reverse Proxy mit automatischem HTTPS
- Backrest: Restic-Backups mit Weboberfläche
- Invoice Ninja auf GitHub mit Release v5.13.45
- Offizieller Debian-Docker-Stack im Repository invoiceninja/dockerfiles
- Image-Tags auf Docker Hub
- Dokumentation der Umgebungsvariablen
- Dokumentation zu Updates
- Erläuterung zur Elastic License 2.0
- White-Label-Lizenz für Selbsthoster
- E-Invoicing-Dokumentation


