diff --git a/debian/Dockerfile b/debian/Dockerfile index 2be80b5..9f089ea 100644 --- a/debian/Dockerfile +++ b/debian/Dockerfile @@ -1,14 +1,50 @@ -FROM php:8.3-fpm AS base +FROM composer:latest AS composer +RUN curl -s "https://api.github.com/repos/invoiceninja/invoiceninja/releases/latest" | \ + grep -o '"browser_download_url": "[^"]*invoiceninja.tar"' | \ + cut -d '"' -f 4 | \ + xargs curl -sL | \ + tar -xz + +RUN ln -s ./resources/views/react/index.blade.php ./public/index.html + +# Set permissions: directories 755, files 644 +RUN chmod -R a=r,u+w,a+X . + +# Install dependencies +RUN composer install --no-dev --no-scripts --no-autoloader --ignore-platform-reqs + +RUN composer require laravel/octane --ignore-platform-reqs + +RUN composer dump-autoload --optimize + +RUN php artisan storage:link + +# Octane +RUN php artisan octane:install --server=frankenphp + +# ================== +# InvoiceNinja image +# ================== +FROM dunglas/frankenphp:1-php8.3-bookworm + +ARG user=ninja + +# PHP modules ARG php_require="bcmath gd pdo_mysql zip" ARG php_suggest="exif imagick intl pcntl soap saxon-12.5.0" ARG php_extra="opcache" +# Create a system user UID/GID=999 +RUN useradd -r ${user} + +# Allow to bind to privileged ports +RUN setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp + # Install system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ mariadb-client \ gpg \ - supervisor \ # Unicode support for PDF fonts-noto-cjk-extra \ fonts-wqy-microhei \ @@ -16,19 +52,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ xfonts-wqy \ # Install google-chrome-stable(amd64)/chromium(arm64) && if [ "$(dpkg --print-architecture)" = "amd64" ]; then \ - mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | \ - gpg --dearmor -o /etc/apt/keyrings/google.gpg \ - && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends google-chrome-stable; \ + mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | \ + gpg --dearmor -o /etc/apt/keyrings/google.gpg \ + && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends google-chrome-stable \ + && mkdir /config/google-chrome \ + && chown ${user}: /config/google-chrome; \ elif [ "$(dpkg --print-architecture)" = "arm64" ]; then \ - apt-get install -y --no-install-recommends \ - chromium; \ + apt-get install -y --no-install-recommends \ + chromium \ + && mkdir /config/chromium \ + && chown ${user}: /config/chromium; \ fi \ - # Create config directory for chromium/google-chrome-stable - && mkdir /var/www/.config \ - && chown www-data:www-data /var/www/.config \ # Cleanup && apt-get purge -y gpg \ && apt-get autoremove -y \ @@ -36,52 +73,31 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Install PHP extensions -RUN ( curl -sSLf https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions -o - || echo 'return 1' ) | sh -s \ - ${php_require} \ - ${php_suggest} \ - ${php_extra} \ - @composer +RUN install-php-extensions \ + ${php_require} \ + ${php_suggest} \ + ${php_extra} # Configure PHP -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" +RUN mv "${PHP_INI_DIR}/php.ini-production" "${PHP_INI_DIR}/php.ini" -# Copy scripts -COPY rootfs / +# Create directory for artisan tinker (init.sh) +RUN mkdir /config/psysh \ + && chown ${user}: /config/psysh -USER www-data +# Change owner for caddy directories +RUN chown -R ${user}: \ + /data/caddy \ + /config/caddy -WORKDIR /var/www/html +ENTRYPOINT ["/usr/local/bin/init.sh"] -# Setup InvoiceNinja -RUN curl -s "https://api.github.com/repos/invoiceninja/invoiceninja/releases/latest" | \ - grep -o '"browser_download_url": "[^"]*invoiceninja.tar"' | \ - cut -d '"' -f 4 | \ - xargs curl -sL | \ - tar -oxz -C /var/www/html \ - && cp /var/www/html/resources/views/react/index.blade.php /var/www/html/public/index.html \ - # File permissions - && find /var/www/html/ -type f -exec chmod 644 {} \; \ - # Directory permissions - && find /var/www/html/ -type d -exec chmod 755 {} \; \ - # Install dependencies - && composer install --no-dev --no-scripts --no-autoloader \ - && composer dump-autoload --optimize \ - && php artisan optimize \ - && php artisan storage:link \ - # Workaround for application updates - && mv /var/www/html/public /tmp/public +CMD ["frankenphp", "php-cli", "artisan", "octane:frankenphp"] -USER root - -# Setup supervisor -COPY supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +# InvoiceNinja +COPY --from=composer --chown=${user}:${user} /app /app # Add initialization script COPY --chmod=0755 scripts/init.sh /usr/local/bin/init.sh -# Health check -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD php -v || exit 1 - -ENTRYPOINT ["/usr/local/bin/init.sh"] -CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +USER ${user} diff --git a/debian/docker-compose.yml b/debian/docker-compose.yml index e643610..2d14dc6 100644 --- a/debian/docker-compose.yml +++ b/debian/docker-compose.yml @@ -1,47 +1,78 @@ +# name: invoiceninja + x-logging: &default-logging options: max-size: "10m" max-file: "3" driver: json-file +x-app-volumes: &volumes + volumes: + - ./.env:/app/.env + - ./php/php.ini:/usr/local/etc/php/conf.d/zzz-php.ini:ro + - app_cache:/var/www/html/bootstrap/cache + - app_storage:/app/storage + services: app: build: context: . image: invoiceninja/invoiceninja-debian:${TAG:-latest} restart: unless-stopped + # php artisan help octane:frankenphp + command: --log-level=info + ports: + - "80:8000" env_file: - ./.env - volumes: - - ./.env:/var/www/html/.env - - ./php/php.ini:/usr/local/etc/php/conf.d/zzz-php.ini:ro - - ./php/php-fpm.conf:/usr/local/etc/php-fpm.d/zzz-php-fpm.conf:ro - - ./supervisor/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf:ro - - app_cache:/var/www/html/bootstrap/cache - - app_public:/var/www/html/public - - app_storage:/var/www/html/storage - networks: - - app-network + environment: + LARAVEL_ROLE: app + <<: *volumes + # HEALTHCHECK from frankenphp image + healthcheck: + start_period: 180s depends_on: mysql: condition: service_healthy redis: condition: service_healthy + # mariadb: + # condition: service_healthy + # valkey: + # condition: service_healthy + logging: *default-logging + + app-worker: + image: invoiceninja/invoiceninja-debian:${TAG:-latest} + restart: unless-stopped + # php artisan help queue:work + command: --verbose --sleep=3 --tries=3 --max-time=3600 + deploy: + mode: replicated + replicas: 2 + env_file: + - ./.env + environment: + LARAVEL_ROLE: worker + <<: *volumes + depends_on: + app: + condition: service_healthy logging: *default-logging - nginx: - image: nginx:alpine + app-scheduler: + image: invoiceninja/invoiceninja-debian:${TAG:-latest} restart: unless-stopped - ports: - - "80:80" - volumes: - - ./nginx:/etc/nginx/conf.d:ro - - app_public:/var/www/html/public:ro - - app_storage:/var/www/html/storage:ro - networks: - - app-network + # php artisan help schedule:work + command: --verbose + env_file: + - ./.env + environment: + LARAVEL_ROLE: scheduler + <<: *volumes depends_on: - - app + app: + condition: service_healthy logging: *default-logging mysql: @@ -56,8 +87,6 @@ services: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} volumes: - mysql_data:/var/lib/mysql - networks: - - app-network healthcheck: test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}" ] interval: 10s @@ -70,8 +99,6 @@ services: restart: unless-stopped volumes: - redis_data:/data - networks: - - app-network healthcheck: test: [ "CMD", "redis-cli", "ping" ] interval: 10s @@ -79,18 +106,43 @@ services: retries: 5 logging: *default-logging -networks: - app-network: - driver: bridge + # mariadb: + # image: mariadb:11.4 + # restart: unless-stopped + # env_file: + # - ./.env + # 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"] + # start_period: 60s + # logging: *default-logging + + # valkey: + # image: valkey/valkey:8 + # restart: unless-stopped + # volumes: + # - valkey:/data + # healthcheck: + # test: [ "CMD", "valkey-cli", "ping" ] + # start_period: 10s + # logging: *default-logging volumes: app_cache: driver: local - app_public: - driver: local app_storage: - driver: local + driver: local mysql_data: driver: local redis_data: driver: local + # mariadb: + # driver: local + # valkey: + # driver: local diff --git a/debian/nginx/invoiceninja.conf b/debian/nginx/invoiceninja.conf deleted file mode 100644 index 78add9d..0000000 --- a/debian/nginx/invoiceninja.conf +++ /dev/null @@ -1,14 +0,0 @@ -# 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/debian/nginx/laravel.conf b/debian/nginx/laravel.conf deleted file mode 100644 index aa02988..0000000 --- a/debian/nginx/laravel.conf +++ /dev/null @@ -1,32 +0,0 @@ -# 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; - } -} diff --git a/debian/php/php-fpm.conf b/debian/php/php-fpm.conf deleted file mode 100644 index 63bbada..0000000 --- a/debian/php/php-fpm.conf +++ /dev/null @@ -1 +0,0 @@ -pm.max_children = 10 diff --git a/debian/rootfs/docker-entrypoint-init.d/10-init-in.sh b/debian/rootfs/docker-entrypoint-init.d/10-init-in.sh deleted file mode 100644 index 6c03b9d..0000000 --- a/debian/rootfs/docker-entrypoint-init.d/10-init-in.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -php artisan db:seed --force - -# Build up array of arguments... -if [ ! -z "${IN_USER_EMAIL}" ]; then - email="--email ${IN_USER_EMAIL}" -fi - -if [ ! -z "${IN_PASSWORD}" ]; then - password="--password ${IN_PASSWORD}" -fi - -php artisan ninja:create-account $email $password diff --git a/debian/scripts/init.sh b/debian/scripts/init.sh index 8acc15d..bb6ab65 100755 --- a/debian/scripts/init.sh +++ b/debian/scripts/init.sh @@ -1,81 +1,57 @@ -#!/bin/sh -set -e +#!/bin/sh -eu -in_log() { - local type="$1" - shift - printf '%s [%s] [Entrypoint]: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$type" "$*" -} +# Fallback to app +role=${LARAVEL_ROLE:-app} -docker_process_init_files() { - echo - local f - for f; do - case "$f" in - *.sh) - # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 - # https://github.com/docker-library/postgres/pull/452 - if [ -x "$f" ]; then - in_log INFO "$0: running $f" - "$f" - else - in_log INFO "$0: sourcing $f" - . "$f" - fi - ;; - *) in_log INFO "$0: ignoring $f" ;; - esac - echo - done -} +# Check for default CMD, flag(s) or empty CMD +if [ "$*" = 'frankenphp php-cli artisan octane:frankenphp' ] || [ "${1#-}" != "$1" ] || [ "$#" -eq "0" ]; then -if [ "$*" = 'supervisord -c /etc/supervisor/conf.d/supervisord.conf' ]; then - # Workaround for application updates - if [ "$(ls -A /tmp/public)" ]; then - echo "Updating public folder..." - rm -rf /var/www/html/public/.htaccess \ - /var/www/html/public/.well-known \ - /var/www/html/public/* - mv /tmp/public/* \ - /tmp/public/.htaccess \ - /tmp/public/.well-known \ - /var/www/html/public/ - fi - echo "Public Folder is up to date" + # Run app + if [ "${role}" = "app" ]; then + cmd="frankenphp php-cli artisan octane:frankenphp" - # Ensure owner, file and directory permissions are correct - chown -R www-data:www-data \ - /var/www/html/public \ - /var/www/html/storage - find /var/www/html/public \ - /var/www/html/storage \ - -type f -exec chmod 644 {} \; - find /var/www/html/public \ - /var/www/html/storage \ - -type d -exec chmod 755 {} \; + if [ "$APP_ENV" = "production" ]; then + frankenphp php-cli artisan optimize + fi - # Clear and cache config in production - if [ "$APP_ENV" = "production" ]; then - runuser -u www-data -- php artisan optimize - runuser -u www-data -- php artisan package:discover - runuser -u www-data -- php artisan migrate --force + frankenphp php-cli artisan package:discover + + # Run migrations (if any) + frankenphp php-cli artisan migrate --force # If first IN run, it needs to be initialized - echo "Checking initialization status..." - IN_INIT=$(php artisan tinker --execute='echo Schema::hasTable("accounts") && !App\Models\Account::all()->first();') - echo "IN_INIT value: $IN_INIT" + if [ "$(frankenphp php-cli artisan tinker --execute='echo Schema::hasTable("accounts") && !App\Models\Account::all()->first();')" = "1" ]; then + echo "Running initialization..." - if [ "$IN_INIT" = "1" ]; then - echo "Running initialization scripts..." - docker_process_init_files /docker-entrypoint-init.d/* + frankenphp php-cli 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}" + else + echo "Initialization failed - Set IN_USER_EMAIL and IN_PASSWORD in .env" + exit 1 + fi fi echo "Production setup completed" - echo "IN_INIT value: $IN_INIT" - + # Run worker + elif [ "${role}" = "worker" ]; then + cmd="frankenphp php-cli artisan queue:work" + # Run scheduler + elif [ "${role}" = "scheduler" ]; then + cmd="frankenphp php-cli artisan schedule:work" + # Invalid role + else + echo "Invalid role: ${role}" + exit 1 fi - echo "Starting supervisord..." + # Append flag(s) to role cmd + if [ "${1#-}" != "$1" ]; then + set -- ${cmd} "$@" + else + set -- ${cmd} + fi fi exec "$@" diff --git a/debian/supervisor/supervisord.conf b/debian/supervisor/supervisord.conf deleted file mode 100644 index ffa858a..0000000 --- a/debian/supervisor/supervisord.conf +++ /dev/null @@ -1,49 +0,0 @@ -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 - -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock - -[program:php-fpm] -command=/usr/local/sbin/php-fpm -F -autostart=true -autorestart=true -priority=5 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=2 -environment=HOME="/var/www" -stdout_logfile=/var/log/php-worker.log -stderr_logfile=/var/log/php-worker.err.log -stopwaitsecs=3600 - -[program:scheduler] -command=/bin/sh -c "while [ true ]; do (php /var/www/html/artisan schedule:run --verbose --no-interaction &); sleep 60; done" -autostart=true -autorestart=true -user=www-data -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 \ No newline at end of file