InvoiceNinja is a great tool to manage clients, invoices and track all sorts of stuff related to you getting paid!
As you may know I'm the sort of chap who runs everything I possibly can using containers. However, setting up a Self-Hosted InvoiceNinja with a reverse proxy - Traefik in this case - wasn't the easiest thing ever. Here's how I did it.
Lots of files
You will need to run 3 different containers (4 if you include Traefik). This is because InvoiceNinja doesn't ship with either a built-in database or web server, no big deal in container land though, we'll just spin those up using compose.
Quite a few moving pieces to this one. We have:
docker-compose.yaml
- defines the 4 containers requiredtraefik.yaml
- configures traefikninja.conf
- to configure nginx
For the purposes of this example we'll use invoiceninja.123.me
as our target domain to be secured with TLS via Traefik. Note that I am using the DNS validation method with Cloudflare for the domain 123.me
, this requires your Cloudflare API key - setting this up is a whole 'nother article though.
Here's the full docker-compose.yaml
file needed for all 4 containers.
---
version: "2"
services:
traefik:
image: traefik
container_name: traefik
volumes:
- /mnt/tank/appdata/traefik:/etc/traefik
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 80:80
- 443:443
- 8080:8080
environment:
- [email protected]
- CLOUDFLARE_API_KEY=123
restart: unless-stopped
mysql:
image: mariadb
container_name: mysql
volumes:
- /mnt/tank/appdata/mysql:/var/lib/mysql
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=123
- MYSQL_PASSWORD=123
- MYSQL_DATABASE=ninja
- MYSQL_USER=ninja
restart: unless-stopped
ninja_nginx:
image: nginx
container_name: ninja_nginx
volumes:
- /mnt/tank/appdata/invoiceninja/ninja.conf:/etc/nginx/conf.d/default.conf:ro
- /mnt/tank/appdata/invoiceninja/storage:/var/www/app/storage
- /mnt/tank/appdata/invoiceninja/public/logo:/var/www/app/logo
- /mnt/tank/appdata/invoiceninja/public:/var/www/app/public
labels:
- traefik.enable=true
- traefik.http.routers.nginx.rule=Host(`invoiceninja.123.me`)
- traefik.http.routers.nginx.entrypoints=websecure
- traefik.http.routers.nginx.tls.certresolver=cloudflare
- traefik.http.services.nginx.loadbalancer.server.port=80
restart: unless-stopped
invoiceninja:
image: invoiceninja/invoiceninja:4.5.18
container_name: invoiceninja
volumes:
- /mnt/tank/appdata/invoiceninja/storage:/var/www/app/storage
- /mnt/tank/appdata/invoiceninja/public/logo:/var/www/app/logo
- /mnt/tank/appdata/invoiceninja/public:/var/www/app/public
environment:
- MYSQL_DATABASE=ninja
- MYSQL_ROOT_PASSWORD=123
- APP_DEBUG=0
- APP_URL=https://invoiceninja.123.me
- APP_KEY=base64:123
- APP_CIPHER=AES-256-CBC
- DB_USERNAME=ninja
- DB_PASSWORD=123
- DB_HOST=mysql
- DB_DATABASE=ninja
- MAIL_HOST=smtp.gmail.com
- [email protected]
- MAIL_PASSWORD=123
- MAIL_DRIVER=smtp
- MAIL_FROM_NAME="Alex"
- [email protected]
- REQUIRE_HTTPS=true
- TRUSTED_PROXIES='*'
depends_on:
- mysql
restart: unless-stopped
This particular method of deploying Traefik requires a config file to be mounted from a volume at /etc/traefik/traefik.yaml
. Here is that file:
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
traefik:
address: ":8080"
ping: {}
providers:
docker:
endpoint: unix:///var/run/docker.sock
watch: true
exposedByDefault: false
api:
dashboard: true
insecure: true
log:
level: debug
certificatesResolvers:
cloudflare:
acme:
email: [email protected]
storage: /etc/traefik/acme.json
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 0
resolvers:
- 1.1.1.1:53
- 1.0.0.1:53
serversTransport:
insecureSkipVerify: true
The final piece of the jigsaw in the nginx configuration file which is mounted again using a volume into the nginx container at /etc/nginx/conf.d/default.conf
. Here is that file:
server {
listen 80 default_server;
server_name invoiceninja.*;
root /var/www/app/public/;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# Handle PHP Applications
location ~ \.php$ {
set $upstream_invoiceninja invoiceninja:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass $upstream_invoiceninja;
fastcgi_index index.php;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_param HTTPS 1;
resolver 127.0.0.11 valid=30s;
}
}