diff --git a/.github/workflows/bake.yaml b/.github/workflows/bake.yaml index 57f7afc7..a765445e 100644 --- a/.github/workflows/bake.yaml +++ b/.github/workflows/bake.yaml @@ -47,7 +47,7 @@ jobs: prepare strategy: matrix: - target: [app, scheduler, worker] + target: [aio, app, scheduler, worker] steps: - name: Checkout uses: actions/checkout@v4 @@ -145,7 +145,7 @@ jobs: - build strategy: matrix: - target: [app, scheduler, worker] + target: [aio, app, scheduler, worker] steps: - name: Download meta bake definition diff --git a/Dockerfile b/Dockerfile index 42fe00d3..1e4ab442 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ ARG php_extra="opcache" ARG saxon_edition="HE" # Create a system user UID/GID=999 -RUN useradd -r ${user} +RUN groupadd -g 999 ${user} && \ + useradd -u 999 -g ${user} -r ${user} # Allow to bind to privileged ports RUN setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp @@ -77,25 +78,34 @@ COPY --from=prepare-app --chown=${user}:${user} /app /app # Add initialization script COPY --chmod=0755 ./entrypoint.sh /usr/local/bin/entrypoint.sh -USER ${user} - ENV FILESYSTEM_DISK=debian_docker ENV IS_DOCKER=true ENV SNAPPDF_CHROMIUM_PATH=/usr/bin/chromium ENTRYPOINT ["entrypoint.sh"] +FROM base AS aio +ENV LARAVEL_ROLE=aio +HEALTHCHECK --start-period=100s CMD curl -f http://localhost/health || exit 1 +RUN apt-get update && apt-get install -y --no-install-recommends supervisor && rm -rf /var/lib/apt/lists/* +COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf +CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"] + FROM base AS app +# STOPSIGNAL SIGQUIT ENV LARAVEL_ROLE=app -HEALTHCHECK --start-period=100s CMD curl -f http://localhost/health -CMD ["frankenphp", "php-cli", "artisan", "octane:frankenphp"] +USER ${user} +HEALTHCHECK --start-period=100s CMD curl -f http://localhost/health || exit 1 +CMD ["php", "artisan", "octane:frankenphp"] FROM base AS scheduler ENV LARAVEL_ROLE=scheduler -HEALTHCHECK --start-period=10s CMD pgrep -f schedule:work -CMD ["frankenphp", "php-cli", "artisan", "schedule:work"] +USER ${user} +HEALTHCHECK --start-period=10s CMD pgrep -f schedule:work || exit 1 +CMD ["php", "artisan", "schedule:work", "--no-interaction"] FROM base AS worker ENV LARAVEL_ROLE=worker -HEALTHCHECK --start-period=10s CMD pgrep -f queue:work -CMD ["frankenphp", "php-cli", "artisan", "queue:work"] +USER ${user} +HEALTHCHECK --start-period=10s CMD pgrep -f queue:work || exit 1 +CMD ["php", "artisan", "queue:work", "--no-interaction"] diff --git a/compose/aio-compose.yaml b/compose/aio-compose.yaml new file mode 100644 index 00000000..a9674f66 --- /dev/null +++ b/compose/aio-compose.yaml @@ -0,0 +1,47 @@ +name: invoiceninja + +services: + aio: + image: benbrummer/invoiceninja:latest-aio + restart: unless-stopped + ports: + - "8012:80" # HTTP + env_file: + - ./.env + volumes: + - app_storage:/app/storage + - caddy_data:/data + # - ./php/php.ini:/usr/local/etc/php/conf.d/invoiceninja.ini + depends_on: + mariadb: + condition: service_healthy + valkey: + condition: service_healthy + # tty: true + + mariadb: + image: mariadb:11.8 + restart: unless-stopped + environment: + MARIADB_DATABASE: ${DB_DATABASE} + MARIADB_USER: ${DB_USERNAME} + MARIADB_PASSWORD: ${DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + volumes: + - mariadb:/var/lib/mysql + healthcheck: + test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] + + valkey: + image: valkey/valkey:9 + restart: unless-stopped + volumes: + - valkey:/data + healthcheck: + test: [ "CMD", "valkey-cli", "ping" ] + +volumes: + app_storage: + caddy_data: + mariadb: + valkey: diff --git a/sample.compose.yaml b/compose/multi-compose.yaml similarity index 97% rename from sample.compose.yaml rename to compose/multi-compose.yaml index 013b1d81..61001a35 100644 --- a/sample.compose.yaml +++ b/compose/multi-compose.yaml @@ -10,10 +10,11 @@ services: app: image: benbrummer/invoiceninja:5-app restart: unless-stopped + stop_grace_period: 30s command: --port=80 --workers=2 # command: --host=example.com --port=443 --workers=2 --https --http-redirect --log-level=info ports: - - "80:80" # HTTP + - "8012:80" # HTTP # - "443:443" # HTTPS # - "443:443/udp" # HTTP/3, Works for chromium based browser, but causes H3_GENERAL_PROTOCOL_ERROR for pdf previews in Firefox env_file: diff --git a/sample.env b/compose/sample.env similarity index 100% rename from sample.env rename to compose/sample.env diff --git a/docker-bake.hcl b/docker-bake.hcl index a2046286..f14a62c3 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -20,6 +20,7 @@ variable "MINOR" { group "default" { targets = [ + "aio", "app", "scheduler", "worker" @@ -43,6 +44,13 @@ target _common { pull = true } +target "aio" { + description = "AIO for Invoiceninja Application Image" + inherits = ["_common"] + tags = [for tag in target._common.tags : replace(tag, "-", "-aio")] + target = "aio" +} + target "app" { description = "Invoiceninja Application Image" inherits = ["_common"] diff --git a/entrypoint.sh b/entrypoint.sh index 9befc68d..b13afbb2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,45 +1,22 @@ -#!/bin/sh -eu - -if [ "--help" = "$1" ]; then - echo [FLAGS] - echo The CMD defined can be extended with flags for artisan commands - echo - echo Available flags can be displaced: - echo docker run --rm benbrummer/invoiceninja:5-octane frankenphp php-cli artisan help octane:frankenphp - echo docker run --rm benbrummer/invoiceninja:5-octane-worker frankenphp php-cli artisan help queue:work - echo docker run --rm benbrummer/invoiceninja:5-octane-scheduler frankenphp php-cli artisan help schedule:work - echo - echo Example: - echo docker run benbrummer/invoiceninja:5-octane-worker --verbose --sleep=3 --tries=3 --max-time=3600 - echo - echo [Deployment] - echo Docker compose is recommended - echo - echo Example: - echo https://github.com/benbrummer/dockerfiles/blob/octane-action/sample.compose.yaml - echo - exit 0 -fi - -case "${LARAVEL_ROLE}" in -app) - if [ "$*" = 'frankenphp php-cli artisan octane:frankenphp' ] || [ "${1#-}" != "$1" ]; then - cmd="frankenphp php-cli artisan octane:frankenphp" +#!/bin/bash -eu +# --- ROLE: aio (Runs as root) --- +if [ "${LARAVEL_ROLE}" = 'aio' ]; then + # Clear and cache config in production + if [ "$*" = 'supervisord -c /etc/supervisor/supervisord.conf' ]; then if [ "$APP_ENV" = "production" ]; then - frankenphp php-cli artisan migrate --force - frankenphp php-cli artisan cache:clear # Clear after the migration - frankenphp php-cli artisan ninja:design-update - frankenphp php-cli artisan optimize + echo "Running production setup..." + runuser -u ninja -- php artisan migrate --force + runuser -u ninja -- php artisan cache:clear + runuser -u ninja -- php artisan ninja:design-update + runuser -u ninja -- php artisan optimize - # If first IN run, it needs to be initialized - if [ "$(frankenphp php-cli artisan tinker --execute='echo Schema::hasTable("accounts") && !App\Models\Account::all()->first();')" = "1" ]; then + # Check if initialization is needed + if [ "$(runuser -u ninja -- php artisan tinker --execute='echo Schema::hasTable("accounts") && !App\Models\Account::all()->first();')" = "1" ]; then echo "Running initialization..." - - frankenphp php-cli artisan db:seed --force - + runuser -u ninja -- php artisan db:seed --force if [ -n "${IN_USER_EMAIL}" ] && [ -n "${IN_PASSWORD}" ]; then - frankenphp php-cli artisan ninja:create-account --email "${IN_USER_EMAIL}" --password "${IN_PASSWORD}" + runuser -u ninja -- php artisan ninja:create-account --email "${IN_USER_EMAIL}" --password "${IN_PASSWORD}" else echo "Initialization failed - Set IN_USER_EMAIL and IN_PASSWORD in .env" exit 1 @@ -47,33 +24,77 @@ app) fi fi fi - ;; + echo "Handing off to supervisord..." + # Fall through to exec "$@" at the bottom -scheduler) - if [ "$*" = 'frankenphp php-cli artisan schedule:work' ] || [ "${1#-}" != "$1" ]; then - cmd="frankenphp php-cli artisan schedule:work" +# --- ROLES: app, worker, scheduler (Run as ninja) --- +else + if [ "--help" = "$1" ]; then + echo [FLAGS] + echo The CMD defined can be extended with flags for artisan commands + echo + echo Available flags can be displaced: + echo docker run --rm benbrummer/invoiceninja:5-app php artisan help octane:frankenphp + echo docker run --rm benbrummer/invoiceninja:5-worker php artisan help queue:work + echo docker run --rm benbrummer/invoiceninja:5-scheduler php artisan help schedule:work + echo + echo Example: + echo docker run benbrummer/invoiceninja:5-worker --verbose --sleep=3 --tries=3 --max-time=3600 + echo + echo [Deployment] + echo Docker compose is recommended + echo + echo Example: + echo https://github.com/benbrummer/dockerfiles/blob/main/sample.compose.yaml + echo + exit 0 fi - if [ "$APP_ENV" = "production" ]; then - frankenphp php-cli artisan optimize - fi - ;; + case "${LARAVEL_ROLE}" in + app) + # Check if we should prepend the octane command + if [ $# -eq 0 ] || [[ "$1" == -* ]] || [ "$*" = "php artisan octane:frankenphp" ]; then + if [ "$APP_ENV" = "production" ]; then + echo "Running production setup..." + php artisan migrate --force + php artisan cache:clear + php artisan ninja:design-update + php artisan optimize -worker) - if [ "$*" = 'frankenphp php-cli artisan queue:work' ] || [ "${1#-}" != "$1" ]; then - cmd="frankenphp php-cli artisan queue:work" - fi + if [ "$(php artisan tinker --execute='echo Schema::hasTable("accounts") && !App\Models\Account::all()->first();')" = "1" ]; then + echo "Running initialization..." + php artisan db:seed --force + if [ -n "${IN_USER_EMAIL:-}" ] && [ -n "${IN_PASSWORD:-}" ]; then + php artisan ninja:create-account --email "${IN_USER_EMAIL}" --password "${IN_PASSWORD}" + fi + fi + fi - if [ "$APP_ENV" = "production" ]; then - frankenphp php-cli artisan optimize - fi - ;; + # CRITICAL FIX: Prepend the base command if only flags were passed + if [ $# -eq 0 ] || [[ "$1" == -* ]]; then + set -- php artisan octane:frankenphp "$@" + fi + fi + ;; -esac + scheduler) + [ "$APP_ENV" = "production" ] && php artisan optimize + echo "Starting scheduler ..." + + if [ $# -eq 0 ] || [[ "$1" == -* ]]; then + set -- php artisan queue:work --no-interaction "$@" + fi + ;; + + worker) + [ "$APP_ENV" = "production" ] && php artisan optimize + echo "Starting worker..." + if [ $# -eq 0 ] || [[ "$1" == -* ]]; then + set -- php artisan queue:work --no-interaction "$@" + fi + ;; + esac -# Append flag(s) to role cmd -if [ "${1#-}" != "$1" ] && [ -n "$cmd" ]; then - set -- ${cmd} "$@" fi exec "$@" diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 00000000..d63d231b --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,47 @@ +[supervisord] +nodaemon=true +user=root +pidfile=/var/run/supervisord.pid +logfile=/dev/null +logfile_maxbytes=0 + +[program:app] +command=php artisan octane:frankenphp --port=80 --workers=2 +user=ninja +priority=5 +autostart=true +autorestart=true +startretries=3 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +stopasgroup=true +killasgroup=true + +[program:worker] +process_name=%(program_name)s_%(process_num)02d +command=php artisan queue:work --sleep=3 --tries=3 --max-time=3600 +user=ninja +numprocs=2 +priority=10 +autostart=true +autorestart=true +startretries=3 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +stopasgroup=true +killasgroup=true + +[program:scheduler] +command=php artisan schedule:work +user=ninja +priority=15 +autostart=true +autorestart=true +startretries=3 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +stopasgroup=true +killasgroup=true diff --git a/version.sh b/version.sh index 6f9c6122..30f5f9b4 100755 --- a/version.sh +++ b/version.sh @@ -1,18 +1,27 @@ #!/bin/bash -export VERSION="$(cat version.txt | tr -d v)" -export MAJOR="$(echo "${VERSION}" | cut -d. -f1)" -export MINOR="${MAJOR}.$(echo "${VERSION}" | cut -d. -f2)" -export URL="https://github.com/invoiceninja/invoiceninja/releases/download/v${VERSION}/invoiceninja.tar.gz" +VERSION="$(cat version.txt | tr -d v)" +export VERSION + +MAJOR="$(echo "${VERSION}" | cut -d. -f1)" +export MAJOR + +MINOR="${MAJOR}.$(echo "${VERSION}" | cut -d. -f2)" +export MINOR + +URL="https://github.com/invoiceninja/invoiceninja/releases/download/v${VERSION}/invoiceninja.tar.gz" +export URL echo "Current version: ${VERSION}" if [ "${GITHUB_ACTIONS}" ]; then - echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" - echo "MAJOR=${MAJOR}" >> "${GITHUB_ENV}" - echo "MINOR=${MINOR}" >> "${GITHUB_ENV}" - echo "URL=${URL}" >> "${GITHUB_ENV}" - echo "VERSION=${VERSION}" >> "${GITHUB_OUTPUT}" - echo "MAJOR=${MAJOR}" >> "${GITHUB_OUTPUT}" - echo "MINOR=${MINOR}" >> "${GITHUB_OUTPUT}" - echo "URL=${URL}" >> "${GITHUB_OUTPUT}" + { + echo "VERSION=${VERSION}" + echo "MAJOR=${MAJOR}" + echo "MINOR=${MINOR}" + echo "URL=${URL}" + echo "VERSION=${VERSION}" + echo "MAJOR=${MAJOR}" + echo "MINOR=${MINOR}" + echo "URL=${URL}" + } >>"${GITHUB_OUTPUT}" fi