feat: enterprise-grade Docker setup for Dokploy deployment
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

- Add multi-stage Dockerfile (node-builder, composer-builder, production)
- Add Nginx config with WebSocket proxy at /_ws path
- Add PHP-FPM pool config, php.ini with OPcache tuning
- Add Supervisord managing 7 processes: php-fpm, nginx, horizon, scheduler, reverb, pulse-check, pulse-work
- Add entrypoint.sh with auto-migration, config caching, storage setup
- Add .dockerignore and .env.production.example
- Install laravel/horizon for production queue management and dashboard
- Install laravel/pulse for production monitoring with Reverb integration
- Configure TrustProxies middleware for HTTPS behind Traefik
- Add horizon:snapshot to scheduler
- Add VITE_REVERB_PATH for WebSocket path routing through Nginx
This commit is contained in:
idevakk
2026-03-09 22:55:05 +05:30
parent c01dcaf4bc
commit dae2bedca4
24 changed files with 1327 additions and 1 deletions

24
.dockerignore Normal file
View File

@@ -0,0 +1,24 @@
.git
.idea
.vscode
.agents
.ai
.claude
.gemini
.junie
node_modules
vendor
npm-debug.log
yarn-error.log
.env
.env.*
*.md
!README.md
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
tests/
phpunit.xml
phpunit.result.cache
docker-compose.yml

View File

@@ -83,6 +83,7 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
VITE_REVERB_PATH=""
ACTIVITY_LOGGER_ENABLED=true
ACTIVITY_LOGGER_TABLE_NAME=activity_log

64
.env.production.example Normal file
View File

@@ -0,0 +1,64 @@
APP_NAME=iMail
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://your-domain.com
LOG_CHANNEL=stderr
LOG_LEVEL=info
# Database (MariaDB via Dokploy/External)
DB_CONNECTION=mariadb
DB_HOST=mariadb-host
DB_PORT=3306
DB_DATABASE=imail
DB_USERNAME=root
DB_PASSWORD=
# MongoDB (via Dokploy/External)
MONGODB_URI=mongodb://mongodb-host:27017
# Redis (via Dokploy/External)
REDIS_CLIENT=phpredis # Required for Laravel Pulse ingest
REDIS_HOST=redis-host
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=s3
# S3 Compatible Storage (RustFS via Dokploy/External)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=imail
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_ENDPOINT=http://rustfs-host:9000
# Reverb Configuration
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST="your-domain.com"
REVERB_PORT=443
REVERB_SCHEME=https
# Pulse Configuration
PULSE_INGEST_DRIVER=redis
PULSE_REDIS_CONNECTION=default
VITE_APP_NAME="${APP_NAME}"
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
VITE_REVERB_PATH="/_ws"

View File

@@ -46,8 +46,10 @@ This application is a Laravel application and its main Laravel ecosystems packag
- filament/filament (FILAMENT) - v4
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/horizon (HORIZON) - v5
- laravel/pint (PINT) - v1
- laravel/prompts (PROMPTS) - v0
- laravel/pulse (PULSE) - v1
- laravel/reverb (REVERB) - v1
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3

View File

@@ -46,8 +46,10 @@ This application is a Laravel application and its main Laravel ecosystems packag
- filament/filament (FILAMENT) - v4
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/horizon (HORIZON) - v5
- laravel/pint (PINT) - v1
- laravel/prompts (PROMPTS) - v0
- laravel/pulse (PULSE) - v1
- laravel/reverb (REVERB) - v1
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3

76
Dockerfile Normal file
View File

