If you bill clients monthly, you have two options for invoicing software. Pay a subscription for a cloud service that stores your client data, payment history, and PDFs on someone else's servers. Or run Invoice Ninja on a VPS you control, pay nothing for the software, and keep every invoice on your own infrastructure.
Invoice Ninja is an open-source invoicing platform built on Laravel. It handles quotes, invoices, recurring billing, expenses, time tracking, and payment gateway integration. The self-hosted version is free and unbranded. You only pay for the VPS it runs on.
This article walks through a production-ready Docker Compose setup on a VPS. By the end you will have a working invoice system at your own domain, with SSL, automated backups, and email delivery configured.
Why self-host your invoicing
Cloud invoicing tools charge per month or per client. For a freelancer with twenty active clients, that adds up to a significant recurring cost. Self-hosting replaces that subscription with the fixed cost of a VPS you are probably already running.
More importantly, self-hosting means you own the data. Your client list, payment history, and generated PDFs live on your server, not in a SaaS database subject to terms-of-service changes or acquisition-driven shutdowns. For freelancers in the EU, this also simplifies GDPR compliance. You know exactly where the data resides and who has access.
If you already run a VPS for client projects, a portfolio site, or a self-hosted app stack, adding Invoice Ninja costs nothing extra. A ServerSpan KVM VPS with 2 GB of RAM comfortably handles Invoice Ninja alongside a reverse proxy and a few other lightweight services.
What you need
- A VPS with at least 2 vCPU and 2 GB of RAM. Invoice Ninja plus MariaDB plus Redis will consume around 1.2 GB at idle.
- Docker and Docker Compose installed.
- A domain or subdomain pointed at your VPS IP, for example
billing.yourdomain.com. - A reverse proxy handling SSL. Traefik or Caddy is recommended. If you need a full walkthrough of VPS setup including reverse proxy configuration, the self-host website guide covers server setup, firewall, and SSL from scratch.
Docker Compose setup
Invoice Ninja provides official Docker images. The recommended stack for version 5 uses four services: the Invoice Ninja application, an nginx web server, a MariaDB database, and Redis for caching and queues.
Directory structure
Create a directory for the stack and an .env file:
mkdir -p /opt/invoiceninja && cd /opt/invoiceninja
Generate the application key
Before you start the containers, generate a random application key. Invoice Ninja uses this for encryption.
openssl rand -base64 32
Copy the output. You will paste it into the APP_KEY field in the environment file.
The docker-compose.yml
services:
app:
image: invoiceninja/invoiceninja:5
restart: unless-stopped
env_file: .env
volumes:
- ./public:/var/www/app/public:rw
- ./storage:/var/www/app/storage:rw
depends_on:
- db
- redis
networks:
- invoiceninja
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "127.0.0.1:8000:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/var/www/app/public:ro
- ./storage:/var/www/app/storage:ro
depends_on:
- app
networks:
- invoiceninja
db:
image: mariadb:10.11
restart: unless-stopped
env_file: .env
volumes:
- db_data:/var/lib/mysql
networks:
- invoiceninja
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- invoiceninja
volumes:
db_data:
redis_data:
networks:
invoiceninja:
Note that nginx binds to 127.0.0.1:8000, not 0.0.0.0:80. This means the Invoice Ninja app is only reachable from localhost on the VPS. Your reverse proxy (Traefik or Caddy) forwards public traffic to port 8000. This pattern keeps the application container off the public internet directly.
The .env file
APP_URL=https://billing.yourdomain.com APP_KEY=base64:YOUR_GENERATED_KEY_HERE APP_DEBUG=false DB_HOST=db DB_PORT=3306 DB_DATABASE=ninja DB_USERNAME=ninja DB_PASSWORD=your_strong_password_here DB_ROOT_PASSWORD=your_strong_root_password_here MAIL_MAILER=smtp MAIL_HOST=smtp.sendgrid.net MAIL_PORT=587 MAIL_USERNAME=apikey MAIL_PASSWORD=your_sendgrid_api_key MAIL_FROM_ADDRESS=billing@yourdomain.com MAIL_FROM_NAME="Your Name" REDIS_HOST=redis REDIS_PORT=6379
Replace the placeholders with your actual values. The APP_URL must match the domain you configured in DNS. The database credentials are shared between the app and db containers.
SSL through your reverse proxy
Since nginx listens on localhost only, your reverse proxy handles TLS termination and public routing. Here is a minimal Traefik label configuration:
labels: - "traefik.enable=true" - "traefik.http.routers.invoiceninja.rule=Host(`billing.yourdomain.com`)" - "traefik.http.routers.invoiceninja.tls.certresolver=letsencrypt" - "traefik.http.services.invoiceninja.loadbalancer.server.port=8000"
If you use Caddy, the equivalent Caddyfile block is:
billing.yourdomain.com {
reverse_proxy localhost:8000
}
First run and setup wizard
Start the stack:
docker compose up -d
Wait about thirty seconds for the database to initialize. Then open https://billing.yourdomain.com in your browser. You will see the Invoice Ninja setup wizard.
During setup:
- Enter your company name, email, and password.
- Select your currency and timezone.
- Skip the SMTP test for now if you have not configured a transactional email provider yet.
If the setup wizard fails to connect to the database, check that the .env credentials match and that the db container is healthy: docker compose ps.
Email configuration
Invoice Ninja sends invoices, reminders, and payment confirmations by email. You need a working SMTP configuration.
Important: If your VPS is hosted with ServerSpan, outbound SMTP on port 25 is blocked by default on IPv4. This restriction does not affect authenticated submission on ports 587 or 465, which is what most email providers use. However, running your own mail server for invoicing is not recommended. Use a transactional email provider instead.
Recommended options:
- SendGrid: Free tier covers most freelancers. Use API key as password.
- Postmark: Reliable deliverability, developer-friendly.
- Brevo (formerly Sendinblue): Free daily sending limit for small volumes.
- Mailgun: Good for higher volumes with programmatic control.
After you configure the provider, update the .env file and restart the app container:
docker compose restart app
Then test email delivery from Settings > Email Settings in the Invoice Ninja dashboard.
PDF generation
Invoice Ninja generates PDFs for every invoice and quote. The Docker image includes wkhtmltopdf, but the exact version matters. If PDF generation fails silently or produces blank pages, the most common cause is a missing or incompatible wkhtmltopdf binary inside the container.
To verify it works:
docker compose exec app wkhtmltopdf --version
If the command returns an error or the version is outdated, rebuild the app image with a fixed wkhtmltopdf installation, or switch to the hosted PDF generation option in Invoice Ninja settings. Hosted generation sends the HTML to Invoice Ninja's servers for rendering. It is easier to set up but sends invoice content externally. For strict data privacy, fix the local binary instead.
Cron for recurring invoices
Recurring invoices, automatic reminders, and scheduled reports all depend on Laravel's scheduler, which needs a cron job. The cleanest approach on Docker is a host-level cron that runs the artisan command inside the app container.
Edit the crontab for root:
sudo crontab -e
Add this line:
* * * * * cd /opt/invoiceninja && docker compose exec -T app php artisan schedule:run >> /dev/null 2>&1
This runs the scheduler every minute. Invoice Ninja internally decides whether to send invoices, reminders, or reports based on the configured intervals.
Backup strategy
Your invoices, client data, and company settings live in two places: the MariaDB volume and the storage directory on disk. Both must be backed up.
The fastest manual backup is a SQL dump plus a tar of the storage directory:
docker compose exec db mariadb-dump -u ninja -p ninja > invoiceninja-backup-$(date +%F).sql tar czf invoiceninja-storage-$(date +%F).tar.gz storage/
For automated backups, the BorgBackup vs Restic comparison explains which deduplicating backup tool handles Docker volume backups without crashing your VPS during the prune phase. Either tool can push encrypted backups to an off-site destination like S3, B2, or another VPS.
At minimum, back up daily and test a restore monthly. An invoice system without tested backups is a data loss event waiting to happen.
Performance on a small VPS
Invoice Ninja is a PHP Laravel application. It is not lightweight, but it is manageable on a small VPS if you configure it correctly.
| Service | Idle RAM | Peak RAM |
|---|---|---|
| MariaDB | 300 MB | 600 MB |
| Invoice Ninja app | 250 MB | 500 MB |
| Redis | 20 MB | 100 MB |
| nginx | 10 MB | 30 MB |
| Total | ~580 MB | ~1.2 GB |
These are approximate values. PDF generation, large CSV exports, and payment gateway API calls spike memory temporarily. A 2 GB VPS handles the stack comfortably at idle but can struggle during concurrent operations.
Set memory limits on the containers to prevent any single service from consuming all RAM. The Docker OOM kill diagnostic guide covers how to set limits and what happens if you skip them.
If you also run a reverse proxy, a database for another app, or any memory-hungry service on the same VPS, consider upgrading to 4 GB. A ServerSpan VPS in the ct.Ready tier (2 Core, 2 GB RAM) is the entry point. If you consistently hit memory limits, the ct.Steady tier (4 Core, 4 GB RAM) gives you room to grow without constant tuning.
FAQ
Invoice Ninja versus FreshBooks or Wave?
| Feature | Invoice Ninja self-hosted | FreshBooks | Wave |
|---|---|---|---|
| Monthly cost | Free (VPS cost only) | Subscription | Free (transaction fees) |
| Data ownership | Full | Provider-hosted | Provider-hosted |
| White-label | Free | Paid tiers | Not available |
| Recurring invoices | Yes | Yes | Yes |
| Payment gateways | Stripe, PayPal, Mollie, 40+ | Limited set | Stripe only |
| API access | Full | Limited | None |
The trade-off is clear. Cloud tools require less setup but charge forever and hold your data. Invoice Ninja requires a VPS and some configuration but costs nothing ongoing and keeps everything local.
Can I migrate from cloud to self-hosted?
Yes. Invoice Ninja supports JSON export from the cloud version and JSON import into the self-hosted version. Export your company data from the cloud dashboard, then use the import tool in your self-hosted instance. Test the import on a staging instance first. Invoice IDs and client references may need adjustment if you already have live data in the self-hosted instance.
Do I need a paid license?
No. The self-hosted version of Invoice Ninja is free and includes full functionality. The paid white-label license removes Invoice Ninja branding from client-facing pages and emails. It costs around thirty dollars per year. For a personal freelancer setup, the free version is sufficient. If you send invoices to enterprise clients who might notice the branding, the white-label license is worth it.
What if email stops sending?
Check three things in this order:
- Verify your SMTP credentials in the
.envfile and restart the app container. - Check the email provider's dashboard for bounce or rate-limit events.
- Check Invoice Ninja's system logs under Settings > System Logs for SMTP connection errors.
If you recently changed your DNS or domain, verify that SPF and DKIM records are configured for your transactional email provider. Without them, invoices land in spam folders or get rejected entirely.
When to scale
A 2 GB VPS is enough for one freelancer with a moderate client list. If you add multiple users, integrate with external accounting tools, or process hundreds of invoices per month, the database and app containers will need more headroom.
Signs you need more RAM:
- PDF generation times exceed ten seconds.
- The dashboard loads slowly even with Redis enabled.
- Container restarts show up in
docker psafter heavy usage. - Other services on the same VPS become unresponsive.
Upgrading from 2 GB to 4 GB is the correct fix. Do not try to compensate with swap. Swap masks the problem and turns a fast SSD into a performance bottleneck.
Running a business means invoicing clients reliably. A self-hosted Invoice Ninja instance on a VPS you control gives you that reliability without the ongoing cost or data lock-in of a cloud service.
Source & Attribution
This article is based on original data belonging to serverspan.com blog. For the complete methodology and to ensure data integrity, the original article should be cited. The canonical source is available at: Self-Hosted Invoice Ninja on Your VPS: The Freelancer's Billing Stack.