diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..5970f59b8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,104 @@ +ARG PHP_VERSION=8.3 +ARG FRANKENPHP_VERSION=latest + +FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} + +ARG TZ=UTC +ARG APP_DIR=/app + +# IMPORTANT: If you're using a reverse proxy use :80, else set your domain name +ENV SERVER_NAME=:80 \ + WITH_SCHEDULER=true \ + WITH_HORIZON=true \ + USER=www-data \ + ROOT=${APP_DIR} + +WORKDIR ${ROOT} + +# INSTALL DEPS AND PHP EXTESIONS +RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - + +RUN apt-get update; \ + apt-get upgrade -yqq; \ + apt-get install -yqq --no-install-recommends --show-progress \ + apt-utils \ + curl \ + wget \ + nano \ + git \ + ncdu \ + procps \ + ca-certificates \ + supervisor \ + libsodium-dev \ + unzip \ + nodejs \ + mariadb-client \ + # Install PHP extensions (included with dunglas/frankenphp) + && install-php-extensions \ + @composer \ + pcntl \ + pdo_mysql \ + gd \ + intl \ + opcache \ + mbstring \ + bcmath \ + gmp \ + zip \ + redis \ + && apt-get -y autoremove \ + && apt-get clean \ + && docker-php-source delete \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && rm /var/log/lastlog /var/log/faillog + + +RUN arch="$(uname -m)" \ + && case "$arch" in \ + armhf) _cronic_fname='supercronic-linux-arm' ;; \ + aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ + x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ + x86) _cronic_fname='supercronic-linux-386' ;; \ + *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ + esac \ + && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.29/${_cronic_fname}" \ + -O /usr/bin/supercronic \ + && chmod +x /usr/bin/supercronic \ + && mkdir -p /etc/supercronic \ + && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel + +RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini + +COPY --link --chown=${USER}:${USER} composer.json composer.lock ./ + +RUN composer install \ + --no-dev \ + --no-interaction \ + --no-autoloader \ + --no-ansi \ + --no-scripts + +COPY --link . . + +RUN mkdir -p \ + storage/framework/{sessions,views,cache,testing} \ + storage/logs \ + bootstrap/cache && chmod -R a+rw storage + +COPY --link resources/docker/supervisord.conf /etc/supervisor/ +COPY --link resources/docker/supervisord.*.conf /etc/supervisor/conf.d/ + +COPY --link resources/docker/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini + +COPY --link resources/docker/start-task-runner /usr/local/bin/start-task-runner + +RUN chmod +x /usr/local/bin/start-task-runner + +# FrankenPHP embedded PHP configuration +COPY --link resources/docker/php.ini /lib/php.ini + +RUN npm install --loglevel=error --no-audit +RUN npm run production + +RUN cat resources/docker/utilities.sh >> ~/.bashrc diff --git a/README.md b/README.md index 55854aa5b..14837d383 100755 --- a/README.md +++ b/README.md @@ -86,3 +86,15 @@ make build-assets ``` This will build all of the assets according to the webpack file. + +## Production Environment with Docker + +You may also want to use Docker in production. We have chosen to use [FrankenPHP](https://frankenphp.dev/docs/), which is a module based on the [Caddy web server](https://caddyserver.com/docs/) and serves as a replacement for PHP-FPM. You will also have access to MariaDB and Redis. + +First, you need to set your domain name in the `SERVER_NAME` of the `Dockerfile` + +Then, to start the production environment, run: + +```bash +docker-compose -f docker-compose.prod.yml up -d +``` diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php new file mode 100644 index 000000000..878baa4bb --- /dev/null +++ b/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,38 @@ +id, [ + 1, + ]); + }); + } +} diff --git a/composer.json b/composer.json index 540848d39..ac36eea47 100644 --- a/composer.json +++ b/composer.json @@ -92,7 +92,8 @@ "spatie/laravel-activitylog": "^4.7", "socialiteproviders/vatsim": "^5.0", "socialiteproviders/ivao": "^4.0", - "mailersend/laravel-driver": "^2.6" + "mailersend/laravel-driver": "^2.6", + "laravel/horizon": "^5.29" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.8.1", diff --git a/composer.lock b/composer.lock index 282c79a6b..eb78f8f36 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d81015ac7af5212239384f3937ff0347", + "content-hash": "1dababd802b659a1d627692fcfd6f4b2", "packages": [ { "name": "akaunting/laravel-money", @@ -4055,6 +4055,86 @@ }, "time": "2023-01-09T14:48:11+00:00" }, + { + "name": "laravel/horizon", + "version": "v5.29.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/horizon.git", + "reference": "a48d242759704e598242074daf0060bbeb6ed46d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/horizon/zipball/a48d242759704e598242074daf0060bbeb6ed46d", + "reference": "a48d242759704e598242074daf0060bbeb6ed46d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "illuminate/contracts": "^9.21|^10.0|^11.0", + "illuminate/queue": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.4", + "predis/predis": "^1.1|^2.0" + }, + "suggest": { + "ext-redis": "Required to use the Redis PHP driver.", + "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ], + "aliases": { + "Horizon": "Laravel\\Horizon\\Horizon" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Horizon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Dashboard and code-driven configuration for Laravel queues.", + "keywords": [ + "laravel", + "queue" + ], + "support": { + "issues": "https://github.com/laravel/horizon/issues", + "source": "https://github.com/laravel/horizon/tree/v5.29.3" + }, + "time": "2024-11-07T21:51:45+00:00" + }, { "name": "laravel/prompts", "version": "v0.1.13", @@ -5101,12 +5181,12 @@ "version": "4.2.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/iso3166.git", + "url": "https://github.com/alcohol/iso3166.git", "reference": "74a08ffe08d4e0dd8ab0aac8c34ea5a641d57669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/iso3166/zipball/74a08ffe08d4e0dd8ab0aac8c34ea5a641d57669", + "url": "https://api.github.com/repos/alcohol/iso3166/zipball/74a08ffe08d4e0dd8ab0aac8c34ea5a641d57669", "reference": "74a08ffe08d4e0dd8ab0aac8c34ea5a641d57669", "shasum": "" }, diff --git a/config/app.php b/config/app.php index de844ffea..48764480d 100755 --- a/config/app.php +++ b/config/app.php @@ -52,6 +52,7 @@ App\Providers\CronServiceProvider::class, App\Providers\DirectiveServiceProvider::class, App\Providers\EventServiceProvider::class, + App\Providers\HorizonServiceProvider::class, App\Providers\MeasurementsProvider::class, App\Providers\ObserverServiceProviders::class, App\Providers\RouteServiceProvider::class, diff --git a/config/horizon.php b/config/horizon.php new file mode 100644 index 000000000..67961ef8d --- /dev/null +++ b/config/horizon.php @@ -0,0 +1,212 @@ + env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['default'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 60, + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + ], + + 'local' => [ + 'supervisor-1' => [ + 'maxProcesses' => 3, + ], + ], + ], +]; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 000000000..fdc816dd8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,83 @@ +services: + app: + build: . + restart: unless-stopped + ports: + - "${HTTP_PORT:-80}:80" # HTTP + - "${HTTPS_PORT:-443}:443" # HTTPS + - "${HTTPS_PORT:-443}:443/udp" # HTTP/3 + volumes: + - caddy_data:/data + - caddy_config:/config + - ./modules:/app/modules + - ./public/uploads:/app/public/uploads + - ./storage:/app/storage + - ./.env:/app/.env + networks: + - internal + depends_on: + - mariadb + - redis + - task-runner + task-runner: + build: . + command: start-task-runner + restart: unless-stopped + volumes: + - ./modules:/app/modules + - ./public/uploads:/app/public/uploads + - ./storage:/app/storage + - ./.env:/app/.env + networks: + - internal + depends_on: + - mariadb + - redis + + mariadb: + image: 'mariadb:11' + restart: unless-stopped + # May be useful if someone wants to access the db remotely + ports: + - '${FORWARD_DB_PORT:-3306}:3306' + environment: + MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_DATABASE: '${DB_DATABASE}' + MYSQL_USER: '${DB_USERNAME}' + MYSQL_PASSWORD: '${DB_PASSWORD}' + MYSQL_ALLOW_EMPTY_PASSWORD: 'no' + volumes: + - mariadb:/var/lib/mysql + networks: + - internal + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 3 + + redis: + image: 'redis:alpine' + restart: unless-stopped +# ports: +# - '${FORWARD_REDIS_PORT:-6379}:6379' + volumes: + - redis:/data + networks: + - internal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + retries: 3 + timeout: 5s + +# Volumes needed for Caddy certificates and configuration +volumes: + caddy_data: + caddy_config: + mariadb: + redis: + +networks: + internal: + driver: bridge \ No newline at end of file diff --git a/resources/docker/.gitignore b/resources/docker/.gitignore index c2f1693ec..03be9d38e 100755 --- a/resources/docker/.gitignore +++ b/resources/docker/.gitignore @@ -2,3 +2,8 @@ !nginx/ !php/ !.gitignore +!php.ini +!start-task-runner +!supervisord.conf +!supervisord.laravel.conf +!utilities.sh \ No newline at end of file diff --git a/resources/docker/nginx/default.conf b/resources/docker/nginx/default.conf deleted file mode 100644 index ced8c907b..000000000 --- a/resources/docker/nginx/default.conf +++ /dev/null @@ -1,27 +0,0 @@ -server { - listen 80 default_server; - server_name phpvms.test; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - root /var/www/public; - index index.php index.html index.htm; - - location / { - try_files $uri $uri/ /index.php$is_args$args; - } - - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass app:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include /etc/nginx/fastcgi_params; - } - - location ~ /\.ht { - deny all; - } -} diff --git a/resources/docker/php.ini b/resources/docker/php.ini new file mode 100644 index 000000000..9c6b29484 --- /dev/null +++ b/resources/docker/php.ini @@ -0,0 +1,31 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +expose_php = 0 +realpath_cache_size = 16M +realpath_cache_ttl = 360 +max_input_time = 5 +memory_limit = 512M + +[Opcache] +opcache.enable = 1 +opcache.enable_cli = 1 +opcache.memory_consumption = 256M +opcache.use_cwd = 0 +opcache.max_file_size = 0 +opcache.max_accelerated_files = 32531 +opcache.validate_timestamps = 0 +opcache.file_update_protection = 0 +opcache.interned_strings_buffer = 16 +opcache.file_cache = 60 + +[JIT] +opcache.jit_buffer_size = 128M +opcache.jit = function +opcache.jit_prof_threshold = 0.001 +opcache.jit_max_root_traces = 2048 +opcache.jit_max_side_traces = 256 + +[zlib] +zlib.output_compression = On +zlib.output_compression_level = 9 \ No newline at end of file diff --git a/resources/docker/php/ext-opcache.ini b/resources/docker/php/ext-opcache.ini deleted file mode 100644 index d5050ea45..000000000 --- a/resources/docker/php/ext-opcache.ini +++ /dev/null @@ -1,8 +0,0 @@ -[opcache] -opcache.enable = 1 -opcache.memory_consumption = 128 -opcache.interned_strings_buffer = 8 -opcache.max_accelerated_files = 4000 -opcache.revalidate_freq = 60 -opcache.fast_shutdown = 1 -opcache.enable_cli = 1 diff --git a/resources/docker/php/www.conf b/resources/docker/php/www.conf deleted file mode 100644 index 725d0d2cc..000000000 --- a/resources/docker/php/www.conf +++ /dev/null @@ -1,15 +0,0 @@ -[www] - -user = www-data -group = www-data - -listen = :9000 - -pm = dynamic -pm.max_children = 5 -pm.start_servers = 2 -pm.min_spare_servers = 1 -pm.max_spare_servers = 3 - -php_flag[display_errors] = on -php_admin_flag[log_errors] = on diff --git a/resources/docker/start-task-runner b/resources/docker/start-task-runner new file mode 100644 index 000000000..93340a4af --- /dev/null +++ b/resources/docker/start-task-runner @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +set -e + +composer dump-autoload --optimize --classmap-authoritative --no-interaction --no-ansi + +# Since this will start before the app, we initialize everything here (allowing us to avoid overriding the app entrypoint). +php artisan storage:link + +php artisan optimize:clear +php artisan cache:clear +php artisan view:clear + +php artisan event:cache +php artisan config:cache +php artisan route:cache +#php artisan view:cache + +php artisan migrate --force + +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.laravel.conf \ No newline at end of file diff --git a/resources/docker/supervisord.conf b/resources/docker/supervisord.conf new file mode 100644 index 000000000..3063a9ca0 --- /dev/null +++ b/resources/docker/supervisord.conf @@ -0,0 +1,16 @@ +[supervisord] +nodaemon=true +#user=%(ENV_USER)s +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[unix_http_server] +file=/var/run/supervisor.sock + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock +#chown=%(ENV_USER)s:%(ENV_USER)s + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface \ No newline at end of file diff --git a/resources/docker/supervisord.laravel.conf b/resources/docker/supervisord.laravel.conf new file mode 100644 index 000000000..d57f7582f --- /dev/null +++ b/resources/docker/supervisord.laravel.conf @@ -0,0 +1,41 @@ +[program:scheduler] +process_name=%(program_name)s_%(process_num)02d +command=supercronic -overlapping /etc/supercronic/laravel +user=root +#user=%(ENV_USER)s +autostart=%(ENV_WITH_SCHEDULER)s +autorestart=true +stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log +stdout_logfile_maxbytes=200MB +stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log +stderr_logfile_maxbytes=200MB + +[program:clear-scheduler-cache] +process_name=%(program_name)s_%(process_num)02d +command=php %(ENV_ROOT)s/artisan schedule:clear-cache +user=root +#user=%(ENV_USER)s +autostart=%(ENV_WITH_SCHEDULER)s +autorestart=false +startsecs=0 +startretries=1 +stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log +stdout_logfile_maxbytes=200MB +stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log +stderr_logfile_maxbytes=200MB + +[program:horizon] +process_name=%(program_name)s_%(process_num)02d +command=php %(ENV_ROOT)s/artisan horizon +user=root +#user=%(ENV_USER)s +autostart=%(ENV_WITH_HORIZON)s +autorestart=true +stdout_logfile=%(ENV_ROOT)s/storage/logs/horizon.log +stdout_logfile_maxbytes=200MB +stderr_logfile=%(ENV_ROOT)s/storage/logs/horizon.log +stderr_logfile_maxbytes=200MB +stopwaitsecs=3600 + +[include] +files=/etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/resources/docker/utilities.sh b/resources/docker/utilities.sh new file mode 100644 index 000000000..4daaa42c6 --- /dev/null +++ b/resources/docker/utilities.sh @@ -0,0 +1,4 @@ +# Commonly used aliases +alias ..="cd .." +alias ...="cd ../.." +alias art="php artisan" \ No newline at end of file