@@ -0,0 +1,76 @@
# 1. Node Builder Stage
FROM node:22-alpine AS node-builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY ./ ./
RUN npm run build
# 2. Composer Builder Stage
FROM php:8.4-cli-alpine AS composer-builder
RUN apk add --no-cache unzip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
# Note: ignoring platform requirements since mongo/redis aren't natively in this alpine CLI image
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqs
COPY ./ ./
RUN composer dump-autoload --optimize --no-dev
# 3. Production Stage
FROM php:8.4-fpm-alpine
WORKDIR /var/www/html
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libzip-dev \
icu-dev \
git \
$PHPIZE_DEPS \
linux-headers \
openssl-dev
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
gd \
zip \
intl \
bcmath \
pcntl \
opcache \
sockets
# Install PECL extensions (MongoDB and Redis)
RUN pecl install mongodb redis \
&& docker-php-ext-enable mongodb redis \
&& rm -rf /tmp/pear
# Copy source code and vendor dependencies
COPY . .
COPY --from=composer-builder /app/vendor ./vendor
COPY --from=node-builder /app/public/build ./public/build
# Copy configuration files
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/production.ini
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
# Make entrypoint executable
RUN chmod +x /usr/local/bin/entrypoint.sh
# Cleanup
RUN apk del $PHPIZE_DEPS \
&& rm -rf /var/cache/apk/*
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -46,8 +46,10 @@ This application is a Laravel application and its main Laravel ecosystems packag
- filament/filament (FILAMENT) - v4
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/horizon (HORIZON) - v5
- laravel/pint (PINT) - v1
- laravel/prompts (PROMPTS) - v0
- laravel/pulse (PULSE) - v1
- laravel/reverb (REVERB) - v1
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
parent::boot();
// Horizon::routeSmsNotificationsTo('15556667777');
// Horizon::routeMailNotificationsTo('example@example.com');
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
}
/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewHorizon', function ($user = null) {
return in_array(optional($user)->email, [
//
]);
});
}
}

View File

@@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
$middleware->alias([
'verify.webhook.secret' => VerifyWebhookSecret::class,
]);

View File

@@ -5,4 +5,5 @@ return [
App\Providers\DynamicMailConfigServiceProvider::class,
App\Providers\Filament\DashPanelProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
];

View File

@@ -14,6 +14,8 @@
"jacobtims/filament-logger": "^1.0",
"laravel/fortify": "^1.30",
"laravel/framework": "^12.0",
"laravel/horizon": "^5.45",
"laravel/pulse": "^1.6",
"laravel/reverb": "^1.8",
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1",

284
composer.lock generated
View File

@@ -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": "ac4723b249fb557c0154b1b59679a168",
"content-hash": "cd1592da57958973057c2f7e7a9cfb1d",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -1245,6 +1245,61 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "doctrine/sql-formatter",
"version": "1.5.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/sql-formatter.git",
"reference": "9563949f5cd3bd12a17d12fb980528bc141c5806"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806",
"reference": "9563949f5cd3bd12a17d12fb980528bc141c5806",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^14",
"ergebnis/phpunit-slow-test-detector": "^2.20",
"phpstan/phpstan": "^2.1.31",
"phpunit/phpunit": "^10.5.58"
},
"bin": [
"bin/sql-formatter"
],
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\SqlFormatter\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeremy Dorn",
"email": "jeremy@jeremydorn.com",
"homepage": "https://jeremydorn.com/"
}
],
"description": "a PHP SQL highlighting library",
"homepage": "https://github.com/doctrine/sql-formatter/",
"keywords": [
"highlight",
"sql"
],
"support": {
"issues": "https://github.com/doctrine/sql-formatter/issues",
"source": "https://github.com/doctrine/sql-formatter/tree/1.5.4"
},
"time": "2026-02-08T16:21:46+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.6.0",
@@ -3072,6 +3127,86 @@
},
"time": "2025-11-25T14:46:28+00:00"
},
{
"name": "laravel/horizon",
"version": "v5.45.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
"reference": "637e065ae0a704288595b896ad1c7c3c9741869b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/horizon/zipball/637e065ae0a704288595b896ad1c7c3c9741869b",
"reference": "637e065ae0a704288595b896ad1c7c3c9741869b",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcntl": "*",
"ext-posix": "*",
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
"laravel/sentinel": "^1.0",
"nesbot/carbon": "^2.17|^3.0",
"php": "^8.0",
"ramsey/uuid": "^4.0",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/error-handler": "^6.0|^7.0|^8.0",
"symfony/polyfill-php83": "^1.28",
"symfony/process": "^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0",
"phpstan/phpstan": "^1.10|^2.0",
"predis/predis": "^1.1|^2.0|^3.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|^3.0)."
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Horizon": "Laravel\\Horizon\\Horizon"
},
"providers": [
"Laravel\\Horizon\\HorizonServiceProvider"
]
},
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"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.45.1"
},
"time": "2026-03-06T15:31:27+00:00"
},
{
"name": "laravel/pint",
"version": "v1.27.1",
@@ -3198,6 +3333,94 @@
},
"time": "2026-02-06T12:17:10+00:00"
},
{
"name": "laravel/pulse",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pulse.git",
"reference": "7cde76c1abe23492edeee7dadec01906cf70427d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pulse/zipball/7cde76c1abe23492edeee7dadec01906cf70427d",
"reference": "7cde76c1abe23492edeee7dadec01906cf70427d",
"shasum": ""
},
"require": {
"doctrine/sql-formatter": "^1.4.1",
"guzzlehttp/promises": "^1.0|^2.0",
"illuminate/auth": "^10.48.4|^11.0.8|^12.0",
"illuminate/cache": "^10.48.4|^11.0.8|^12.0",
"illuminate/config": "^10.48.4|^11.0.8|^12.0",
"illuminate/console": "^10.48.4|^11.0.8|^12.0",
"illuminate/contracts": "^10.48.4|^11.0.8|^12.0",
"illuminate/database": "^10.48.4|^11.0.8|^12.0",
"illuminate/events": "^10.48.4|^11.0.8|^12.0",
"illuminate/http": "^10.48.4|^11.0.8|^12.0",
"illuminate/queue": "^10.48.4|^11.0.8|^12.0",
"illuminate/redis": "^10.48.4|^11.0.8|^12.0",
"illuminate/routing": "^10.48.4|^11.0.8|^12.0",
"illuminate/support": "^10.48.4|^11.0.8|^12.0",
"illuminate/view": "^10.48.4|^11.0.8|^12.0",
"laravel/sentinel": "^1.0",
"livewire/livewire": "^3.6.4|^4.0",
"nesbot/carbon": "^2.67|^3.0",
"php": "^8.1",
"symfony/console": "^6.0|^7.0"
},
"conflict": {
"nunomaduro/collision": "<7.7.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.7",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^8.36|^9.15|^10.8",
"pestphp/pest": "^2.0|^3.0|^4.0",
"pestphp/pest-plugin-laravel": "^2.2|^3.0|^4.0",
"phpstan/phpstan": "^1.12.21",
"predis/predis": "^1.0|^2.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Pulse": "Laravel\\Pulse\\Facades\\Pulse"
},
"providers": [
"Laravel\\Pulse\\PulseServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Pulse\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.",
"homepage": "https://github.com/laravel/pulse",
"keywords": [
"laravel"
],
"support": {
"issues": "https://github.com/laravel/pulse/issues",
"source": "https://github.com/laravel/pulse"
},
"time": "2026-02-12T18:51:24+00:00"
},
{
"name": "laravel/reverb",
"version": "v1.8.0",
@@ -3277,6 +3500,65 @@
},
"time": "2026-02-21T14:37:48+00:00"
},
{
"name": "laravel/sentinel",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sentinel.git",
"reference": "7a98db53e0d9d6f61387f3141c07477f97425603"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sentinel/zipball/7a98db53e0d9d6f61387f3141c07477f97425603",
"reference": "7a98db53e0d9d6f61387f3141c07477f97425603",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^8.0"
},
"require-dev": {
"laravel/pint": "^1.27",
"orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0",
"phpstan/phpstan": "^2.1.33"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sentinel\\SentinelServiceProvider"
]
},
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Sentinel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Mior Muhammad Zaki",
"email": "mior@laravel.com"
}
],
"support": {
"source": "https://github.com/laravel/sentinel/tree/v1.0.1"
},
"time": "2026-02-12T13:32:54+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.10",

254
config/horizon.php Normal file
View File

@@ -0,0 +1,254 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Horizon Name
|--------------------------------------------------------------------------
|
| This name appears in notifications and in the Horizon UI. Unique names
| can be useful while running multiple instances of Horizon within an
| application, allowing you to identify the Horizon you're viewing.
|
*/
'name' => env('HORIZON_NAME'),
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain.
|
*/
'domain' => 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,
],
'silenced_tags' => [
// 'notifications',
],
/*
|--------------------------------------------------------------------------
| 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,
],
],
],
/*
|--------------------------------------------------------------------------
| File Watcher Configuration
|--------------------------------------------------------------------------
|
| The following list of directories and files will be watched when using
| the `horizon:listen` command. Whenever any directories or files are
| changed, Horizon will automatically restart to apply all changes.
|
*/
'watch' => [
'app',
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'public/**/*.php',
'resources/**/*.php',
'routes',
'composer.lock',
'composer.json',
'.env',
],
];

