diff --git a/.env.sample b/.env.sample index b45fb94..2a6bfeb 100644 --- a/.env.sample +++ b/.env.sample @@ -3,7 +3,62 @@ TYPE=invoiceninja DOMAIN=invoiceninja.example.com LETS_ENCRYPT_ENV=production -SECRET_API_SECRET_VERSION=v1 +# Run this anywhere to generate a key: +# `docker run --rm -it invoiceninja/invoiceninja-debian php artisan key:generate --show` SECRET_APP_KEY_VERSION=v1 -SECRET_DB_PASSWD_VERSION=v1 -SECRET_DB_ROOT_PASSWD_VERSION=v1 +SECRET_DB_PASSWORD_VERSION=v1 +SECRET_DB_ROOT_PASSWORD_VERSION=v1 +SECRET_MAIL_PASSWORD_VERSION=v1 +SECRET_NORDIGEN_KEY_VERSION=v1 + +# IN application vars +APP_URL="https://${DOMAIN}" + +# Create initial user - will default to these values if empty +# ! Set these to whatever you like before the first deploy, then you can remove them later +IN_USER_EMAIL=admin@example.com +IN_PASSWORD=changeme! + +APP_ENV=production +APP_DEBUG=false +REQUIRE_HTTPS=false +PHANTOMJS_PDF_GENERATION=false +PDF_GENERATOR=snappdf +TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + +CACHE_DRIVER=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis + +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +FILESYSTEM_DISK=debian_docker + +# DB connection +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=ninja +DB_USERNAME=ninja +DB_CONNECTION=mysql + +# Mail options +MAIL_MAILER=log +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS='user@example.com' +MAIL_FROM_NAME='Self Hosted User' + +# MySQL +MYSQL_USER=$DB_USERNAME +MYSQL_DATABASE=$DB_DATABASE + +# GoCardless/Nordigen API key for banking integration +NORDIGEN_SECRET_ID= + +IS_DOCKER=true +SCOUT_DRIVER=null +#SNAPPDF_CHROMIUM_PATH=/usr/bin/google-chrome-stable diff --git a/abra.sh b/abra.sh index 0a5b1d5..d8364ba 100644 --- a/abra.sh +++ b/abra.sh @@ -1 +1,4 @@ -export NGINX_CONF_VERSION=v1 +export NGINX_LARAVEL_CONF_VERSION=v1 +export NGINX_INVOICENINJA_CONF_VERSION=v2 + +export ENTRYPOINT_VERSION=v1 diff --git a/compose.yml b/compose.yml index 7a359a5..7b77fc1 100644 --- a/compose.yml +++ b/compose.yml @@ -1,19 +1,21 @@ -version: "3.8" services: web: - image: "nginx:1.21.1" + image: nginx:alpine environment: - DOMAIN=${DOMAIN} configs: - - source: nginx_conf - target: /etc/nginx/nginx.conf + - source: nginx_laravel_conf + target: /etc/nginx/conf.d/laravel.conf + - source: nginx_invoiceninja_conf + target: /etc/nginx/conf.d/invoiceninja.conf volumes: - - "public:/var/www/app/public" + - app_public:/var/www/html/public:ro + - app_storage:/var/www/html/storage:ro networks: - proxy - internal depends_on: - - invoiceninja + - app deploy: update_config: failure_action: rollback @@ -26,78 +28,150 @@ services: - "traefik.http.routers.${STACK_NAME}.middlewares=${STACK_NAME}-redirect" - "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLForceHost=true" - "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLHost=${DOMAIN}" - - coop-cloud.${STACK_NAME}.web.version=1.20.0-c1e6c074 + - coop-cloud.${STACK_NAME}.web.version=xx # @TODO fix me + app: - image: "invoiceninja/invoiceninja:5" - volumes: - - "public:/var/www/app/public" - - "storage:/var/www/app/storage" - secrets: - - api_secret - - app_key - - db_passwd + image: invoiceninja/invoiceninja-debian:5 environment: - - API_SECRET_FILE=/run/secrets/api_secret - - APP_CIPHER=AES-256-CBC - - APP_DEBUG=true - - APP_ENV=production + - APP_URL - APP_KEY_FILE=/run/secrets/app_key - - APP_LOCALE=en - - APP_URL=${DOMAIN} - - DB_DATABASE=ninja - - DB_HOST=mariadb - - DB_PASSWORD_FILE=/run/secrets/db_passwd - - DB_STRICT=false - - DB_TYPE=mysql - - DB_USERNAME=ninja - - LOG=single - - REQUIRE_HTTPS=false - - SESSION_ENCRYPT=true - - SESSION_SECURE=true - - TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + - APP_ENV + - APP_DEBUG + - REQUIRE_HTTPS + - CACHE_DRIVER + - QUEUE_CONNECTION + - SESSION_DRIVER + - REDIS_HOST + - REDIS_PASSWORD + - REDIS_PORT + - FILESYSTEM_DISK + - DB_HOST + - DB_PORT + - DB_DATABASE + - DB_USERNAME + - DB_PASSWORD_FILE=/run/secrets/db_password + - DB_ROOT_PASSWORD_FILE=/run/secrets/db_root_password + - DB_CONNECTION + - IN_USER_EMAIL + - IN_PASSWORD + - MAIL_MAILER + - MAIL_HOST + - MAIL_PORT + - MAIL_USERNAME + - MAIL_PASSWORD_FILE=/run/secrets/mail_password + - MAIL_ENCRYPTION + - MAIL_FROM_ADDRESS + - MAIL_FROM_NAME + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password + - MYSQL_USER + - MYSQL_PASSWORD_FILE=/run/secrets/db_password + - MYSQL_DATABASE + - NORDIGEN_SECRET_ID + - NORDIGEN_SECRET_KEY_FILE=/run/secrets/nordigen_key + - IS_DOCKER + - SCOUT_DRIVER + - SNAPPDF_CHROMIUM_PATH + configs: + - source: entrypoint + target: /custom-entrypoint.sh + mode: 0555 + entrypoint: /custom-entrypoint.sh + secrets: + - app_key + - db_password + - db_root_password + - mail_password + - nordigen_key + deploy: + labels: + - "coop-cloud.${STACK_NAME}.app=true" # Abra won't let me NOT have labels for 'app'? + volumes: + # - ./php/php.ini:/usr/local/etc/php/conf.d/invoiceninja.ini:ro + # - ./php/php-fpm.conf:/usr/local/etc/php-fpm.d/invoiceninja.conf:ro + # - ./supervisor/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf:ro + - app_public:/var/www/html/public + - app_storage:/var/www/html/storage depends_on: - - mariadb + - db + - redis networks: - internal + db: - image: "mariadb:10.6" + image: mariadb:12 environment: - - MYSQL_DATABASE=ninja - - MYSQL_USER=ninja - - MYSQL_PASSWORD_FILE=/run/secrets/db_passwd - - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_passwd + - MYSQL_DATABASE + - MYSQL_USER + - MYSQL_PASSWORD_FILE=/run/secrets/db_password + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password secrets: - - db_root_passwd - - db_passwd + - db_root_password + - db_password volumes: - - "mariadb:/var/lib/mysql" + - db_data:/var/lib/mysql networks: - internal deploy: - labels: ['coop-cloud.${STACK_NAME}.db.version=10.6-718cb856'] + labels: + backupbot.backup.pre-hook: 'mariadb-dump --single-transaction -u root -p"$$(cat /run/secrets/db_root_password)" nextcloud > /var/lib/mysql/backup.sql' + backupbot.backup.volumes.mariadb.path: "backup.sql" + backupbot.restore.post-hook: 'mariadb -u root -p"$$(cat /run/secrets/db_root_password)" nextcloud < /var/lib/mysql/backup.sql' + healthcheck: + test: + [ + "CMD-SHELL", + 'mariadb-admin -p"$$(cat /run/secrets/db_root_password)" ping', + ] + interval: 30s + timeout: 10s + + redis: + image: redis:alpine + volumes: + - redis_data:/data + networks: + - internal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + volumes: - mariadb: - public: - storage: + app_public: + app_storage: + db_data: + redis_data: + networks: proxy: external: true internal: + secrets: - db_root_passwd: - name: ${STACK_NAME}_db_root_passwd_${SECRET_DB_ROOT_PASSWD_VERSION} - external: true - db_passwd: - name: ${STACK_NAME}_db_passwd_${SECRET_DB_PASSWD_VERSION} - external: true app_key: name: ${STACK_NAME}_app_key_${SECRET_APP_KEY_VERSION} external: true - api_secret: - name: ${STACK_NAME}_api_secret_${SECRET_API_SECRET_VERSION} + db_root_password: + name: ${STACK_NAME}_db_root_password_${SECRET_DB_ROOT_PASSWORD_VERSION} external: true + db_password: + name: ${STACK_NAME}_db_password_${SECRET_DB_PASSWORD_VERSION} + external: true + mail_password: + name: ${STACK_NAME}_mail_password_${SECRET_MAIL_PASSWORD_VERSION} + external: true + nordigen_key: + name: ${STACK_NAME}_nordigen_key_${SECRET_NORDIGEN_KEY_VERSION} + external: true + configs: - nginx_conf: - name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION} - file: nginx.conf.tmpl + nginx_laravel_conf: + name: ${STACK_NAME}_nginx_conf_${NGINX_LARAVEL_CONF_VERSION} + file: nginx.laravel.conf.tmpl + template_driver: golang + nginx_invoiceninja_conf: + name: ${STACK_NAME}_nginx_conf_${NGINX_INVOICENINJA_CONF_VERSION} + file: nginx.invoiceninja.conf.tmpl + template_driver: golang + entrypoint: + name: ${STACK_NAME}_entrypoint_${ENTRYPOINT_VERSION} + file: entrypoint.sh.tmpl template_driver: golang diff --git a/entrypoint.sh.tmpl b/entrypoint.sh.tmpl new file mode 100644 index 0000000..a819da2 --- /dev/null +++ b/entrypoint.sh.tmpl @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + + local val="$def" + + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +file_env "APP_KEY" +file_env "DB_PASSWORD" +file_env "DB_ROOT_PASSWORD" +file_env "MAIL_PASSWORD" +file_env "NORDIGEN_KEY" + +/usr/local/bin/init.sh supervisord -c /etc/supervisor/supervisord.conf diff --git a/helpers.sh b/helpers.sh deleted file mode 100755 index f851a55..0000000 --- a/helpers.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -create-secrets () { - pwgen -n 32 1 | docker secret create "${STACK_NAME}_db_root_passwd_${DB_ROOT_PASSWD_VERSION}" - - pwgen -n 32 1 | docker secret create "${STACK_NAME}_db_passwd_${DB_PASSWD_VERSION}" - - pwgen -n 32 1 | docker secret create "${STACK_NAME}_app_key_${APP_KEY_VERSION}" - - pwgen -n 32 1 | docker secret create "${STACK_NAME}_api_secret_${API_SECRET_VERSION}" - -} diff --git a/nginx.conf.tmpl b/nginx.conf.tmpl deleted file mode 100644 index 37a5d31..0000000 --- a/nginx.conf.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -user www-data; - -events { - worker_connections 768; -} - -http { - upstream backend { - server invoiceninja:9000; - } - - include /etc/nginx/mime.types; - default_type application/octet-stream; - gzip on; - gzip_disable "msie6"; - - server { - listen 80 default; - server_name {{ env "DOMAIN" }}; - - root /var/www/app/public; - - index index.php; - - charset utf-8; - - 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; } - - sendfile off; - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass backend; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_intercept_errors off; - fastcgi_buffer_size 16k; - fastcgi_param HTTPS 1; - fastcgi_buffers 4 16k; - } - - location ~ /\.ht { - deny all; - } - } -} diff --git a/nginx.invoiceninja.conf.tmpl b/nginx.invoiceninja.conf.tmpl new file mode 100644 index 0000000..78add9d --- /dev/null +++ b/nginx.invoiceninja.conf.tmpl @@ -0,0 +1,14 @@ +# https://nginx.org/en/docs/http/ngx_http_core_module.html +client_max_body_size 10M; +client_body_buffer_size 10M; +server_tokens off; + +# https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html +fastcgi_buffers 32 16K; + +# https://nginx.org/en/docs/http/ngx_http_gzip_module.html +gzip on; +gzip_comp_level 2; +gzip_min_length 1M; +gzip_proxied any; +gzip_types *; diff --git a/nginx.laravel.conf.tmpl b/nginx.laravel.conf.tmpl new file mode 100644 index 0000000..f04a458 --- /dev/null +++ b/nginx.laravel.conf.tmpl @@ -0,0 +1,32 @@ +# https://laravel.com/docs/master/deployment#nginx +server { + listen 80 default_server; + server_name _; + root /var/www/html/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + + index index.php; + + charset utf-8; + + 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; } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_pass app:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\.(?!well-known).* { + deny all; + } +}