244
config/pulse.php Normal file
View File

@@ -0,0 +1,244 @@
<?php
use Laravel\Pulse\Http\Middleware\Authorize;
use Laravel\Pulse\Pulse;
use Laravel\Pulse\Recorders;
return [
/*
|--------------------------------------------------------------------------
| Pulse Domain
|--------------------------------------------------------------------------
|
| This is the subdomain which the Pulse dashboard will be accessible from.
| When set to null, the dashboard will reside under the same domain as
| the application. Remember to configure your DNS entries correctly.
|
*/
'domain' => env('PULSE_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Pulse Path
|--------------------------------------------------------------------------
|
| This is the path which the Pulse dashboard will be accessible from. Feel
| free to change this path to anything you'd like. Note that this won't
| affect the path of the internal API that is never exposed to users.
|
*/
'path' => env('PULSE_PATH', 'pulse'),
/*
|--------------------------------------------------------------------------
| Pulse Master Switch
|--------------------------------------------------------------------------
|
| This configuration option may be used to completely disable all Pulse
| data recorders regardless of their individual configurations. This
| provides a single option to quickly disable all Pulse recording.
|
*/
'enabled' => env('PULSE_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Pulse Storage Driver
|--------------------------------------------------------------------------
|
| This configuration option determines which storage driver will be used
| while storing entries from Pulse's recorders. In addition, you also
| may provide any options to configure the selected storage driver.
|
*/
'storage' => [
'driver' => env('PULSE_STORAGE_DRIVER', 'database'),
'trim' => [
'keep' => env('PULSE_STORAGE_KEEP', '7 days'),
],
'database' => [
'connection' => env('PULSE_DB_CONNECTION'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Pulse Ingest Driver
|--------------------------------------------------------------------------
|
| This configuration options determines the ingest driver that will be used
| to capture entries from Pulse's recorders. Ingest drivers are great to
| free up your request workers quickly by offloading the data storage.
|
*/
'ingest' => [
'driver' => env('PULSE_INGEST_DRIVER', 'storage'),
'buffer' => env('PULSE_INGEST_BUFFER', 5_000),
'trim' => [
'lottery' => [1, 1_000],
'keep' => env('PULSE_INGEST_KEEP', '7 days'),
],
'redis' => [
'connection' => env('PULSE_REDIS_CONNECTION'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Pulse Cache Driver
|--------------------------------------------------------------------------
|
| This configuration option determines the cache driver that will be used
| for various tasks, including caching dashboard results, establishing
| locks for events that should only occur on one server and signals.
|
*/
'cache' => env('PULSE_CACHE_DRIVER'),
/*
|--------------------------------------------------------------------------
| Pulse Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Pulse route, giving you the
| chance to add your own middleware to this list or change any of the
| existing middleware. Of course, reasonable defaults are provided.
|
*/
'middleware' => [
'web',
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Pulse Recorders
|--------------------------------------------------------------------------
|
| The following array lists the "recorders" that will be registered with
| Pulse, along with their configuration. Recorders gather application
| event data from requests and tasks to pass to your ingest driver.
|
*/
'recorders' => [
Laravel\Reverb\Pulse\Recorders\ReverbConnections::class => [
'sample_rate' => 1,
],
Laravel\Reverb\Pulse\Recorders\ReverbMessages::class => [
'sample_rate' => 1,
],
Recorders\CacheInteractions::class => [
'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true),
'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1),
'ignore' => [
...Pulse::defaultVendorCacheKeys(),
],
'groups' => [
'/^job-exceptions:.*/' => 'job-exceptions:*',
// '/:\d+/' => ':*',
],
],
Recorders\Exceptions::class => [
'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true),
'sample_rate' => env('PULSE_EXCEPTIONS_SAMPLE_RATE', 1),
'location' => env('PULSE_EXCEPTIONS_LOCATION', true),
'ignore' => [
// '/^Package\\\\Exceptions\\\\/',
],
],
Recorders\Queues::class => [
'enabled' => env('PULSE_QUEUES_ENABLED', true),
'sample_rate' => env('PULSE_QUEUES_SAMPLE_RATE', 1),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\Servers::class => [
'server_name' => env('PULSE_SERVER_NAME', gethostname()),
'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')),
],
Recorders\SlowJobs::class => [
'enabled' => env('PULSE_SLOW_JOBS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_JOBS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\SlowOutgoingRequests::class => [
'enabled' => env('PULSE_SLOW_OUTGOING_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_OUTGOING_REQUESTS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD', 1000),
'ignore' => [
// '#^http://127\.0\.0\.1:13714#', // Inertia SSR...
],
'groups' => [
// '#^https://api\.github\.com/repos/.*$#' => 'api.github.com/repos/*',
// '#^https?://([^/]*).*$#' => '\1',
// '#/\d+#' => '/*',
],
],
Recorders\SlowQueries::class => [
'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000),
'location' => env('PULSE_SLOW_QUERIES_LOCATION', true),
'max_query_length' => env('PULSE_SLOW_QUERIES_MAX_QUERY_LENGTH'),
'ignore' => [
'/(["`])pulse_[\w]+?\1/', // Pulse tables...
'/(["`])telescope_[\w]+?\1/', // Telescope tables...
],
],
Recorders\SlowRequests::class => [
'enabled' => env('PULSE_SLOW_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_REQUESTS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000),
'ignore' => [
'#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
'#^/telescope#', // Telescope dashboard...
],
],
Recorders\UserJobs::class => [
'enabled' => env('PULSE_USER_JOBS_ENABLED', true),
'sample_rate' => env('PULSE_USER_JOBS_SAMPLE_RATE', 1),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\UserRequests::class => [
'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1),
'ignore' => [
'#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
'#^/telescope#', // Telescope dashboard...
],
],
],
];

View File

@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Pulse\Support\PulseMigration;
return new class extends PulseMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! $this->shouldRun()) {
return;
}
Schema::create('pulse_values', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->mediumText('value');
$table->index('timestamp'); // For trimming...
$table->index('type'); // For fast lookups and purging...
$table->unique(['type', 'key_hash']); // For data integrity and upserts...
});
Schema::create('pulse_entries', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->bigInteger('value')->nullable();
$table->index('timestamp'); // For trimming...
$table->index('type'); // For purging...
$table->index('key_hash'); // For mapping...
$table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries...
});
Schema::create('pulse_aggregates', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('bucket');
$table->unsignedMediumInteger('period');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->string('aggregate');
$table->decimal('value', 20, 2);
$table->unsignedInteger('count')->nullable();
$table->unique(['bucket', 'period', 'type', 'aggregate', 'key_hash']); // Force "on duplicate update"...
$table->index(['period', 'bucket']); // For trimming...
$table->index('type'); // For purging...
$table->index(['period', 'type', 'aggregate', 'bucket']); // For aggregate queries...
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pulse_values');
Schema::dropIfExists('pulse_entries');
Schema::dropIfExists('pulse_aggregates');
}
};

34
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/sh
set -e
echo "Starting iMail container initialization..."
# Ensure storage directories exist
mkdir -p /var/www/html/storage/framework/cache/data
mkdir -p /var/www/html/storage/framework/sessions
mkdir -p /var/www/html/storage/framework/views
mkdir -p /var/www/html/storage/logs
mkdir -p /var/www/html/storage/app/public
mkdir -p /var/www/html/bootstrap/cache
# Fix permissions
chown -R www-data:www-data /var/www/html/storage
chown -R www-data:www-data /var/www/html/bootstrap/cache
# Cache configuration, routes, views, events
echo "Caching configuration and routes..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Create storage symlink
php artisan storage:link
# Run migrations automatically
echo "Running migrations..."
php artisan migrate --force
echo "Initialization complete. Starting Supervisord..."
# Execute supervisord in the foreground
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

71
docker/nginx.conf Normal file
View File

@@ -0,0 +1,71 @@
server {
listen 80;
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;
client_max_body_size 64M;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Laravel routes
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pulse Dashboard
location = /pulse {
try_files $uri $uri/ /index.php?$query_string;
}
# Horizon Dashboard
location = /horizon {
try_files $uri $uri/ /index.php?$query_string;
}
# Reverb WebSocket Proxy
# The trailing slash on proxy_pass strips the /_ws prefix:
# /_ws/app/{key} → /app/{key}
location /_ws/ {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 60m;
proxy_connect_timeout 60m;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

21
docker/php-fpm.conf Normal file
View File

@@ -0,0 +1,21 @@
[global]
error_log = /dev/stderr
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
access.log = /dev/null

21
docker/php.ini Normal file
View File

@@ -0,0 +1,21 @@
[PHP]
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /dev/stderr
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60
max_input_time = 60
variables_order = "EGPCS"
[opcache]
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1

83
docker/supervisord.conf Normal file
View File

@@ -0,0 +1,83 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
[program:php-fpm]
command=php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:horizon]
command=php /var/www/html/artisan horizon
user=www-data
autostart=true
autorestart=true
priority=15
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600
[program:scheduler]
command=/bin/sh -c "while sleep 60; do php /var/www/html/artisan schedule:run; done"
user=www-data
autostart=true
autorestart=true
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:reverb]
command=php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080
user=www-data
autostart=true
autorestart=true
priority=25
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:pulse-check]
command=php /var/www/html/artisan pulse:check
user=www-data
autostart=true
autorestart=true
priority=30
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600
[program:pulse-work]
command=php /var/www/html/artisan pulse:work
user=www-data
autostart=true
autorestart=true
priority=35
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600

BIN
install.log Normal file

Binary file not shown.

View File

@@ -10,6 +10,7 @@ if (document.querySelector('[data-requires-reverb]')) {
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
wsPath: import.meta.env.VITE_REVERB_PATH ?? '',
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

View File

@@ -0,0 +1,19 @@
<x-pulse>
<livewire:pulse.servers cols="full" />
<livewire:pulse.usage cols="4" rows="2" />
<livewire:pulse.queues cols="4" />
<livewire:pulse.cache cols="4" />
<livewire:pulse.slow-queries cols="8" />
<livewire:pulse.exceptions cols="6" />
<livewire:pulse.slow-requests cols="6" />
<livewire:pulse.slow-jobs cols="6" />
<livewire:pulse.slow-outgoing-requests cols="6" />
</x-pulse>

View File

@@ -9,3 +9,4 @@ Artisan::command('inspire', function (): void {
})->purpose('Display an inspiring quote');
Schedule::command('mailboxes:cleanup')->everyMinute();
Schedule::command('horizon:snapshot')->everyFiveMinutes();